Skip to content

Parameter uprating takes 11+ seconds per Simulation with reform (700x slowdown) #397

@MaxGhenis

Description

@MaxGhenis

Parameter uprating takes 11+ seconds per Simulation with reform

Problem

Creating a Simulation with a reform is 700x slower than creating a baseline simulation due to redundant parameter uprating.

Profiling results:

Baseline simulation: 0.010s
Reform simulation:   7.029s (700x slower!)

Bottleneck: uprate_parameters() - 11.4s cumulative time
  - 5,747,582 calls to instant()
  - 2,761,186 calls to _get_at_instant()
  - 12,131,056 calls to period helpers
  - 1,702,664 calendar operations

Root Cause

When creating Simulation(situation, reform=reform):

  1. A brand new TaxBenefitSystem is created (cannot reuse baseline)
  2. uprate_parameters() is called to inflate all parameters to target year
  3. This processes all descendants of the parameter tree, even unused ones
  4. The uprated values are not cached between Simulations

From policyengine_core/parameters/operations/uprate_parameters.py:20:

def uprate_parameters(root: ParameterNode) -> ParameterNode:
    descendants = list(root.get_descendants())
    # Processes ALL parameters, whether used or not
    for parameter in descendants:
        if isinstance(parameter, Parameter):
            if parameter.metadata.get("uprating") is not None:
                # Expensive date/period calculations for each parameter

Impact

Any application using reforms is severely degraded:

  • Web calculators (7+ second chart generation delays)
  • Policy analysis notebooks
  • API endpoints with policy variations
  • Research tools comparing scenarios

Proposed Solutions

Option 1: Cache uprated parameters at TaxBenefitSystem level (Quick fix)

class CountryTaxBenefitSystem:
    _uprated_parameters_cache = {}

    def get_parameters_at_instant(self, instant):
        cache_key = (self.parameters_hash, instant)
        if cache_key not in self._uprated_parameters_cache:
            self._uprated_parameters_cache[cache_key] = uprate_parameters(...)
        return self._uprated_parameters_cache[cache_key]

Option 2: Pre-compute uprating at build time (Best performance)

  • Generate uprated parameter files when package is built
  • Ship pre-computed values for common years (e.g., 2020-2030)
  • Fall back to runtime uprating only for years outside pre-computed range

Option 3: Lazy uprating (Most flexible)

  • Only uprate parameters that are actually accessed during simulation
  • Track which parameters are used and uprate on-demand
  • Cache results per TaxBenefitSystem instance

Option 4: Optimize uprate_parameters algorithm

Current: O(n × m) where n = parameters, m = target year - base year

  • Use vectorized operations instead of loops
  • Pre-compute period lookups
  • Avoid redundant calendar calculations

Reproduction

from policyengine_us import Simulation
from policyengine_core.reforms import Reform
import time

# Simple household with axes
situation = {
    "people": {"you": {"age": {2026: 35}}},
    "families": {"your family": {"members": ["you"]}},
    "spm_units": {"your household": {"members": ["you"]}},
    "tax_units": {"your tax unit": {"members": ["you"]}},
    "households": {"your household": {"members": ["you"], "state_name": {2026: "TX"}}},
    "axes": [[{"name": "employment_income", "count": 1001, "min": 0, "max": 1000000}]]
}

reform = Reform.from_dict({
    "gov.aca.ptc_phase_out_rate[0].amount": {"2026-01-01.2100-12-31": 0}
}, country_id="us")

# Baseline: ~0.01s
t0 = time.time()
sim_baseline = Simulation(situation=situation)
print(f"Baseline: {time.time()-t0:.3f}s")

# Reform: ~7s (700x slower!)
t0 = time.time()
sim_reform = Simulation(situation=situation, reform=reform)
print(f"Reform: {time.time()-t0:.3f}s")

Environment

  • policyengine-core: 3.22.3
  • policyengine-us: 1.189.0
  • Python: 3.12

References

  • Affects all country packages (us, uk, canada, etc.)
  • Similar to historical issues with OpenFisca parameter loading
  • Real-world impact: https://github.com/PolicyEngine/ACA-Calc (ACA calculator with 8s chart delays)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions