-
Notifications
You must be signed in to change notification settings - Fork 26
Open
Labels
bugSomething isn't workingSomething isn't working
Description
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):
- A brand new TaxBenefitSystem is created (cannot reuse baseline)
uprate_parameters()is called to inflate all parameters to target year- This processes all descendants of the parameter tree, even unused ones
- 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 parameterImpact
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
Labels
bugSomething isn't workingSomething isn't working