Skip to content

Drop the clone override now that policyengine-core keeps each clone's input record - #2262

Merged
MaxGhenis merged 3 commits into
mainfrom
retire-core-559-workaround
Oct 10, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
retire-core-559-workaround

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #2260. The rest waits on PolicyEngine/policyengine-core#605.

What this changes

policyengine-core 3.32.27 is the first release with PolicyEngine/policyengine-core#561. That PR fixes PolicyEngine/policyengine-core#559, where Simulation._user_input_keys, the record of set_input values, drifted from storage. With #561:

  • Simulation.clone gives each clone its own copy of the record and an empty _user_input_contexts.
  • Holder.delete_arrays forgets the record of each value it deletes, including deletes made straight on the holder.

This PR:

  • removes PE-UK's Simulation.clone override, which copied the record and contexts by hand;
  • raises policyengine-core>=3.32.12 to >=3.32.27. The 3.32.26 tag is behind the Add total wealth #561 merge commit and 3.32.27 is ahead of it; PyPI serves 3.32.27. The lock moves core from 3.32.13 to 3.32.27, and the lock's own entry for this package catches up to 2.125.1;
  • rewrites the comments of the delete_arrays override and the calculate sweep, and the supplied_inputs module docstring, so they say what each now covers (below);
  • adds tests (below).

What stays, and why

build_from_situation installs fresh populations, and so fresh holders, through Simulation.build_from_populations, which keeps the record. That gap is filed as PolicyEngine/policyengine-core#605, reproduced there with country_template, and the issue has a further comment showing the export consequence. Two pieces of PE-UK bookkeeping still cover it:

  • The Simulation.calculate sweep and SUPPLIED_INPUT_VARIABLES. After a rebuild, a carried-over trading loss could pass as supplied, giving Class 4 profits of £20,000 and NIC of £445.80 instead of £30,000 and £1,045.80.
  • The Simulation.delete_arrays override. Core's deletion forgets only the records of values it removes. The override also drops records a rebuild left with no stored value, for any variable. Without it, rebuilding without the 2026 hours_worked input, deleting 2026 and calculating 2026 makes to_input_dict export 2025's carried-over hours as a 2026 input (core 3.32.27 and 3.33.0).

Both can go once core fixes #605. The independent reviews found these cases (see below); the first two versions of this PR had removed them.

Behaviour

No calculated value changes, and no existing test expectation changes. With the same core installed, the PE-UK code differs from main only by the missing clone override. Round 3 of the review compared clones with and without it on 3.32.27 and 3.33.0: plain clones, inputs set after cloning, nested branches through get_branch, clones of branches, rebuilding a clone, and disk-backed inputs all came out the same. Two boundaries differ:

  • A clone made while a set_input is running on the original starts with an empty _user_input_contexts (core's behaviour); the removed override copied the original's. No PE-UK set_input handler clones a simulation. Round 1 showed the old copy could misrecord a later write on such a clone as an input.
  • A clone of a simulation with no _user_input_keys attribute at all keeps that absence; the override created an empty set. Normal PE-UK initialisation creates the record, so this arises only from a simulation built some other way (round 3 emulated it by deleting the attribute).

Raising the floor also brings in core's own changes between 3.32.13 and 3.32.27; see core's changelog.

Invariants

  • A trading loss counts as supplied (supplied_input, supplied_input_periods) exactly while the last value set for it, by set_input or a rebuild, has been neither deleted (Simulation.delete_arrays or Holder.delete_arrays) nor dropped by a rebuild. A value the engine carries over or calculates into a period never counts.
  • A clone starts with its original's supplied inputs, and what either does afterwards does not reach the other.
  • After a rebuild and a deletion, to_input_dict exports only values that were supplied.

Tests:

  • test_only_losses_set_and_kept_count_as_supplied (new) is a model-based Hypothesis property over random sequences of set_input, both kinds of delete, calculation, clones and rebuilds. Each step can act on any simulation made so far, and every simulation is checked after every step. It runs 30 derandomized examples plus two pinned @examples. The pins make it fail on core 3.32.13 (a clone sharing one record) and with the sweep disabled (Generate takeup-informing variables before calibration, not after #605). Without the pins, 200 examples were needed to find the clone case on 3.32.13.
  • test_class_4_does_not_count_a_loss_a_rebuild_drops (new) fails with the sweep disabled.
  • test_input_records.py::test_deleting_after_a_rebuild_does_not_export_a_carried_over_input (new) fails without the delete_arrays override.
  • test_supplied_input_helpers_ignore_missing_stored_arrays now puts a stale record back itself, since core no longer leaves one after a holder delete.

Tests run

Locally. Evidence is in ~/reviews/core-user-input-keys-2026-10-02/uk_retire/. In the table, "related pytest" means the 23 test files that mention entered_directly, _user_input_keys, clone, delete_arrays or supplied_input, plus test_input_records.py. "Supplied-input YAML" means the bus journeys, bus fare spending, loss relief, Class 2 and Class 4 profits tests.

Run Core Result
main, unchanged (control) 3.32.13 lifecycle 16 passed; related pytest 533 passed, 1 skipped; policyengine-core test .../finance/benefit 1,062 passed; supplied-input YAML 66 passed
c64dd70 (clone and delete overrides removed) 3.32.27 16 / 534 + 1 skipped / 1,062 / 66 passed
c64dd70 3.33.0 (what CI installs) 16 / 534 + 1 skipped / 1,062 / 66 passed
5f2ea22 (this head but for a docstring) 3.32.27 lifecycle 16 passed; related pytest 536 passed, 1 skipped; YAML benefit 1,062 passed; supplied-input YAML 66 passed
5f2ea22 3.33.0 (what CI installs) 16 / 536 + 1 skipped / 1,062 / 66 passed
this head, focused (new and changed tests, 14) 3.32.27 and 3.33.0 14 passed on each
this head, negative control 3.32.13 the property test and test_class_4_counts_only_supplied_losses_on_plain_clones fail, so the 3.32.27 floor is required
this head, sweep disabled (mutant) 3.32.27 the property test and the rebuild test fail
this head, delete_arrays override removed (mutant) 3.32.27 test_input_records.py fails
  • uv run --frozen ruff format --check policyengine_uk and ruff check policyengine_uk: pass.
  • uv lock --check: passes.

make test and the whole tests folder were not run locally; CI runs them.

Review

Independent review by GPT-6.1 Sol through Subfleet (review task, standard tier):

  • Round 1, on aa57eb7, which also removed the sweep and registry: REQUEST_CHANGES (rebuild). Fixed in b9009cf.
  • Round 2, on b9009cf: REQUEST_CHANGES (the delete_arrays override was still needed for rebuilt simulations), plus one precision note on the property test's selector. Fixed in 5f2ea22.
  • Round 3, on 5f2ea22: APPROVE. One P3 wording note about build_from_populations is fixed in a25ce01. Round 3 also reproduced every negative control above independently.
  • Round 4, on the delta 5f2ea22..a25ce01 (docstring only; AST identical otherwise): APPROVE.

Reports: review-2260.md, review-2262-r2.md, review_r3.md and review_r4.md in the evidence folder.

axiom: n/a: engine bookkeeping only; no policy rule changes.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 10, 2026 12:14
…put record

policyengine-core 3.32.27 contains PolicyEngine/policyengine-core#561, which
fixes policyengine-core#559: Holder.delete_arrays forgets the record
(Simulation._user_input_keys) of an input it deletes, including deletes made
straight on the holder, and Simulation.clone gives each clone its own copy of
the record and an empty _user_input_contexts.

PE-UK's Simulation.clone and Simulation.delete_arrays overrides did the same
by hand, so remove them and raise the policyengine-core floor from 3.32.12 to
3.32.27. Relock core 3.32.13 -> 3.32.27; the lock's own entry for this package
catches up to 2.125.1.

Keep the sweep in Simulation.calculate and SUPPLIED_INPUT_VARIABLES.
Simulation.build_from_populations, which build_from_situation calls, replaces
every holder but keeps the record (policyengine-core#605). A rebuilt
simulation could then pass a carried-over trading loss off as supplied, which
the sweep prevents. Its comment and the supplied_inputs docstring now name
that case instead of #559.

Tests:
- a rebuild regression for Class 4 (fails with the sweep disabled);
- a model-based property test: over random sequences of set_input, both
  kinds of delete, calculation, clones and rebuilds, a loss counts as
  supplied exactly while the last value set for it is kept. Two pinned
  examples make it fail on core 3.32.13 (one record shared by a clone) and
  with the sweep disabled (#605);
- the missing-stored-array test now puts a stale record back itself, since
  core no longer leaves one after a holder delete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2 found that the removed Simulation.delete_arrays override was
not pure duplication of policyengine-core 3.32.27. Core's deletion forgets
only the records of the values it removes. The override also dropped records
that a population rebuild had already left with no stored value
(policyengine-core#605), for any variable. Without it, deleting hours_worked
for 2026 after a rebuild that dropped the 2026 input, then calculating 2026,
made to_input_dict export the carried-over 2025 hours as a 2026 input (core
3.32.27 and 3.33.0).

Restore the override, with its comment naming #605 instead of #559, and add
test_input_records.py for that sequence (it fails without the override). The
property test's step selector now reaches every simulation a run can make.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis changed the title Drop the clone and delete_arrays overrides now that policyengine-core keeps the input record Drop the clone override now that policyengine-core keeps each clone's input record Oct 10, 2026
Review round 3 (APPROVE) noted that Simulation.build_from_populations adopts
whatever populations it is given: passed the existing ones, it keeps their
holders. The holders are replaced because build_from_situation passes freshly
instantiated populations. Word the supplied_inputs docstring that way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the retire-core-559-workaround branch from 173aa4a to a25ce01 Compare October 10, 2026 16:52
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 10, 2026 17:54
@MaxGhenis
MaxGhenis merged commit d1afea8 into main Oct 10, 2026
11 of 12 checks passed
@MaxGhenis
MaxGhenis deleted the retire-core-559-workaround branch October 10, 2026 18:54
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged at head a25ce01 (merge commit d1afea8) with a merge commit, pinned by --match-head-commit. Every check passed on the latest run; Test took 58m44s on a rerun, after the first run was cancelled at the 60-minute limit at 99% with no failures (#2257). Independent review by GPT-6.1 Sol: rounds 1 and 2 requested changes, both addressed; round 3 approved 5f2ea22, and round 4 approved the docstring-only delta to a25ce01. No calculated value changes. #2260 stays open for the delete_arrays override, the calculate sweep and SUPPLIED_INPUT_VARIABLES, which can go once PolicyEngine/policyengine-core#605 is fixed.

MaxGhenis added a commit that referenced this pull request Oct 10, 2026
Brings in #2262 (retire the policyengine-core 559 workaround). No file
overlaps this branch's changes; no conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…mulated-benefits-scoped-awards

Conflict in simulation.py: main dropped the clone override because
policyengine-core 3.32.27 copies the input record per clone (#2262). Core's
clone now also links a cloned tax-benefit system to the clone, which was the
only other thing this branch's override did, so the override goes. The
apply_reform override (link while applying, restore after) stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
Brings in #2258 (120-minute Test job), #2180 (NICs thresholds indexed by
September CPI after 2030-31), #2063 (salary sacrifice needs pay), #2038
(winter payments to the member a benefit is paid to), #2259, #2248,
#2251, #2262, #2266, #2270, #2285, #2288, #2294 and #2021.

Conflict: test_legacy_award_readers_properties.py imports. Kept both
hypothesis.event (this branch's screened-out events) and
hypothesis.example (#2294's found families). The rest of the file merged
cleanly: #2294's without/silent/with-other comparison, the severe
disability premium bound and the held benefit cap sit alongside this
branch's work generators, screened reference and outside-member tests.
Two comments now say the ESA and JSA readers also read the members' work.

_legacy_award_payee.py merged cleanly: #2038's can_claim (Pension Credit)
and this branch's own_report_is_paid are independent. The shared NI
parameters already match main (#2058); #2180 changes only 2031-32 onward.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…2259, #2262, #2285) into benefit-cap-75f-claimant-partner

The payee helper takes both main's can_claim (Pension Credit) and this
branch's couple (the benefit cap's couple).

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.

_user_input_keys drifts from stored values after delete_arrays and clone

1 participant