Skip to content

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:

  1. What equity principle(s) it implements
  2. What value judgments are embedded
  3. 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:

  1. Know your principles first - You should be able to explain what equity principle the approach implements before writing code
  2. Docstrings include math - Document the mathematical foundation in the docstring (see existing approaches for examples)
  3. Tests validate behavior - Tests should verify both correctness and that parameters have expected effects
  4. 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:

Python
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/:

Python
"""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

  1. Validate inputs using AllocationInputs Pydantic model
  2. Shares must sum to 1 for each group (e.g., climate-assessment, quantile)
  3. Validate outputs using AllocationOutputs model
  4. Document thoroughly with academic citations (see Docstring Template for required structure)

Step 3: Export the Function

Add to the module's __init__.py:

Python
# 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:

Python
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/:

Python
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

  1. Docstring - See Docstring Template for required sections
  2. Science docs - Add to docs/science/allocations.md if introducing new principle
  3. 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

Bash
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