Adding Allocation Approaches¶
This guide explains how to add new allocation approaches to fair-shares.
Overview¶
Allocation approaches live in two directories:
| Type | Location | Output |
|---|---|---|
| Budget | allocations/budgets/ |
Single cumulative allocation |
| Pathway | allocations/pathways/ |
Time-varying annual allocations |
Each approach is registered in allocations/registry.py.
Before You Begin¶
Every allocation approach operationalizes specific equity principles. Before writing code, you should be able to explain:
- What equity principle(s) it implements
- What value judgments are embedded
- How it relates to existing approaches
If unclear, read Principle-to-Code first.
Value neutrality
Do not present the approach as value-neutral — all allocation choices embed normative judgments [Dooley 2021; Kartha 2018].
Use the Entry Points Framework to structure your normative documentation: (1) foundational principles, (2) allocation quantity, (3) allocation approach, (4) indicators, (5) implications for all others [Pelz 2025b]. See From Principle to Code.
Grandfathering
Grandfathering-based allocation — assigning shares proportional to current emissions — has no support in the equity literature and contradicts the normative foundations of this project [Dooley 2021; Kartha 2018]. Contraction-and-convergence approaches that begin from current national emissions levels embed grandfathering during the convergence period and should be treated with the same caution [Kartha 2018]. New approaches should be grounded in defensible equity principles documented in From Principle to Code.
Contribution Workflow¶
flowchart LR
A[Identify Principles] --> B[Implement Function]
B --> C[Add Tests]
C --> D[Register]
D --> E[Update Docs if Needed]
Key principles:
- Know your principles first - You should be able to explain what equity principle the approach implements before writing code
- Docstrings include math - Document the mathematical foundation in the docstring (see existing approaches for examples)
- Tests validate behavior - Tests should verify both correctness and that parameters have expected effects
- Registration makes it discoverable - Until registered, the approach cannot be used via the high-level API
Step 1: Understand the Pattern¶
Function Signature¶
All allocation functions follow a consistent pattern:
def my_new_budget(
# Required data
population_ts: TimeseriesDataFrame,
allocation_year: int,
emission_category: str,
# Optional data (for adjustments)
country_actual_emissions_ts: TimeseriesDataFrame | None = None,
gdp_ts: TimeseriesDataFrame | None = None,
# Parameters specific to this approach
my_parameter: float = 0.5,
# Common parameters
group_level: str = "iso3c",
unit_level: str = "unit",
) -> BudgetAllocationResult:
"""Docstring with academic context."""
...
Required Return Type¶
- Budget approaches:
BudgetAllocationResult - Pathway approaches:
PathwayAllocationResult
Both are dataclasses in allocations/results/.
Step 2: Implement the Function¶
Budget Example¶
Create a new file or add to existing module in allocations/budgets/:
"""My new allocation approach."""
from fair_shares.library.allocations.results import BudgetAllocationResult
from fair_shares.library.validation.models import AllocationInputs, AllocationOutputs
def my_new_budget(
population_ts,
allocation_year: int,
emission_category: str,
my_parameter: float = 0.5,
group_level: str = "iso3c",
unit_level: str = "unit",
) -> BudgetAllocationResult:
"""
Allocate budget using my new approach.
This approach does X based on principle Y from Author (Year).
Parameters
----------
population_ts : TimeseriesDataFrame
Population data with MultiIndex (iso3c, unit)
allocation_year : int
Year when allocation begins
emission_category : str
Emission category being allocated
my_parameter : float, default 0.5
Controls strength of adjustment
Returns
-------
BudgetAllocationResult
Contains relative_shares_cumulative_emission
See Also
--------
docs/science/allocations.md : Theoretical grounding
"""
# Validate inputs
AllocationInputs(
population_ts=population_ts,
first_allocation_year=allocation_year,
last_allocation_year=allocation_year,
)
# Calculate shares (must sum to 1)
# ... your implementation ...
relative_shares = calculate_shares(population_ts, allocation_year, my_parameter)
# Validate outputs
AllocationOutputs(
shares=relative_shares,
dataset_name="my-new-budget allocation",
)
# Return result
return BudgetAllocationResult(
approach="my-new-budget",
relative_shares_cumulative_emission=relative_shares,
parameters={
"allocation_year": allocation_year,
"my_parameter": my_parameter,
},
)
Key Implementation Details¶
- Validate inputs using
AllocationInputsPydantic model - Shares must sum to 1 for each group (e.g., climate-assessment, quantile)
- Validate outputs using
AllocationOutputsmodel - Document thoroughly with academic citations (see Docstring Template for required structure)
Step 3: Export the Function¶
Add to the module's __init__.py:
# allocations/budgets/__init__.py
from fair_shares.library.allocations.budgets.my_module import my_new_budget
__all__ = [
"equal_per_capita_budget",
"per_capita_adjusted_budget",
"per_capita_adjusted_gini_budget",
"my_new_budget", # Add here
]
Step 4: Register the Approach¶
Add to allocations/registry.py:
from fair_shares.library.allocations.budgets import (
# ... existing imports ...
my_new_budget,
)
def get_allocation_functions() -> dict[str, Callable[..., Any]]:
return {
# ... existing approaches ...
"my-new-budget": my_new_budget,
}
Step 5: Add Tests¶
Create tests in tests/unit/allocations/:
from fair_shares.library.utils import create_example_data
def test_my_new_budget_basic():
"""Test basic functionality."""
data = create_example_data()
result = my_new_budget(
population_ts=data["population"],
allocation_year=2020,
emission_category="co2-ffi",
)
# Shares sum to 1
shares = result.relative_shares_cumulative_emission["2020"]
assert abs(shares.sum() - 1.0) < 1e-10
# Result type
assert result.approach == "my-new-budget"
def test_my_new_budget_parameter_effect():
"""Test that parameter has expected effect."""
data = create_example_data()
result_low = my_new_budget(
population_ts=data["population"],
allocation_year=2020,
emission_category="co2-ffi",
my_parameter=0.0,
)
result_high = my_new_budget(
population_ts=data["population"],
allocation_year=2020,
emission_category="co2-ffi",
my_parameter=1.0,
)
# Verify parameter affects results as expected
assert result_low.approach == "my-new-budget"
assert result_high.approach == "my-new-budget"
Key test assertions:
- Shares sum to 1.0:
abs(shares.sum() - 1.0) < 1e-10 - Approach name matches registry key
- Parameters stored in result object
Step 6: Document¶
- Docstring - See Docstring Template for required sections
- Science docs - Add to
docs/science/allocations.mdif introducing new principle - API docs - Auto-generated from docstrings
Please consider the following:
- Explicit statement or link to documentation regarding normative foundations
- Indicator choices and justifications
- Implications for all parties, not just a focus country
Existing Implementations as Examples¶
| Approach | File | Good Example Of |
|---|---|---|
equal_per_capita_budget |
budgets/per_capita.py |
Simple budget allocation |
per_capita_adjusted_budget |
budgets/per_capita.py |
Multiple adjustments |
cumulative_per_capita_convergence |
pathways/cumulative_per_capita_convergence.py |
Complex pathway allocation |
Common Utilities¶
| Utility | Purpose |
|---|---|
filter_time_columns() |
Extract year columns from DataFrame |
calculate_relative_adjustment() |
Compute pre-allocation responsibility/capability adjustments |
apply_deviation_constraint() |
Limit extreme allocations |
validate_weight_constraints() |
Check weights sum correctly |
See Utils API for full documentation.
Before Merging¶
uv run pytest tests/ -v # Run tests
uv run ruff check src/ tests/ # Lint
uv run ruff format src/ tests/ # Format
uv run mkdocs build --strict # Check docs build
Common issues:
- Shares don't sum to 1.0 — Check for division by zero or NaN values
- Missing year columns — Use
ensure_string_year_columns()on input DataFrames