PolicyEngine / PolicyEngine/policyengine-core
Parameter uprating takes 11+ seconds per Simulation with reform (700x slowdown)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 22
- Forks
- 30
- Avg merge
- 14h 33m
- Merged PRs (30d)
- 7
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 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)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Reproduce the baseline and reform timings from the issue, then inspect policyengine_core/parameters/operations/uprate_parameters.py and the Simulation construction path. Compare the proposed caching, precomputation, lazy, and algorithm options before choosing a direction; done means reform simulations preserve behavior without the reported redundant uprating slowdown, with a regression benchmark or test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend, performance
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100