Skip to content

Key the URL dataset cache on the uprating inputs, and let parameter changes that reach them re-uprate the data - #2405

Merged
MaxGhenis merged 8 commits into
mainfrom
uprating-inputs-reach-data
Oct 15, 2026
Merged

MaxGhenis merged 8 commits into
mainfrom
uprating-inputs-reach-data

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem (verified on main 714b143 by running code with the cached Enhanced FRS 2023-24)

  1. The URL dataset cache couples a macro scenario to every other simulation in the process.
    • _url_dataset_cache holds hf:// and gs:// datasets already extended and uprated with whichever parameters loaded the URL first. It is keyed by the URL alone, plus the CGT projection.
    • The documented macro route is Scenario(parameter_changes={"…yoy_growth.obr.average_earnings": {"year:2027-01-01:1": 0.2}}, applied_before_data_load=True). With it:
      • macro first: the macro's own .baseline and every later plain simulation got 20% employment-income growth in 2027 (should be 2.4%);
      • baseline first: the macro got 2.4% (should be 20%).
    • Either way the scenario's effect on incomes was zero. no_economic_assumptions was exposed the same way.
  2. Parameter changes applied after the data load never reach the dataset's uprating. That includes every reform= dict. A reform dict on average_earnings moved the triple lock and the indices, but incomes in the data still grew 2.4%.

Fix

  • uprating_input_paths() and uprating_input_fingerprint(parameters) in data/economic_assumptions.py. The first lists exactly what the dataset uprating reads; the second digests those parameters' full timelines plus the CGT projection metadata. The read set includes gov.dft.rail.ridership_index and generated indices, not only yoy_growth.
  • The cache is used only when a simulation's uprating inputs are current law's. Any other simulation builds its own dataset. Existing keys are unchanged.
  • A scenario's parameter changes that change the uprating inputs are written before the data is built, so its projected years follow. Every other scenario keeps today's timing, so ordinary reforms are unaffected.
Real FRS, employment income growth 2027 main this branch
Macro first: macro / its .baseline / later baseline 20.0% / 20.0% / 20.0% 20.0% / 2.4% / 2.4%
Baseline first: macro 2.4% 20.0%
reform={AE: {"year:2027-01-01:1": 0.2}} 2.4% 20.0% (= before-load Scenario)
  • The probe that decides whether a scenario's changes reach the uprating runs only for sources the extension projects. A situation, a DataFrame and a multi-year dataset keep today's path, so household calculations build no extra system.

With #2407 (merged into this branch). #2407 splits a scenario sum by data-load timing.

  • A sum with a before-load part already applies all its parameter changes before the build, so this PR's probe only decides for scenarios without one, on their after-load part.
  • _dataset_cache_key keeps Run each part's modifier at its own time in a sum of scenarios #2407's before-load check (a before-load structural reform can change how the data is read, such as an enum order, without changing any parameter) and adds this PR's uprating fingerprint check after it.

#2417. On the URL-cache route of #2417, #2407's before-load check already keeps a no_economic_assumptions simulation out of the cache. This PR adds a second guard, because the scenario's uprating fingerprint differs from current law's. The other routes in that issue, where callers overwrite self.dataset after the reset copy, are unchanged here.

Reproducer: policyengine_uk/tests/test_uprating_inputs_reach_data.py::test_a_macro_scenario_and_current_law_get_their_own_data
Reproducer: policyengine_uk/tests/test_uprating_inputs_reach_data.py::test_a_growth_reform_dict_projects_the_data_as_the_before_load_route

Invariants (tested in test_uprating_inputs_reach_data.py, MOCK datasets only)

  1. Cache isolation. A simulation whose uprating inputs differ from current law's neither reads nor writes the cache. So it gets its own projection, and everyone else gets current law's, its own .baseline included. This is tested in both orders, for hf:// and gs://, and for no_economic_assumptions.
  2. Sharing. Current-law simulations still share one extension, ordinary reforms and their baselines included. Extension calls are counted.
  3. Data follows the parameters.
    • A growth reform dict projects the data exactly as the before-load route does.
    • Property: for any rate and year, the data grows by the rate in that year and by current law's in every other year.
    • An ordinary reform keeps the data and the after-build timing; the event order is recorded.
    • A macro scenario summed with a reform still projects the data.
  4. Fingerprint.
    • Property: changing any one read value changes the fingerprint.
    • A read-recording guard runs a real extension over a MOCK dataset carrying every uprated variable, and asserts that every parameter it reads is covered. The rail ridership index is read only past the 2030 horizon. A mutation check confirmed the guard fails without it.

Tests

Run Result
New file + test_simulation_dataset_sources.py + test_variable_module_release.py, at acb8cc5 (before the P3 fixes and merges) 34 passed
The 72 existing test files that touch reforms, scenarios, datasets or uprating (on 074e9e1, before the probe shortcut) 1,385 passed, 7 skipped
At 2581da7 (with #2404 and #2407): the new file, test_simulation_dataset_sources.py, test_variable_module_release.py, test_process_time_reforms.py, test_dated_reform_routes.py, test_no_economic_assumptions.py, test_scenario_sum_timing.py 219 passed

Review

Round 1 (independent Opus 5.5 review on a Subfleet lane, at de78bb8): requested changes. Its one blocking finding:

  • The fingerprint tests built a tax-benefit system while the tests were collected. That failed the repo's existing check test_variable_module_release.py::test_no_system_dies_before_the_freeze. The tests now read the module's system and clone its parameters.

It also noted, as non-blocking, that the probe built a whole new system (every variable loaded) for each dataset reform. The probe now rebuilds on a shallow copy of the simulation's own system (CountryTaxBenefitSystem.parameters_with), which leaves that system untouched; a test checks this.

Both fixes are in 3eaa753. The reviewer otherwise confirmed:

  • both cache orders on hf:// and gs:// with the real FRS (macro 0.2000, its baseline and other simulations 0.0240);
  • reform-dict data growth of 0.2000 for employment income, social rent and England council tax;
  • one rebuild for probe plus application (the memo hit);
  • unknown-path messages unchanged;
  • the read set covering every current read.

Round 2 (confirming review at acb8cc5): APPROVE.

  • Round 1's P1 is fixed: the collection check passes, and it still fails at de78bb8.
  • parameters_with left the original system untouched in six starting states.
  • One rebuild for probe plus application (the memo hit).
  • Real-FRS cache orders and data growth are as in the table.
  • 151 tests passed.

Its three P3 notes are fixed in a84a386:

  • a failing probe cleared the original's node caches; the probe now clones the tree;
  • the memo docstring now says when the memo applies;
  • parameters_with([]) returns a copy.

Since then. The branch merges #2404 (5b09051) and #2407 (2581da7).

Companion: #2404, the parameter side (calendar-year bare years and generated series), merged as 655b594 and merged into this branch.

Round 3 (confirming review at 2581da7): APPROVE. It checked:

  • the P3 fixes: 9 failing probes on five kinds of system leave all 1,494 filled node caches intact;
  • both merge resolutions, with a 33-case comparison against main: 17 cases match exactly, and the 16 that differ are the intended after-only changes that reach the uprating inputs;
  • after-load against before-load for six reforms: identical projections;
  • the real-FRS cache orders and data growth;
  • the five test files: 140 passed.

Left as non-blocking:

  • about 0.6–1.0 s more per dataset simulation with a reform (one parameter rebuild either way; the tree clone is 0.06–0.2 s of it);
  • a stale test comment;
  • the docs' cache sentence, which doesn't mention that a before-load part also skips the cache.

axiom: n/a: infra

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 14, 2026 02:28
…hanges that reach them re-uprate the data

The cache held datasets already extended and uprated with whichever
parameters loaded the URL first. A macro scenario built first left its
uprated incomes for its own baseline and every later simulation; built
second, it got current-law data. Datasets are now cached only when the
parameters the uprating reads are current law's; any other simulation
builds its own.

A scenario applied after data load, which includes every reform dict,
wrote its parameter changes after the data was built, so a changed
growth rate moved the triple lock and the indices but not the incomes in
the data. Changes that change what the uprating reads are now written
before the data is built.

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

MOCK datasets only. Both cache orders on hf:// and gs://, no economic
assumptions, one shared extension under current law, reform dict versus
the before-load route, the after-build timing of ordinary reforms, a
property over growth rates and years, the fingerprint's sensitivity to
every read input, and a read-recording guard that the fingerprint covers
everything the extension reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 14, 2026 06:26
A situation, a DataFrame and a multi-year dataset are taken as given, so
their scenarios keep the after-build timing without building a probe
system to compare uprating inputs.

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

The fingerprint tests built a tax-benefit system while the tests were
collected, which the collection-lifetime check rejects; they now read the
module's system. The uprating probe rebuilt parameters on a whole new
system, loading every variable; it now rebuilds them on a shallow copy of
the simulation's own system (CountryTaxBenefitSystem.parameters_with).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 2 commits October 14, 2026 15:14
parameters_with rebuilt on a shallow copy that shared the system's tree,
so a failing rebuild restored that tree onto the copy and its cache reset
emptied the system's node caches. The copy now holds its own clone of the
tree, so the system is untouched either way and the tree returned is
never the system's own. The docstring says when the memo serves the real
application, and the changelog names dated parameter changes.

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

# Conflicts:
#	docs/book/usage/scenarios.md
#	policyengine_uk/simulation.py
…-data

# Conflicts:
#	docs/book/usage/scenarios.md
#	policyengine_uk/simulation.py
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 15, 2026 02:58
@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 611fc69 Oct 15, 2026
17 of 19 checks passed
@MaxGhenis
MaxGhenis deleted the uprating-inputs-reach-data branch October 15, 2026 03:53
MaxGhenis added a commit that referenced this pull request Oct 15, 2026
…ards

Brings in #2277, #2391, #2402, #2404, #2405, #2407, #2411, #2413, #2415,
#2418, #2419 and #2420. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 15, 2026
…2277) into uc-hb-split-shared-cap-lha-rules

No conflicts; no benefit cap, LHA, UC or HB file changed on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant