Repository navigation
Apply Housing Benefit accommodation disregards from their start date - #2234
Conversation
a859805 to
de48e39
Compare
|
Parent-gated build hand-off. Head remains Tests for this child: 0 executed in this job; no claim is made about the current head's CI. Preserve Anthony's corrected disabled-worker earnings-disregard regression (£1,929.20) when updating the base. Draft status unchanged; no merge/readiness/closure action. Once #2229 lands, the pending commands are: git fetch origin main fix-hb-accommodation-dates
git switch fix-hb-accommodation-dates # create tracking branch if absent
git merge --no-ff origin/main
# Resolve, run focused foreground tests with -n 2 after load1 <=110 and
# os.statvfs available disk >=55 GB decimal; commit fixes and push once.
gh api -X PATCH repos/PolicyEngine/policyengine-uk/pulls/2234 -f base=mainOpen items: parent landing; merge/test/push/body refresh as needed; retarget; hub independent review. Corrected the published body to describe existing childcare_expenses fixtures (no new qualifying-charge input) and main merges instead of rebases. Historical author validation (17 Python tests / 49 YAML tests) is now explicitly labelled as historical, not rerun here. Preserve #2131's shared entitlement changes when resolving the future merge. No extra simulation.py edits have been made by this job. |
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed at de48e391989960e56c8454ec5aea9693e299dec5. No actionable findings from independent policy/source and code reviews of the date-integration change.
GB and NI sources establish commencement on 5 October 2026; the intervening GB amendment’s 2 October start does not move the allowance forward. Computing completed awards within each date segment preserves the taper and floors before day-weighting.
Validation: 137 focused tests passed. An actual-income NI case produces £10214.80, matching 182/365 of the zero-allowance control plus 183/365 of the full-allowance control, and differs from tapering averaged income. Supplied overrides and parameter-parent links were also checked. The annual constant-circumstance approximation and documented whole-stack reconciliation prerequisites remain.
|
#2229 merged into The UK hub is restacking it the way #2219 and #2229 went:
Anthony, the commits will appear on |
…xfail CI's Test / Result job fails any xfail without an allowlist entry, and the allowlist is for known bugs. The D4 counterexample is the law, not a bug: HB Regs 2006 Sch. 4 para. 17(1) and (3), as SI 2026/978 reg. 2 amends them, apply the additional disregard only where net earnings reach the standard, accommodation, childcare and additional amounts together, so a penny of accommodation can remove all £889.20 of it. Main shows the same cliff in 2027-28, with no date segments. The new test pins the case: £10,400 with no accommodation, £10,110.22 with a penny, the later segment's award from the independent oracle, and the 182/183-day combination. The restricted monotonicity property stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; population; poverty rates; people in poverty; inequality. Checks failing on both base and head (1)
Counted-once checkEach of 91 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
|
UK hub: restacked on main after #2229, reviewed, and fixed the one finding. Head
The PR is still a draft. Merging waits for that review, green CI, the aggregate-impact check, and the team's request for no automatic agent merges. 🤖 Generated with Claude Code |
main's test_every_walk_up_to_an_ancestor_simulation_is_reviewed lists each function that walks parent_branch. hb_uses_annual_override walked it itself to collect branch names; _visible_branch_names returns the same set (this branch, its ancestors, default), so the check reads the same storage keys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed
On the merged tree: the guard, accommodation date examples and accommodation properties gave 24 passed (xfail strict); earnings-disregard properties gave 10 passed; Ruff and |
A Sol review of b488ed1 found that build_from_populations keeps the replaced holders' input records (policyengine-core#605). Once the applicable-income formula refilled the period, hb_uses_annual_override read the stale record as a supplied value and skipped the date segments: £10,400 instead of the fresh £10,322.21. The five variables the check asks about join SUPPLIED_INPUT_VARIABLES, so Simulation.calculate forgets such a record before the formula refills the period. The check moves to a shared stored_as_input helper, which counts a value stored as an input, supplied or the model's own (set_model_value). supplied_input would drop a scenario's own value: the variable would read £2,000 while the 2026-27 award ignored it (£10,322.21, not £9,425). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed
Local runs and mutations are in the body's Validation section. A Sol review of this commit is running; the PR stays a draft until it, CI and the aggregate impact report. |
stored_as_input checked this simulation's input record, so after build_from_populations a stale record could call a donor's calculated applicable income supplied (award £10,400, not £10,322.21), and a value the donor had supplied could count as calculated (£10,322.21, not the annual taper's £8,775). It now asks the holder, which answers from the record of the simulation it was built in, so moved populations keep their own provenance. set_model_value values still count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed
|
Simulation.build_from_populations points each population at the new simulation, but core leaves each holder bound to the simulation that made it and keeps the new simulation's record of the values the rebuild replaced (policyengine-core#605). A clone or branch copies the simulation's record and binds its copied holders to itself, so it read the replaced populations' record: a supplied income of 3,000 from another simulation awarded 8,775 in the rebuilt simulation but 10,322.21 in its clone or branch. An input set on the rebuilt simulation went into the other simulation's record, and supplied_input missed the other simulation's inputs (#2747). adopt_population_records binds each holder from another simulation to this one, keeps that simulation's input and model-value records for the values the holder still stores, drops the values it calculated or carried over, and forgets this simulation's records of values the rebuild removed. build_from_populations also empties the fast cache, which formulas read. A Hypothesis property checks that the rebuilt simulation, its clone and its branch give the provenance and award of a fresh copy of the other simulation. It found two cases that also fail at d5c96d9, now pinned as examples: a donor's income cached before a scenario set its disregard (10,244 against 10,075), and a receiver's fast-cache income from before the rebuild (10,400 against 9,042.15). Fixes #2747 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed
A Sol review of |
… them 6e090ed rebound each holder taken from another simulation to the rebuilt one. The two simulations still shared those holders, so the rebinding left the other simulation with holders bound to the rebuilt one. The sixth review found three failures: - A holder from a simulation that stores values on disk kept its disk setting. The rebuilt simulation has no memory configuration, so the next input it stored crashed in core's Holder._set. - The input-record filter asked only the first storage tier. A cache in memory hid an input on disk, so the rebuilt simulation lost the input: £10,322.21 instead of £8,775 with a supplied £3,000. - An input set on the other simulation went into the rebuilt simulation's record. Neither simulation noted the change, so both kept a stale weekly_hours of 10 instead of 20. Simulation.build_from_populations now takes a copy of each population and holder another simulation made, as Simulation.clone copies them (copy_other_populations in utils/supplied_inputs.py), and leaves the other simulation's populations, values and records as they were. Neither simulation's later changes reach the other. Populations no simulation has taken yet, the only ones the model's own builders pass, go in as before. Before copying, the helper reads what the other simulation recorded of each holder: the inputs that survive in either storage tier, and the model values (set_model_value) still stored. keep_population_records then keeps those records and this simulation's own records for the holders it made, drops the other values the copies hold, and gives each copy the storage settings this simulation gives its own holders. New tests in test_input_records.py cover: - a later input on populations from a simulation that stores on disk; - storage on disk when this simulation stores there; - an input on disk under a cache in memory; - the other simulation keeping its own values and inputs. In the Housing Benefit tests, two simulations rebuilt from the same populations now answer independently. The R1 property's docstring now names the states it generates. Fixes #2747 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy at 371c220 keeps this simulation's input records only for the holders it made, but no test failed when it kept them all: the readers that also check storage hid the stale record. supplied_input does not check storage marks, so a value cached in the replaced period read back as the replaced input. The new test sets a cache of 123 for an adjusted net income the rebuild replaced and checks that supplied_input returns None. It fails on main (daec84d), where supplied_input returns the cache, and passes at 6e090ed and 371c220. The keep_replaced_records mutation, which survived the hub's mutation run, now fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seventh review found that copying another simulation's holders dropped a model value (set_model_value) stored on disk under a value cached in memory. _records_of compared the recorded model value with what storage returned first, the cache, so the copy kept the input key but not the model marker. _drop_computed then removed the cache, and the disk value read back as a supplied input. In the review's example, adjusted net income of £50,000 set by the model on disk under a cache of 999 gave a supplied_input of 50,000 and net income of £50,000 in the rebuilt simulation, its clone and its branch, against the original simulation's None and £30,000. _records_of now compares each model value with the stored input, read through core's get_stored_input, which asks each tier whether it holds the input. The review's script now gives None and £30,000 in all three copies, and the aggregate-guard snapshot keeps the model origin. test_rebuilt_populations_keep_a_model_value_a_cache_hides pins the case. It fails at 3cfab06 and passes here. The copy_other_populations docstring now says that the copy shares each in-memory array with the original until it first reads it, so writing in place into the original's array before then changes what the copy reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed
An eighth Sol review of |
Towncrier adds each entry's bullet itself, so a fragment that starts with '- ' renders as '- - ...' in CHANGELOG.md. The Changelog fragments check warned on both at 8cac7e9, and the eighth review asked for the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The eighth Sol review approved The PR stays a draft until CI and the aggregate impact pass at |
#2234 settles Schedule 4 paragraph 17 separately before and after the accommodation disregard starts on 5 October 2026, but its segment income left out the WTC disregard. Paragraph 56 follows paragraph 17's outcome, so each segment now has its own WTC disregard: the childcare limit takes it, hb_segment_deductions returns it, and entitlement deducts it. The annual variable combines the segments by days. The WTC and earnings decisions now share one paragraph 17 test and one check for a supplied or replaced childcare deduction, #2234's hb_uses_annual_override. Under a reform that keeps tax credits in 2026-27, a claimant in specified accommodation with £500 of earnings and £2,000 of WTC now gets £9,600.19 rather than £9,022.21. Current-law awards do not change: tax credits end before the accommodation disregard starts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #2233
Fixes #2747
This PR changes 18 files and adds no model inputs. The fiscal-year loader samples parameters on 30 April, before the accommodation earnings disregard starts on 5 October 2026. The PR keeps the seven monetary parameters' actual dates and uses
fiscal_year_segmentsto calculate a complete Housing Benefit award for each date segment, then combines those awards by elapsed fiscal-year days.Calculation and scope
Each segment floors net earnings at zero, reads the shared
housing_benefit_applied_standard_earnings_disregard, and caps its full accommodation amount against remaining earnings. The childcare helper uses that full amount and adds nonnegative WTC/CTC payments to residual earnings. The calculation tests additional-disregard eligibility before applying the income floor, applicable-amount threshold, 65% taper and zero-award floor. It weights completed awards after these steps.Calendar arithmetic gives 182 days before commencement and 183 days afterwards in 2026–27. For annual earnings of £1,000, a £260 standard disregard, a £500 applicable amount and £10,400 eligible rent, the complete annual-equivalent awards equal £10,244 before commencement and £10,400 afterwards. Their day-weighted mean equals
(10,244 × 182 + 10,400 × 183) / 365 = £10,322.21.The calculation reads reformed schedules and clones the seven monetary leaves for its temporary schedule. The parameter-integrity test confirms that their values and parent links remain intact. The override checks treat a value stored as an input for the year, supplied or a scenario's own (
set_model_value), as the annual amount, and recalculate the segments over a cached formula result. Example tests cover dated and fiscal-year reforms, supplied-period attribution, laterset_input, replacement formulas, neutralization, and populations taken from another simulation.Annual earnings, rent, ages and household circumstances stay constant across segments. Other parameters keep the existing annual convention. Entitlement recalculates segment income and deductions when needed; pension-age and passported cases keep their annual income treatment. The PR changes no accommodation eligibility, working-age closure, rent valuation, take-up or dataset contents.
Parameter metadata and formula references cite GB commencement, SI 2026/753 regulation 1, GB amounts and earnings-test amendment, SI 2026/978 regulation 2, NI commencement and amounts, SR 2026/157, the GB award taper, childcare earnings limits, and their NI counterparts.
Properties
test_housing_benefit_accommodation_dates_properties.pychecks:D4 tests monotonicity without the additional disregard. The statutory earnings threshold can break monotonicity when that disregard applies. GB Schedule 4 paragraph 17 gives the whole £17.10 weekly additional disregard only where net earnings cover the standard disregard, accommodation disregard, deductible childcare and additional disregard together. SI 2026/978 regulation 2 adds the accommodation disregard to that threshold; NI Schedule 5 paragraph 17 and SR 2026/157 regulation 2 make the same change. So a penny of accommodation can remove the whole disregard:
test_d4_a_penny_of_accommodation_can_switch_off_the_additional_disregardpins that case, and checks the later segment's award (£9,822.03) against the oracle and the 182/183-day combination;An earlier commit recorded the cliff as a strict xfail. CI's
Test / Resultcheck fails any xfail that has no allowlist entry, and the allowlist is for known bugs, sobda6163b8replaces it with the passing regression.Validation
The branch merges main after #2229 and keeps #2211's ordinary-income helper, #2225's disabled-worker correction, #2219's permitted-work and savings-credit paths, and #2229's shared applied standard and residual childcare limit. Each segment uses those shared helpers before the calculation weights the completed award.
An independent GPT-6.1 Sol review at
218c03bb7checked the merge resolution against both parents, every call into #2229's helpers, the law behind D4, and main's behaviour. It found one P2, the xfail, whichbda6163b8fixes. Its runs:At
bda6163b8, the UK hub ran:git diff --check, passed.A second Sol review, of the change from
218c03bb7tobda6163b8, approved it with nits in this body, which the body now applies (REVIEW-delta.mdin the UK hub's records).At
b488ed1ccthe branch merges main again (2d437eb). Main's newtest_every_walk_up_to_an_ancestor_simulation_is_reviewedlists each function that walksparent_branch, and CI failed it on this PR'shb_uses_annual_override, which walked the chain itself to collect branch names.b8e40c3a1made it call main's reviewed_visible_branch_namesinstead, which returns the same set: this branch, the branches it came from, anddefault.A third Sol review, at
b488ed1cc, confirmed that set equality on five branch shapes, the clean merge, and that a household subset reads only its own households. It found one P2:Simulation.build_from_populationskeeps the replaced holders' input records (policyengine-core#605). Once the applicable-income formula refilled the period, the check read the stale record as a supplied value and skipped the segments, so the award came to £10,400 instead of the fresh simulation's £10,322.21.5dc13b793fixed that case:SUPPLIED_INPUT_VARIABLES, soSimulation.calculateforgets a record that a rebuild left without its value before the formula refills the period;stored_as_inputinutils/supplied_inputs.py, which counts a value stored as an input for the period, supplied or the model's own;test_rebuilt_populations_leave_no_supplied_income_behindpins the rebuild case at £10,322.21, andtest_scenario_adjusted_income_keeps_its_valuepins aset_model_valueadjustment of £2,000 at the annual taper's £9,425.The review proposed
supplied_inputfor the check. That helper skips the model's own values, so a scenario that set the income to £2,000 would read £2,000 while the 2026–27 award ignored it and stayed at £10,322.21.stored_as_inputkeeps them together.A fourth Sol review, at
5dc13b793, agreed that the check should count the model's own values and passed 42 rebuild-order, branch and later-input checks. It found one P2:stored_as_inputstill read this simulation's record, so populations whose donor had already calculated the applicable income, or either disregard, still awarded £10,400, and a donor's supplied income of £3,000 counted as calculated (£10,322.21 instead of the annual taper's £8,775).d5c96d95afixes it:stored_as_inputasks the holder (Holder._is_input), which answers from the input record of the simulation bound to it.build_from_populationspoints the populations at the new simulation but leaves each holder bound to the simulation that made it, so moved populations kept their own provenance;test_rebuilt_populations_keep_a_calculated_value_calculatedpins the three precomputed-donor cases at £10,322.21, andtest_rebuilt_populations_keep_a_supplied_incomepins the supplied donor income at £8,775.At
5dc13b793the UK hub ran:supplied_inputin place ofstored_as_inputfails the scenario test; skipping the record cleanup inSimulation.calculatefails the rebuild test; dropping the record condition fails four date tests;git diff --check: passed.None of the UK hub's local runs used a survey dataset. The household-subset file's one
Microsimulationtest builds a synthetic 60-person dataset in memory.At
d5c96d95athe UK hub ran:5dc13b793and pass here;test_ni_class_4_profits_properties.py: 1 passed; input export: 25 passed; the aggregate-impact files: childcare provenance 42, compare 133, data mode 4, dataset 13, guard 39, measure 44, partition 11 and reforms 20 passed; all with xfail strict;git diff --check: passed.It did not run
test_aggregate_impact_certified.py: with a Hugging Face token in the hub's environment, its fixture builds aMicrosimulationon the survey dataset, and the 4 GB RSS cap stopped it after 36 s, before any test ran. CI ran all 9 of its tests on the certified dataset at this head, and they passed (run 40857494571, Python shards 1 and 3–6).A fifth Sol review, at
d5c96d95a, passed 49 lifecycle checks and 964 tests, and found one P2: a clone or branch of the rebuilt simulation lost the other simulation's provenance. Core'sSimulation.clonecopies the simulation's own input record, andHolder.clonebinds each copied holder to the clone, so the clone read the record of the populations the rebuild had replaced. A supplied income of £3,000 in the other simulation gave £8,775 in the rebuilt simulation but £10,322.21 in its clone or branch, and the aggregate guard'ssnapshot_inputsdiscarded that income before capture (also £10,322.21).6e090ed00moved the records with the holders.Simulation.build_from_populationscalled a new helper,adopt_population_records, which:Holder._setrecords an input in its own simulation's record, so before this an input set on the rebuilt simulation went into the other simulation's record, andsupplied_inputmissed it;set_model_value) the holder still stores;build_from_populationsalso empties the fast cache (it still does), as a rebuild from a situation already does: formulas read it, so it served values calculated for the replaced populations.Clones and branches copy the simulation's records, so they now read the new populations' provenance, and so does a household subset, which starts from a branch. UK builds a reform's baseline when it constructs the simulation, so a later rebuild does not reach it.
supplied_input,supplied_input_periodsandsupplied_byread the same record, so the change also fixes #2747. policyengine-core#636 reports the holder binding upstream, with core-only reproductions; core's ownSimulationclass keeps both gaps until core fixes it.Invariant R1: for the states the property generates (each override variable absent, supplied, set by a scenario or calculated for 2026, on the default branch, in either simulation, with every value in memory), after
build_from_populations(other.populations)a simulation, its clone and its branch give the samestored_as_inputandsupplied_inputanswers for the three override variables, and the same award, as a fresh copy of the other simulation.test_rebuilt_populations_answer_as_the_simulation_that_made_themchecks R1 with Hypothesis over those states. It does not cover other branches or values stored on disk; example tests intest_input_records.pycover the disk and cache cases. Its first runs found two more gaps, both present atd5c96d95a, which two@examples now pin:New example tests:
test_copies_of_rebuilt_populations_keep_a_supplied_income,…_keep_a_calculated_income_calculatedand…_keep_a_scenario_adjusted_incomecheck the award andsupplied_inputin the rebuilt simulation, a clone, a branch and an aggregate-guard snapshot;test_rebuilt_populations_replace_values_calculated_beforechecks the fast-cache case;test_input_records.py: the export of the new populations' inputs, an input set after the rebuild (in the simulation and a clone), and Supplied-input helpers misread provenance after build_from_populations with another simulation's populations #2747's two adjusted-net-income cases.At
d5c96d95a, 16 of the 17 new example tests fail; only the scenario-adjusted income in the rebuilt simulation itself passes there. Both of the property's pinned examples fail there too.At
6e090ed00the UK hub ran:test_aggregate_impact_certified.py: childcare provenance 42, compare 133, data mode 4, dataset 13, guard 39, measure 44, partition 11 and reforms 20 passed; one file at a time, with xfail strict and no survey dataset;derandomize=False) under three seeds, 20 examples each: 60 passed, peak 1.3 GB per run;test_rebuilt_populations_record_an_input_set_later; skipping_drop_computedfails the property's first pinned example; dropping the other simulation's model values fails the four scenario-adjusted cases; keeping the fast cache failstest_rebuilt_populations_replace_values_calculated_before; skippingadopt_population_recordsfails 15 of the 17 new example tests;supplied_inputreturns 3,000 and the award is £8,775 (atd5c96d95a, the same award, withsupplied_inputNone);uv.lockpins) format and check on the five changed Python files, the F811/F821 check on the package, andgit diff --check: passed.A sixth Sol review, at
6e090ed00, found three P2s, all from the two simulations sharing holders after the rebinding:Holder._set;weekly_hoursof 10 instead of 20.371c22093copies instead of sharing.Simulation.build_from_populationsnow callscopy_other_populationsinutils/supplied_inputs.py, which reads what the other simulation recorded of each holder it made (the inputs that survive in either storage tier, and the model values still stored), then copies those populations and holders with core'sPopulation.clone,GroupPopulation.cloneandHolder.clone, asSimulation.clonedoes.keep_population_recordsthen keeps those records and this simulation's own records for the holders it made, drops the other values the copies hold, and gives each copy the storage settings this simulation gives its own holders. The other simulation keeps its own populations, values and records, so an input either simulation stores later withset_inputdoes not reach the other (but see the in-place caveat at8cac7e91fbelow). Populations no simulation has taken yet, the only ones the model's own builders pass, go in as before. The rebuild no longer rebinds anything, so the holder-binding gap in policyengine-core#636 no longer reaches UK simulations.New tests:
test_input_records.py:test_rebuilt_populations_take_inputs_from_a_simulation_storing_on_disk,…_store_values_where_this_simulation_does,…_keep_an_input_a_cache_hidesand…_leave_their_simulation_as_it_was;test_simulations_rebuilt_from_the_same_populations_answer_alone, where two simulations rebuild from the same populations, taken from a simulation that supplied an income of £3,000. An income of £4,000 set on the first afterwards gives it £8,125 and leaves the second and the original simulation at £8,775.At
371c22093the UK hub ran:6e090ed00and pass here;6e090ed00, one at a time, with xfail strict and no survey dataset: accommodation date examples, the property included, 38 passed; input records 9; accommodation properties 6; earnings-disregard properties 10; household subset 9; input export 25; scenario and direct modifier inputs 40 and 21; reform inputs through a reset 17; measurement branches 5; data-source provenance 20; benefit-cap properties 46; capital-gains projection 115; childcare birthday terms 53; current education order 11; Gift Aid 12; Class 4 NI profits properties 25; the program registry 106; the aggregate-impact files other thantest_aggregate_impact_certified.py: childcare provenance 42, compare 133, data mode 4, dataset 13, guard 39, measure 44, partition 11 and reforms 20 passed (874 tests, all passed);…_answer_aloneand…_leave_their_simulation_as_it_was; asking only the first storage tier fails…_keep_an_input_a_cache_hides; skipping the storage settings fails both disk tests, and skipping only the new disk storage fails…_store_values_where_this_simulation_does; skipping_drop_computedfails the property's first pinned example; dropping the other simulation's model values fails the four scenario-adjusted cases; keeping the fast cache failstest_rebuilt_populations_replace_values_calculated_before; skippingkeep_population_recordsfails 20 tests. Copying the arrays at once instead of sharing them (share_arrays=False) passes every test, as expected. Keeping this simulation's records of the inputs the rebuild replaced passed every test too, which3cfab0639fixes (below);git diff --check: passed.3cfab0639addstest_rebuilt_populations_forget_the_inputs_they_replace. This simulation supplied an adjusted net income that the rebuild replaces; a value of 123 cached in that period afterwards must not read back as the supplied input.supplied_inputchecks the record and the stored value but not storage's input marks, so it returned 123 on main (daec84d5b, policyengine-core#605). The test passes at6e090ed00and371c22093, and fails the mutation above.A seventh Sol review, at
3cfab0639, reran its three counterexamples and found each fixed. It passed 532 Python tests, 584 YAML cases and 45 more random R1 examples, and found one P2._records_ofcompared each model value (set_model_value) with what storage returns first, which is memory. So when a value cached in memory hid a model value stored on disk, the copy kept the disk value as an input but dropped the record that the model set it, and once_drop_computedremoved the cache, the rebuilt simulation read the model's value as supplied. In the review's example, the other simulation's model set an adjusted net income of £50,000 over employment income of £30,000: there,supplied_inputreturns None and net income is £30,000. In the rebuilt simulation, its clone and its branch,supplied_inputreturned £50,000 and net income rose to £50,000.8cac7e91fcompares each model value with the stored input instead. It calls core'sget_stored_input, which returns the value that storage marks as an input for the period and branch, memory first and then disk. The review found it in core 3.33.0, the oldest versionpyproject.tomlallows. The commit also states the limit of the copy incopy_other_populations's docstring: the copy shares each in-memory array with the original until it first reads it (core'sshare_arrays), so writing in place into the original's array before then changes what the copy reads. An input set afterwards withset_inputdoes not reach the other simulation (test_rebuilt_populations_leave_their_simulation_as_it_was).At
8cac7e91fthe UK hub ran:test_rebuilt_populations_keep_a_model_value_a_cache_hides: it fails at3cfab0639(supplied_inputreturns £50,000) and passes here, in the rebuilt simulation, its clone and its branch;supplied_inputNone and net income £30,000, and an aggregate-guard snapshot records the value as the model's;371c22093batch, the ones that build, rebuild or copy simulations, one at a time with xfail strict and no survey dataset: input records 11, accommodation date examples, the property included, 38, accommodation properties 6, household subset 9, input export 25, scenario and direct modifier inputs 40 and 21, reform inputs through a reset 17, measurement branches 5, data-source provenance 20, and the aggregate-impact guard 39 and childcare provenance 42 passed (273 tests);371c22093and3cfab0639, andmodel_value_first_tier, which compares model values with what storage returns first again and fails the new test. Copying the arrays at once (share_arrays=False) passes every test, as expected;git diff --check: passed.An eighth Sol review, at
8cac7e91f, approved it with two nits, both now applied. It reran the seventh review's reproduction (it exits 1 at3cfab0639and 0 here) and checked core 3.33.0'sget_stored_input: memory first, then disk, on the exact branch, and None when neither tier holds an input. It built a model value and a different input in the two tiers, both ways round, a model value on one branch with a supplied input on another, a laterset_inputover a model value, and a model value already dropped, and found the expected record in each. It passed 533 Python tests in 20 files, 619 Housing Benefit YAML cases in 51 files and 40 more random R1 examples. Its nits:dd79d3fb5removes them;set_inputand points to the in-place caveat.Aggregate impact: the run for
3cfab0639(comment, run 40944196229, merge0a10904eb2) shows Housing Benefit spending £2.23 lower in 2026–27, out of about £8.9 billion, and equal to main in every other year from 2025–26 to 2031–32. Government spending and household net income move by the same £2. The runs ford5c96d95aand371c22093gave the same Housing Benefit figures in every year. The run fordd79d3fb5(run 40995789501, mergef108389ffc) shows Housing Benefit £2.23 lower in 2026–27 (£8,886,896,403.24 against main's £8,886,896,405.47) and equal to main in every other year. No metric moves outside 2026–27, it broke no check and it flagged no move.CI at
3cfab0639: all 19 checks passed; the YAML job took 33 minutes 20 seconds. At371c22093it passed all 5,249 cases and then sat after its summary until GitHub cancelled it at the job's 45-minute limit, as it did on other branches that day. CI at8cac7e91fpassed the YAML job in 43 minutes 42 seconds. Atdd79d3fb5all 19 checks passed.Reproducer: policyengine_uk/tests/test_housing_benefit_accommodation_dates.py::test_first_year_combines_completed_awards
axiom: TheAxiomFoundation/rulespec-uk#375 queued
The specified-accommodation disregard dates extension to rulespec-uk#375, with the law, required outputs and companion tests: TheAxiomFoundation/rulespec-uk#375 (comment) (UK hub, 24 Oct).
🤖 Generated with Claude Code