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 (backward-looking) and capability (from allocation year onwards) 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