Skip to content

Structural-trigger parameters are sampled at a fixed instant, so reforms gating structural changes can silently fail to activate #1800

Open

Description

@anth-volk

Summary

Simulation.__init__ creates structural reforms from parameter values once, at a fixed instant:

structural_reform = create_structural_reforms_from_parameters(
    self.tax_benefit_system.parameters,
    period_(self.default_input_period),   # 2025 in current releases
)

Because the trigger parameters (gov.contrib.cps.marriage_tax_reforms.*, gov.contrib.conservatives.cb_hitc_household, gov.contrib.policyengine.disable_simulated_benefits, gov.contrib.scotland.scottish_child_payment) are read only at default_input_period, a reform that sets a trigger for any period not covering that instant changes the parameter but never activates the structural reform it gates. The simulation then returns baseline numbers with no error or warning.

Reproduction (policyengine-uk 2.88.18)

Married couple, one earner at £80,000 (fails the marriage allowance income condition under current law):

  • Reform {"gov.contrib.cps.marriage_tax_reforms.expanded_ma.remove_income_condition": {"2025-01-01.2030-12-31": true}} → structural reform activates, marriage_allowance for 2026 becomes £1,260.
  • Same reform keyed {"2026-01-01.2026-12-31": true} → parameter verifiably changes on the simulation's own system, but marriage_allowance for a 2026 calculation stays £0.

A second, compounding issue

Dict reforms passed as Simulation(reform={...}) go through Scenario.from_reform, which returns a scenario with applied_before_data_load=False — so the parameter changes are applied after create_structural_reforms_from_parameters has already run. On that path a structural trigger can never activate, regardless of period. Constructing the Scenario manually and setting applied_before_data_load=True restores correct behavior; should from_reform default to early application for parameter dicts?

Same pattern in policyengine-us

policyengine_us/system.py does the equivalent with DEFAULT_START_DATE = str(CURRENT_YEAR) + "-01-01" where CURRENT_YEAR = 2024 is hardcoded — the sampling instant there is both fixed and increasingly stale. A fix here probably wants mirroring there.

Suggestions

  • Sample trigger parameters across the periods the reform actually touches (or re-evaluate per requested calculation period), or
  • at minimum, document the constraint that trigger reforms must cover the sampling instant, and
  • consider Scenario.from_reform defaulting to applied_before_data_load=True for dict reforms.

Context

Found while fixing UK /calculate in the PolicyEngine household API, which now applies reforms via a manually-constructed Scenario with applied_before_data_load=True and documents the sampling constraint in its tests.

Activity

  1. self-assigned this
    on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions