Skip to content

Route UK dated reforms through policyengine-uk's statutory pipeline - #575

Merged
MaxGhenis merged 6 commits into
mainfrom
w1-budget-reform-contract
Oct 12, 2026
Merged

MaxGhenis merged 6 commits into
mainfrom
w1-budget-reform-contract

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #572

Draft: GPT-6.1 Sol approved 7a1acc0 in round 2. One commit since then (0d4e9b8, below) is under review. The numerical behaviour needs PolicyEngine/policyengine-uk#2305.

What this changes

  • Simulation(policy=...) (UK). A policy's parameter values now reach policyengine-uk as an explicit-interval reform dict when the Microsimulation is constructed. Before, simulation_modifier_from_parameter_values edited the already-annualised parameters afterwards. With policyengine-uk#2305, construction writes the changes onto the statutory timeline and reprocesses them as current law: uprating, threshold indexation and 30 April fiscal-year annualisation. That happens before the UC rebalancing inputs and the parameter-gated structural reforms read them. A policy or dynamic with a custom simulation_modifier still runs it after construction, in the same order as before.
  • A modifier no longer replaces its object's parameter values. A Policy or Dynamic carrying both (what Policy + Policy gives when one side has a modifier) ran the modifier instead of the values, on main as well. The order is now: policy values, policy modifier, dynamic values, dynamic modifier.
  • pe.uk.calculate_household(reform=...). Date keys now get the same reading as Simulation(policy=...): YYYY-MM-DD is an effective date, running until the parameter's next date or 2100-12-31. Before, the dict went straight to policyengine-uk, where a lone date is a one-day override, so {"2027-04-06": 0.21} changed nothing.
  • Dict policies accept YYYY-MM-DD.YYYY-MM-DD inclusive intervals, so a finite change can be written as a dict and the sim API can keep stop dates (Policy normalisation drops the stop date of start.stop keys, so bounded reforms never expire policyengine-sim-api#740). They reject bare years, with an explanation: policyengine-uk's dict reads "2027" as fiscal year 2027 only, while policyengine-core's Reform.from_dict reads it as onwards. The US household route keeps its country package's key handling.
  • The UK child benefit household test now asserts that the reform changes the result. Before, it only checked that floats came back.

Invariants

  • Representation. A UK reform reaches policyengine-uk in the same explicit-interval form from the household route and from the policy route.
  • Effective dates. A schedule's order doesn't matter: each date runs until the next date for that parameter.
  • Custom modifiers keep their behaviour, and run after their own object's parameter values.

Behaviour by installed policyengine-uk

pinned 2.125.1 with policyengine-uk#2305
Policy route, January-dated reform same results as before; both edit the annualised tree statutory timeline, reprocessed
Household route, {"2027-04-06": 0.21}: income tax change, £30k earner, 2027 £0 (one-day override) +£174.30
Household route, "2027-04-06.2028-04-05": 2027 / 2028 +£174.30 / £0 +£174.30 / £0

Review

GPT-6.1 Sol round 1 (on 883cfca) asked for three changes. All are fixed in 7a1acc0, and round 2 is running.

  1. Custom policy before a parametric dynamic. The dynamic had joined the construction reform, so it ran before the custom policy modifier. It now runs after that modifier, as before. Tests cover all four policy/dynamic combinations on the population route.
  2. Merge precedence. merge_reform_dicts now puts override periods after the base's, so a dynamic wins where it overlaps a policy period that has a different key.
  3. Unpadded dates. Keys like "2027-4-6", which strptime always accepted, are accepted again.

Round 2 approved 7a1acc0 and noted that an object with both a modifier and parameter values lost the values. That is fixed in 0d4e9b8. test_population_route_applies_policy_before_dynamic now covers eight policy/dynamic combinations.

Tests

  • Pinned 2.125.1: tests/test_uk_dated_reforms.py, test_dict_reforms_on_simulation.py, test_household_impact.py, test_parametric_reforms.py, test_extra_variables.py. 86 passed, 3 skipped (needs policyengine-uk#2305); these figures are from before the review fixes.
  • After the review fixes, with policyengine-uk#2305 installed: test_uk_dated_reforms.py, test_dict_reforms_on_simulation.py, test_parametric_reforms.py. 51 passed.
  • With policyengine-uk#2305 installed: test_uk_dated_reforms.py, test_household_impact.py, test_extra_variables.py, test_uk_analysis.py. 53 passed.
  • After 0d4e9b8: test_uk_dated_reforms.py, 17 passed. CI passed at 19ba06b; it runs again on 0d4e9b8.

The Budget reform contract runner that qualification imports is in a separate PR (W1 of the UK Autumn Budget 2026 CI plan).

🤖 Generated with Claude Code

subfleet and others added 2 commits October 11, 2026 15:33
Simulation(policy=...) now passes a UK policy's parameter values to
policyengine-uk as an explicit-interval reform dict at construction, instead
of editing the already-annualised parameters through a modifier afterwards.
With policyengine-uk#2305, that writes the changes onto the statutory
timeline and reprocesses them as current law (uprating, threshold indexation,
30 April fiscal-year annualisation), before the UC rebalancing inputs and
parameter-gated structural reforms read them. Custom simulation modifiers
still run after construction.

pe.uk.calculate_household(reform=...) gives date keys the same reading as
Simulation(policy=...): a YYYY-MM-DD key is an effective date, so an April
date is no longer a one-day override that changes nothing. Dict policies
accept YYYY-MM-DD.YYYY-MM-DD inclusive intervals, and reject bare years,
which policyengine-uk and policyengine-core read differently.

Fixes #572.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes (GPT-6.1 Sol):
- A custom policy modifier followed by a parametric dynamic ran after the
  dynamic, which joined the construction reform. The dynamic is now applied
  after that modifier, as it was before.
- merge_reform_dicts replaced an existing period in place, so a later
  policy period could override the dynamic where they overlapped. Override
  periods now come after the base's.
- The dict compiler accepts any date strptime('%Y-%m-%d') accepts, as it
  always has, including unpadded months and days.

Tests cover all four policy/dynamic combinations on the population route,
overlapping override precedence, and unpadded dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 11, 2026 18:51
Review notes (GPT-6.1 Sol round 2): the merge docstring claimed the
override always wins an overlap, which holds for policyengine-uk but not
for policyengine-core's start-ordered Reform.from_dict on the US route; and
the compiler's docs and error did not mention unpadded dates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… of them

A Policy or Dynamic sum carrying both parameter values and a simulation
modifier lost its values on the UK population route: the modifier ran in
their place, on main as on this branch. The values now go in at
construction and the modifier runs after them, policy before dynamic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 12, 2026 03:05
@MaxGhenis
MaxGhenis merged commit 2b5ea8f into main Oct 12, 2026
13 checks passed
@MaxGhenis
MaxGhenis deleted the w1-budget-reform-contract branch October 12, 2026 09:37
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.

UK dated reforms skip fiscal-year processing in Simulation(policy=...) and calculate_household, and dict policies cannot carry an end date

1 participant