Skip to content

Fix open-ended reforms applying only to their start year in microsimulation#454

Merged
MaxGhenis merged 1 commit into
mainfrom
fix/453-open-ended-reform-periods
Jul 7, 2026
Merged

Fix open-ended reforms applying only to their start year in microsimulation#454
MaxGhenis merged 1 commit into
mainfrom
fix/453-open-ended-reform-periods

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Fixes #453

What

reform_dict_from_parameter_values serialised open-ended parameter values (end_date=None) as bare "YYYY-MM-DD" period keys. policyengine-core's Reform.from_dict applies a bare date key for a single instant only, so any open-ended reform — including every flat-dict reform, whose compile path produces open-ended ParameterValues — silently reverted to baseline after its start year in US microsimulation runs.

Every period key now uses the "start.stop" range format:

  • explicit end_date → serialised verbatim, as before
  • open-ended values → stop the day before the next-later value of the same parameter starts (which also makes the documented step-schedule reform shape order-independent), or 2100-12-31 (OPEN_ENDED_REFORM_STOP) when no later value follows

The UK path (simulation_modifier_from_parameter_values) already handled open-ended values correctly via Parameter.update(stop=None) and is unchanged.

Verification

Unit tests: four existing assertions updated from the buggy shape, three new regression tests (open-ended far-future stop; reverse-order step schedule; open-ended + explicit-end mix). Simulation-level suites tests/test_dict_reforms_on_simulation.py, tests/test_us_reform_application.py, and tests/test_household_impact.py pass (41 tests).

End-to-end, the #453 repro through compile_reform_to_policybuild_reform_dictReform.from_dict:

{"gov.irs.income.bracket.rates.7": {"2026-01-01": 0.39}}
before: 2026/2027/2030/2035 = [0.39, 0.37, 0.37, 0.37]
after:  dict = {"gov.irs.income.bracket.rates.7": {"2026-01-01.2100-12-31": 0.39}}
        2026/2027/2030/2035 = [0.39, 0.39, 0.39, 0.39]

and the docs' step-schedule example:

{"gov.irs.income.bracket.rates.7": {"2026-01-01": 0.39, "2028-01-01": 0.40}}
after:  dict = {"...rates.7": {"2026-01-01.2027-12-31": 0.39, "2028-01-01.2100-12-31": 0.40}}
        2026/2027/2030/2035 = [0.39, 0.39, 0.40, 0.40]

Real-world magnitude: a 10-year (2026–2035) revenue estimate of a 39% top rate scored +$17.7B in 2026 and exactly $0.0B in each of 2027–2035 before this fix; after, the years grow +$17.7B → +$33.5B (+$251.4B over the window).

Note

uv.lock includes a one-line true-up of the project's own version (4.18.7 → 4.18.10) produced by uv sync — the committed lock was stale relative to pyproject.toml on main.

🤖 Generated with Claude Code

Open-ended parameter values (end_date=None) serialised as bare date
period keys, which policyengine-core Reform.from_dict applies for a
single instant only — so flat-dict reforms silently reverted to
baseline after their start year in US microsimulation runs. Serialise
every period as an explicit start.stop range, clipping open-ended
values at the next-later value of the same parameter.

Fixes #453

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis marked this pull request as ready for review July 7, 2026 12:24
@MaxGhenis MaxGhenis merged commit 7597291 into main Jul 7, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Open-ended reforms apply only to the start year in microsimulation (bare-date period keys)

1 participant