Skip to content

Fix disable_simulated_benefits and read one claimant-or-partner award - #2075

Merged
MaxGhenis merged 31 commits into
mainfrom
disable-simulated-benefits-scoped-awards
Oct 17, 2026
Merged

MaxGhenis merged 31 commits into
mainfrom
disable-simulated-benefits-scoped-awards

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2264

Summary

Fixes the disable_simulated_benefits reform (gov.contrib.policyengine.disable_simulated_benefits) so that it pays each family the benefits it reported, read the same way by every rule that asks whether the claimant or partner is on income-related ESA or income-based JSA. It also gives the claimant's and partner's ESA and JSA awards one reading, shared by the claimant_or_partner_* variables, both income-related limbs of income_support_eligible and the benefit cap.

This PR is based on main (last merged in at b4dbaf5, which includes #2047, #2031, #2261, #2076, #2274, #2085, #2399, #2277, #2405, #2242 and #2089). Head: c2e069d.

What was wrong

The reform raised before setting anything, for three reasons, each hiding the next: Simulation.__init__ never set tax_benefit_system.simulation, a situation's dataset year is text, and five listed benefits (Attendance Allowance, both DLA components, both PIP components) have no <name>_reported variable. #2047 (merged 2026-10-13) fixed all three on main with the same changes this PR carried, so on main the reform now runs on the Enhanced FRS. What remains:

  1. Later years misread whose award it is. The reform holds each benefit at its dataset-year amount for ten years, while esa_income_reported and jsa_income_reported are uprated. claimant_or_partner_esa_income, claimant_or_partner_jsa_income (Count only the claimant's and partner's legacy awards in means tests and passports #2027) and the Income Support limbs decide whose award esa_income / jsa_income holds by comparing it with the award on the current period's reports and with their plain total. In a later year neither comparison holds, so the whole award, another member's included, is taken to be the claimant's or partner's.
  2. Another member's Income Support report was set as the family's income_support, which the passports read as the claimant's.
  3. The legacy closure ends the awards the reform pays. Since Move families onto Universal Credit when a legacy benefit they receive closes #2119, once income-based JSA or Income Support closes (from 2026 in the model), a family reporting either is moved off all its legacy benefits (legacy_benefits_closed): their eligibility and the "on" statuses read from it end, and the Universal Credit claim and Housing Benefit and tax credit eligibility follow, while the reform still pays every reported award.
  4. A reform applied after the build (a Reform class passed as reform=, or applied to a built simulation, a clone or a branch) reaches the simulation through the system's link, which on a branch points at the parent, and reset_calculations deletes what it stored, because input_variables is taken at the build.
  5. A Scottish carer's Carer's Allowance is counted twice. The reform sets carers_allowance from carers_allowance_reported in every country and leaves carer_support_payment simulated. From 2025 the model pays a carer in Scotland Carer Support Payment in place of Carer's Allowance, and carer_support_payment_pre_overlap still pays a Scottish carer whose Carer's Allowance is reported. So a Scottish carer reporting £4,000 got £4,000 of Carer's Allowance and £4,331.60 of Carer Support Payment in 2025 (Vahid Ahmadi's review of 5366cf0, and Count the Pension Age Winter Heating Payment in household and HBAI income #2047's note on this PR). Count the Pension Age Winter Heating Payment in household and HBAI income #2047's winter heating split also reads where the household lives in the dataset year only, so a household that moves into or out of Scotland in a later year is paid under the wrong scheme (juaristi22's review of ab800e8 found this for this PR's first carer split).
  6. The benefit cap reads another member's held ESA or JSA as the couple's. The cap's total of welfare benefits (benefit_cap_welfare_benefits) and the ESA support component exemptions from the Universal Credit and Housing Benefit caps (Lift the benefit cap only for the support component and the people HB reg 75F and UC reg 83 name, and cap only the family's benefits #2076, Give Universal Credit and Housing Benefit their own benefit cap and LHA shared-accommodation rules #2089) work out the couple's award from esa_income / jsa_income by value (award_of_members), as in item 1. In a later year the reform's held total matches neither reading, so an award held for a non-dependent adult, claimed in their own right, counted as the couple's: it raised the cap's total and could exempt a claimant with a support component who receives no ESA (review r11 on this PR, by two reviewers).

What changes

  • Simulation link: Simulation.__init__ sets tax_benefit_system.simulation before applying structural reforms, as main does since Count the Pension Age Winter Heating Payment in household and HBAI income #2047 (this PR only rewords its comment). Simulation.apply_reform links the system to the simulation applying the reform while it runs, then restores the link, so a clone or branch that applies the reform sets its own inputs, never its parent's. apply_reform is one method: it also refreshes the capital gains projection after core applies the reform, as Add versioned capital gains projection scenarios #2274's override did.
  • Inputs a later reform stores: input_variables is taken when a simulation is built, and reset_calculations deletes the values of every variable it does not name (as core's derivative did before policyengine-core 3.33.0; from 3.33.0 derivative keeps every input), and the marginal rate formulas those of every such variable that has a formula. Core records each input in the simulation's _user_input_keys as (variable, branch, period): Holder._set adds the key once it has stored the value, and again when it replaces one, on every route (Simulation.set_input, Holder.set_input, a set_input helper, another thread). While Simulation.apply_reform runs, that record is an _InputKeyLog, a set that also logs each key added. _user_input_keys is a property on Simulation (kept in __dict__, as core keeps it), so a record core replaces mid-reform (a branch's, rebuilt when a reform is applied to its parent, or one rebuilt when the system is rebound) is logged too. The active logs are kept by simulation id outside the simulation, because core's clone copies __dict__, and a deep copy or pickle of the record is a plain set. Afterwards apply_reform adds to input_variables every variable with a logged key that the simulation still holds, under a branch name it reads (its own, its parent branches' or default). So a reform applied after the build keeps through a reset every input it stores and leaves in place, including one that replaces an input set since the build. It names nothing it did not store: a value core does not store (Simulation.set_input past the variable's end, a neutralized variable, a month a set_input helper skips because it already holds an input), an input the reform stored and then deleted, or anything stored on a clone or branch made during the reform. An input set since the build that the reform did not store is still reset, as before, unless the reform stores an input of the same variable: input_variables names whole variables, so a reset keeps every input of a named variable. A reform applied within another to the same simulation (a tuple's parts, or a reform that applies one) logs for both. If the reform raises, apply_reform names nothing it stored itself (a reform it applied first has named its own). It assigns a new list, because a clone or branch shares its parent's. The parameter route (disable_simulated_benefits switched on in a scenario) runs before input_variables is taken and is unchanged.
  • Reform (Count the Pension Age Winter Heating Payment in household and HBAI income #2047's winter heating split is kept: winter_fuel_allowance is not in BENEFITS, and the one dataset-year winter heating report is set as pawhp in Scotland while PAWHP is paid and as winter_fuel_allowance otherwise; it now reads each year's country, below):
    • reads the dataset year as the first four characters of its text (main's int() reads a plain year, text or number, the same way);
    • sets the one reported Carer's Allowance the same way as the winter heating report: in each year it is a Scottish carer's carer_support_payment from CARER_SUPPORT_PAYMENT_FROM (2025, the year from which carers_allowance_pre_overlap and carer_support_payment_pre_overlap move a Scottish carer to Carer Support Payment) and their carers_allowance before then, and everyone else's carers_allowance. Neither benefit is simulated beside the report. This split and the winter heating split read where the household lives in each year (country is a YEAR variable), so a household that moves into or out of Scotland is paid under that year's scheme. carers_allowance is no longer in BENEFITS. The entitlements before the overlapping-benefit reduction (the two *_pre_overlap variables) stay the model's, for both schemes, as they did for Carer's Allowance;
    • reads Count the Pension Age Winter Heating Payment in household and HBAI income #2047's PAWHP switch (gov.social_security_scotland.pawhp.active) from the system of the simulation applying it. When the parameter switches the reform on that is the tree it was built from; a reform class built from other parameters and applied later used to raise AttributeError there;
    • sets claimant_or_partner_esa_income and claimant_or_partner_jsa_income for each of the ten years, from the dataset-year reports of whoever is the claimant or partner that year (is_claimant_or_partner). Only the amounts are held at the dataset year; the roles are read in each year, as every reader of the awards reads them;
    • sets income_support from the claimant's and partner's reports alone. In the model income_support is the claimant's family's award, and the passports read it as theirs;
    • holds legacy_benefits_closed false for its ten years (NOT_MOVED): it pays every listed benefit, the legacy ones and Universal Credit alike, at its dataset-year report in every year, so it moves no family off legacy benefits. Only the closure is held; the model's other rules still apply. A family the model has claim Universal Credit anyway (would_claim_uc) is still not eligible for Housing Benefit or tax credits, and an adult outside the couple who reports income-based JSA is on it, as their own claim, only in the years the model has the scheme open (gov.dwp.JSA.income.active, to 2025; Apply the income-related ESA and income-based JSA remunerative work conditions #2031's rule), though the reform pays it in every year. The claimant and partner are on the award the reform holds for them in every year;
    • reads what it uses when it is applied: the dataset year's reports, and who is the claimant or partner and where each household lives in each year. Switched on by the parameter, it is applied in the constructor, after the data is built and before a scenario's modifier applied after data load (main's order, unchanged here). The amounts it sets do not follow a change made after that, by such a modifier or set_input, as main's reform did not; to have it read one, make the change in the situation or dataset, or apply the reform after it (Simulation.apply_reform).
  • Neutralization: neutralizing esa_income or jsa_income also neutralizes its claimant-or-partner award (CountryTaxBenefitSystem.variable_parts). An award the reform set, or one entered directly, no longer outlives the benefit it is part of, whichever order the reforms are applied in.
  • One formula: claimant_or_partner_award in policyengine_uk/utils/benefit_unit.py is the formula of both claimant_or_partner_* variables. It is Lift the benefit cap only for the support component and the people HB reg 75F and UC reg 83 name, and cap only the family's benefits #2076's award_of_members for the claimant and partner, plus the rule that an award within half a penny of zero, stored or worked out, is no award (the rule income_support_eligible gained in Apply the Income Support conditions to the claimant and partner only #2013's after-merge review).
  • Benefit cap: benefit_cap_welfare_benefits, is_uc_benefit_cap_exempt_specified_benefit and is_housing_benefit_benefit_cap_exempt_specified_benefit read the couple's income-related ESA and income-based JSA through a new helper, award_of_couple: the claimant_or_partner_* variable wherever the cap's couple is the claimant and partner (the Housing Benefit exemption's couple always is; the cap's Universal Credit couple is unless is_uc_claimant or is_uc_assessed_claimant is entered, or more than two members are entered as claimant or partner), and award_of_members for that couple otherwise. CLAIMANT_OR_PARTNER_AWARDS, the map from each award to its claimant-or-partner variable, moves to utils/benefit_unit.py, and the reform imports it.
  • Income Support gate: the income-related ESA limb (s.124(1)(h)) is claimant_or_partner_esa_income > 0 and the income-based JSA limb (s.124(1)(f)) is claimant_or_partner_jsa_income > 0. The gate now reads what the means tests and passports read, including an award entered for those variables directly or set by the reform. Main's other conditions (remunerative work, contributory JSA, the Pension Credit bar of s.124(1)(g), the State Pension Credit qualifying age) are unchanged.

Why the limbs read the variables and do not call the helper

A helper recomputes the claimant's and partner's award from esa_income and the reports. It cannot see a value the reform set on claimant_or_partner_esa_income, so limbs that called it would keep the later-year defect. With the formula deriving the award, reading the variable gives the same result as main's limbs, (award > ½p) & (~as_reported | scoped > ½p), except in the plain-total case below. Where the value is formula-derived, the variable is zero when the stored award is within half a penny of zero, the scoped award when the reports explain the stored award, and the stored award itself otherwise.

Behaviour changes to note

  • Plain total of the reports. When esa_income / jsa_income equals the plain total of everyone's reports but not the formula's award, the gate used to read the claimant's and partner's award after the capital screen. It now reads the plain total of their reports, as Count only the claimant's and partner's legacy awards in means tests and passports #2027's variables already did. Three existing tests pinned the old reading and now state the new one.
  • Directly entered claimant-or-partner awards now reach the Income Support gate, and the benefit cap wherever its couple is the claimant and partner.
  • Float residuals. Reports that exactly match the tariff income leave a residual of about £0.000002 in the stored award. income_support_eligible already read it as no award; the claimant-or-partner variables now do too. So it is no longer the claimant's or partner's award for any reader of those variables, among them the Housing Benefit, tax credit and council tax reduction means tests, the claimant's and partner's is_on_income_related_esa and is_on_income_based_jsa, in_receipt_of_income_support_jsa_ib_or_esa_ir, the council tax reduction passport, is_scp_eligible and the childcare entitlements. The benefit cap now reads it as no award too, wherever its couple is the claimant and partner. A member outside the couple is still read on their own report (own_report_is_paid), without the half-penny rule. No benefit unit in the Enhanced FRS has a stored ESA or JSA award between £0 and £1, so this moves nothing there. award_of_members itself is unchanged.
  • Neutralizing an award removes its claimant-or-partner part, including one entered directly.
  • Carers in Scotland under the reform are paid their report once, as Carer Support Payment from 2025, and get the Scottish Carer Supplement, which the model pays with any Carer Support Payment (from March 2026). The Scottish Council Tax Reduction counts carer_support_payment_pre_overlap, the model's entitlement, with the reform on as with it off.
  • Another member's Income Support (for example a non-dependent adult claiming in their own right) is left out of income_support, and so of household income, under the reform. That is what the model does with the reform off: would_claim_IS and income_support_eligible read only the claimant's and partner's reports. ESA and JSA differ: esa_income and jsa_income count every member's award either way.
  • Caching. The gate and the benefit cap read a calculated variable, so code that changes esa_income / jsa_income after calculating them has to clear claimant_or_partner_* as well as the gate or the cap's variables. The three test helpers that recalculate the gate do so.
  • Benefit cap under the reform. Wherever the cap's couple is the claimant and partner (always, unless is_uc_claimant or is_uc_assessed_claimant is entered, or more than two members are entered as claimant or partner), another member's held ESA or JSA no longer enters the cap's total or lifts the cap in any year. For any other Universal Credit couple the cap still reads esa_income and jsa_income by value, so under the reform it still takes another member's held award to be the couple's after the dataset year (the Enhanced FRS enters none of these inputs, and is_claimant_or_partner's formula flags at most two members). The cap's own rules are unchanged.
  • Reform amounts. As before, the reform holds amounts at their dataset-year level; it does not uprate them, and because they are inputs, a reform that changes a benefit's parameters does not change them.
  • Inputs a later reform stores, including inputs it replaces, are now kept by reset_calculations and the marginal rate formulas, and exported as inputs by UKSingleYearDataset.from_simulation, as the parameter route's always were. Under policyengine-core 3.33 (the lock), derivative keeps every input either way.

Invariants

For every benefit unit and each of the reform's ten years, with non-negative reports:

  • esa_income and jsa_income equal the sum of every member's dataset-year report, and claimant_or_partner_* equals the sum of that year's claimant's and partner's (conservation);
  • 0 <= claimant_or_partner_* <= esa_income / jsa_income (bounds). For formula-derived values this holds to the rule's half penny. A value entered directly is read as entered;
  • the stored values are the same in every year (the reform does not uprate), except that Income Support and the claimant-or-partner awards change in a year in which who is the claimant or partner changes;
  • legacy_benefits_closed is false in every one of the reform's years;
  • income_support_eligible is false whenever either claimant-or-partner award is positive (the limbs only bar);
  • the council tax reduction passport equals "Income Support, or either claimant-or-partner award, is positive".

Under the reform, in each of its years and wherever the cap's couple is the claimant and partner, the benefit cap's total, both schemes' specified-benefit exemptions and both cap reductions are the same whether or not a member outside the couple reports ESA or JSA (metamorphic). With it off, the cap's total equals the reader it replaces to within a penny, and the couple's award the exemptions read equals award_of_members for the couple except that an award within half a penny of zero is none (differential).

For every person and each of the reform's ten years: carers_allowance + carer_support_payment equals the person's dataset-year Carer's Allowance report (conservation), and the report is carer_support_payment exactly in Scotland from 2025 (placement), which is where the model with the reform off pays Carer Support Payment rather than Carer's Allowance (differential). For every household, winter_fuel_allowance + pawhp equals its winter heating report (#2047's invariant).

For a reform applied to a built simulation: it adds to input_variables exactly the variables it leaves an input of its own for, and never changes a parent's list; a reset keeps each of those inputs, and every variable it does not add, and the build did not name, recalculates as if nothing had been set since the build.

Differential: in formula mode, and for awards entered directly, set later, set on a branch or recalculated after deletion, the new limbs and gate equal the code they replace. The copy of that code is main's gate at e89b8ab, which is unchanged at b4dbaf5, including its half-penny limbs and Pension Credit bar. The one exception is the plain-total case above, which is checked against its own expectation.

Enhanced FRS

Real runs on a private copy of the Enhanced FRS 2024-25, 2025-2030. Base b4dbaf57a (main, without this PR) against this PR's head c2e069da9 and the previous head 593259fd8, all clean checkouts, reform off and on. Aggregates only. Scripts and outputs: impact/r2026-10-17 (the base, and 871e46de9), impact/r2026-10-17b (593259fd8) and impact/r2026-10-17c (c2e069da9) in the author's review folder. Every figure below holds at both heads: at c2e069da9 the reform-off run again matches main in all 120 arrays, and with the reform on all 120 arrays equal those at 593259fd8 in every year.

  • Reform off (the default): £0. All 120 compared benefit-unit, person and household arrays are identical in every year 2025-2030. They include income_support_eligible, both claimant-or-partner awards, every passport, carers_allowance, carer_support_payment, the benefit cap's total, exemptions and reductions, and household net income. 74 benefit units have a member who is neither the claimant, the partner nor a child or young person they are responsible for; none of those members reports ESA, JSA or Income Support.
  • Reform on, this branch. It runs. In each year 2025-2030:
    • the claimant-or-partner ESA and JSA awards equal the stored awards in every benefit unit (1,084 with ESA, 38 with JSA), as they must when no other member reports them;
    • the stored awards are the same in every year;
    • no benefit unit is Income Support eligible while either claimant-or-partner award is positive.
  • Reform on, the benefit cap fix. Against the previous head 871e46de9, all 120 arrays are identical in every year, as expected with no member outside the couple reporting ESA or JSA. The carer and winter heating arrays are identical to the run at d274f35eb, and no household's country changes across the years (run r2026-10-16).
  • Reform on, this branch against main's reform. Since Count the Pension Age Winter Heating Payment in household and HBAI income #2047, main's reform runs too. Household incomes differ only in the 119 households with a carer in Scotland who reports Carer's Allowance. main's reform pays them that report as Carer's Allowance (£506m in 2025, £516m in 2030) beside a simulated Carer Support Payment; this branch pays the report once, as Carer Support Payment. So household benefits are £513m lower in 2025 and £590m lower in 2030, and household net income £452m and £515m lower (both benefits are taxable, so household tax is £61m to £74m lower, in 55 households). The same double count had entered the means tests' income: council tax reduction applicable income is £443m lower in 2025 (121 benefit units), Housing Benefit applicable income £260m lower (99) and tax credits applicable income £485m lower (115). The benefit cap's total of welfare benefits is £331m higher in 2025 (106 benefit units) and £378m higher in 2030 (130); household incomes do not move with it outside the 119 households above, as the reform holds Universal Credit and Housing Benefit at their reports. The benefit cap reduction and some people's poverty status also move, each for a small number of records.

As before the fix, the reform holds each amount at its dataset-year cash level, so its later-year totals fall behind the baseline's: with the reform on, household benefits are £8.1bn below the reform-off run in 2025 and £37.6bn below in 2030 (informational only). Whether it should use each year's reported amounts instead is a separate question (follow-up task).

Tests

  • test_disable_simulated_benefits.py:
    • a six-family situation over 2025, 2026 and 2027. It has an adult outside the couple reporting ESA and JSA, the claimant's own ESA, JSA and screened-out ESA, and another member's Income Support;
    • it checks the stored and claimant-or-partner awards, both Income Support limbs, the council tax reduction passport and each person's is_on_* status in each year (the outside adult's own JSA status only while the model has the scheme open). It also shows that a value-only reading would take the excluded adult's ESA as the couple's in later years;
    • no family is moved off its legacy benefits with the reform on, while every one is from 2026 with it off;
    • a multi-year dataset;
    • a conservation and bounds property over random families;
    • review regressions: the reform applied to a clone and to a branch sets their inputs and none of the parent's; applied later (as reform=, to a clone, to a branch) it keeps what it set through reset_calculations and leaves the parent's input_variables alone, also when it replaces claimant-or-partner awards set after the build; neutralizing ESA and JSA after the reform, before it, and with an award entered directly; another member's Income Support left out with the reform on and off; a claimant who stops being the claimant in a later year.
  • test_income_support_gate_reads_claimant_or_partner_awards.py, the differential test against main's gate: examples (formula mode, awards entered in the situation, set after calculating, set on a branch and on a nested branch, recalculated after deletion); a property over stored values at and around both readings (±£0.004, ±£0.006); the full old and new gates on the eligibility property families; an entered claimant-or-partner award alone decides its limb, whatever an adult outside the couple reports.
  • test_reform_inputs_survive_reset.py, on apply_reform's naming: a reform that stores an input keeps it through reset_calculations on every storing route; a reform names nothing it did not store; a deep copy or pickle of the record made during a reform is a plain set; and a property against a model of what the reform leaves.
  • test_disable_simulated_benefits_carers.py: the reported case (a carer in Scotland and one in London, £4,000 each); conservation and placement in every year with the dataset year 2023 and 2025, for Simulation and Microsimulation and for the reform passed as reform=; the differential against the model's own scheme choice with the reform off; household_benefits and hbai_household_net_income rising by exactly the report plus the Scottish Carer Supplement; and a Hypothesis property over regions, reports, care hours and dataset years.
  • test_disable_simulated_benefits_benefit_cap.py: under the reform, on four capped families (Universal Credit and Housing Benefit, with and without the claimant's support component), the cap's total, both exemptions and all reductions are the same in 2025-2027 whether or not the other adult reports ESA and JSA, and the cap bites in each; with the reform off, a Hypothesis differential against main's replaced benefit_cap_welfare_benefits and against award_of_members for the exemptions' couples; the fallback for a couple that is not the claimant and partner; an entered claimant-or-partner award reaching the cap. With the three readers reverted, 4 of its 6 tests fail; with each reverted alone, at least one does.
  • The carer file also has the moving carers from juaristi22's review (each year's outputs equal a carer who stayed, on the scenario and reform= routes, through the supplement and household income), a property with each household's region drawn per year, and a carer moved to Scotland by set_input before the reform is applied, who is paid Carer Support Payment from the move.
  • Count the Pension Age Winter Heating Payment in household and HBAI income #2047's test_disable_simulated_benefits_winter_heating.py (with carers_allowance added to its list of reported benefits set outside BENEFITS, and moving pensioners) and Add versioned capital gains projection scenarios #2274's test_capital_gains_projection.py (which covers apply_reform's projection refresh) pass, as do main's benefit cap property files.
  • Targeted pytest (the files above plus the gate, direct-input, entered-directly, lifecycle, legacy-reader, household-income-list, measurement-branch and marginal-rate files, and main's five benefit cap property files): 380 passed at 593259f (21 files). At c2e069d, which changes only docstrings, the changelog and how the same variable_parts map is built: 116 passed in the six files on the reform, the gate, the entered awards, neutralization, the reset and the benefit cap.
  • YAML at the head: tests/policy/baseline/finance/benefit/family, .../gov/local_authorities/council_tax_reduction and main's benefit cap YAML (gov/dwp/benefit_cap, benefit_cap.yaml, benefit_cap_by_scheme.yaml, carers_allowance_overlapping_benefits.yaml): 1,511 passed at 593259f.
  • ruff format --check (the repo's ruff and the unpinned uvx ruff CI runs) and ruff check clean.

Reviews

  • r1 (REQUEST_CHANGES on 9e88021): clone link fixed; neutralization removes the scoped awards; another member's Income Support answered under Behaviour changes; roles read in each year; wording fixed.
  • Vahid Ahmadi's review (7cd3c34): base conflict resolved by merging main; Income Support as r1; Enhanced FRS re-run on the head; Count the Pension Age Winter Heating Payment in household and HBAI income #2047 has since merged and its split is kept.
  • After merge (135b384), r3 (8b278e3), r4 (0d53268), r5 (5f1c878): the inputs a later reform stores, the routes core stores them by, logging through record replacement, copy and pickle, whole-variable naming. Each counterexample is a regression test (see test_reform_inputs_survive_reset.py).
  • r6 (REQUEST_CHANGES on 587c0e7):
    1. Move families onto Universal Credit when a legacy benefit they receive closes #2119's closure ended an outside member's ESA and JSA the reform still pays: the reform holds legacy_benefits_closed false (NOT_MOVED), with a test that no family is moved with the reform on.
    2. derivative under core 3.33: the comments, the test docstring, the changelog and this body now say reset_calculations deletes unnamed variables, as derivative did before 3.33.0.
    3. The _InputKeyLog docstring says a deep copy or pickle logs nothing.
  • r7 (REQUEST_CHANGES on 5366cf0, documentation only):
    1. This body's derivative claims: fixed above.
    2. The closure comment and changelog overstated what agrees with the held awards: they now say only the closure is held and the model's other rules apply (the Universal Credit claim example).
    3. Holding the closure false is defensible: no change. On the Enhanced FRS it changes no household's benefits or net income with the reform on (only intermediates such as Housing Benefit applicable income), and nothing with it off.
  • Since r7: the merge with main at a98f8f0 brought Apply the income-related ESA and income-based JSA remunerative work conditions #2031's rule that an outside member's own income-based JSA status needs the scheme open. The reform follows it (above); holding the scheme open instead would need a parameter change, which a branch shares with its parent.
  • Vahid Ahmadi's second review (5366cf0):
    1. The conflicts with main: resolved by merging main at a98f8f0, then dd64849, 7e10ccd, a94c65d, e7610ac and b4dbaf5. Count the Pension Age Winter Heating Payment in household and HBAI income #2047's split is kept after the loop, winter_fuel_allowance is out of BENEFITS, the dataset year is int(str(...)[:4]), and there is one tax_benefit_system.simulation link and one apply_reform. Read only the parent's and partner's ESA in extended childcare, and test two more legacy award readers #2261's reader and NOT_MOVED agree: test_no_family_is_moved_off_its_legacy_benefits and test_each_person_is_on_their_own_award pass at the head, so the outside member stays on the income-related ESA the reform pays in every year.
    2. The Scottish carer double count: fixed here (above), with a conservation test like Count the Pension Age Winter Heating Payment in household and HBAI income #2047's.
    3. Overlaps: Let Scenario structural reforms read the simulation, and add baseline regression tests #2193's apply_reform sets the same link, so whichever of Let Scenario structural reforms read the simulation, and add baseline regression tests #2193 and this PR lands second keeps one method. Give War Widow's Pension its own CPI-uprated variable ahead of freezing BSP #2382's war_widows_pension entry should be kept in BENEFITS by whichever lands second, so test_every_listed_benefit_has_what_the_reform_reads_and_sets covers it.
    4. apply_reform and core internals: a core release that renamed or restructured _user_input_keys would not pass silently. Core would stop writing to the logged record, apply_reform would name nothing, and the later-reform tests (test_a_reform_applied_later_keeps_what_it_sets_through_a_reset, test_reform_inputs_survive_reset.py) would fail. Moving "inputs a later reform stores survive a reset" into policyengine-core is a follow-up: Keep the inputs a reform applied after the build stores through reset_calculations policyengine-core#617.
    5. The test counts under Tests are from this head and the commit before it.
  • r8 (25ddfd3), r9 (ab800e8) and r10 (d274f35): lost without a verdict when the lanes running them died.
  • juaristi22's review (CHANGES_REQUESTED on ab800e8): C1, the carer split read the dataset year's country, so a carer who moved to Scotland was paid Carer's Allowance and lost the Scottish Carer Supplement. Fixed in c5a8d1e: both splits read each year's country, with moving-carer and moving-pensioner tests. juaristi22 then APPROVED d274f35.
  • r11 (two independent reviews of the whole PR at 871e46d, by GPT-6.1 Sol and Opus 5.5; both REQUEST_CHANGES):
    1. The benefit cap's readers took another member's held ESA or JSA to be the couple's under the reform: fixed (above, award_of_couple), with metamorphic and differential tests.
    2. The changelog's first bullet claimed fixes Count the Pension Age Winter Heating Payment in household and HBAI income #2047 had shipped: rewritten.
    3. This body was stale: refreshed for this head.
    4. A region changed after the reform is applied, by a scenario's modifier applied after data load or by set_input, is not followed. Answered: the reform reads its inputs when it is applied, as main's reform does, and the constructor applies it before that modifier (main's order, unchanged here). Following a later change would need the reform to replace formulas instead of setting inputs, or the constructor's order to change for every reform. The docstring and changelog now say when it reads its inputs, and a test covers applying the reform after the change.
    5. The outside member's own income-based JSA status under the reform (Apply the income-related ESA and income-based JSA remunerative work conditions #2031's rule, above): no change. Its readers are the winter heating eligibility and the legacy council tax reduction's non-dependant deduction, whose outputs (winter_fuel_allowance, pawhp, council_tax_benefit) the reform sets, so no household figure depends on it; main applies the same rule with the reform on.
  • r12 (two reviews of 593259f, both Opus 5.5; both REQUEST_CHANGES, on this body only): the code is right and needs no change. r11's items 1, 2, 4 and 5 are resolved; both re-ran their probes (the cap's total with the reform on now equals the control's) and the mutants, and neither could make a household output depend on the outside member's JSA status. Both asked for this body to be published with a qualifier: another member's held award stays out of the cap wherever the cap's couple is the claimant and partner, not for a Universal Credit couple that is not (now stated above). Nits, fixed in c2e069d: award_of_members' docstring named the wrong readers of award_of_couple; the fallback's docstring and the changelog now name every input that reaches it; the new test's docstring carries the same qualifier; variable_parts is built from CLAIMANT_OR_PARTNER_AWARDS (same value).
  • r13 (two reviews of c2e069d, the nits commit and this body, both Opus 5.5): both APPROVE. Their nits: the entered-award bullet above needed the same couple qualifier (added); the r12 bullet now gives its verdicts; three comments from main (utils/benefit_cap.py, benefit_cap_welfare_benefits, is_uc_benefit_cap_single_claimant_rate) still say only an entered is_uc_claimant separates the two couples, which this PR's docstring now states in full, and the two claimant_or_partner_* variables' documentation does not yet name the benefit cap among their readers (both follow-ups, documentation only); the float residual bullet's list of readers was short (now completed).

axiom: n/a: model reform

🤖 Generated with Claude Code

Reproducer: policyengine_uk/tests/test_disable_simulated_benefits.py::test_every_listed_benefit_has_what_the_reform_reads_and_sets, policyengine_uk/tests/test_disable_simulated_benefits.py::test_the_reform_is_off_by_default, policyengine_uk/tests/test_disable_simulated_benefits.py::test_the_reform_sets_the_awards_for_ten_years, policyengine_uk/tests/test_disable_simulated_benefits.py::test_the_stored_awards_are_the_dataset_years_reports, policyengine_uk/tests/test_disable_simulated_benefits.py::test_later_years_need_the_awards_set_explicitly, policyengine_uk/tests/test_disable_simulated_benefits.py::test_the_claimant_or_partner_awards_are_their_own_reports

MaxGhenis and others added 3 commits October 2, 2026 07:22
…ons' into disable-simulated-benefits-scoped-awards
The reform raised before setting anything: the simulation never linked
itself to its tax-benefit system, the dataset year is text for a situation
and a number for a multi-year dataset, and Attendance Allowance, DLA and PIP
have no reported amount since #1656.

It holds each benefit at its dataset-year amount while the reported amounts
uprate, so in later years the value rule read the whole of esa_income or
jsa_income as the claimant's or partner's. The reform now sets
claimant_or_partner_esa_income and claimant_or_partner_jsa_income for each
year from their own dataset-year reports, and Income Support from theirs
alone.

income_support_eligible reads those two variables for its income-related
ESA and income-based JSA limbs, as the means tests and passports do. One
helper, claimant_or_partner_award, is the formula of both variables. Where
the stored award equals only the plain total of the reports, the gate now
reads the plain total of the claimant's and partner's reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rtner' into disable-simulated-benefits-scoped-awards
…rtner' into disable-simulated-benefits-scoped-awards
Review r3c on #2027 found that #2013's income_support_eligible ignored a
directly entered claimant_or_partner_esa_income: it worked the couple's
award out from esa_income and everyone's reports, so an adult outside the
couple could flip eligibility. #2075's gate reads the claimant-or-partner
awards as they stand; these tests pin that down.

- A carer otherwise eligible for Income Support, with an outside adult
  reporting 2,000, 3,000 or 5,000 of the award and 3,000 of esa_income or
  jsa_income entered for the benefit unit:
  - 3,000 entered as the claimant's and partner's award: never eligible,
    no Income Support;
  - 0 entered as theirs: always eligible.
  Both for ESA and for JSA, in 2025 (income-based JSA still active).
- Property: with the claimant-or-partner awards entered, redrawing the
  outside adult's ESA and JSA reports never changes the gate, and the gate
  equals the gate with both awards entered as zero, barred exactly where
  either is positive.

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

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Max. I reviewed this at head 7cd3c34df. I looked only at this PR's own change (1f689253a and the two merges of #2027), not at the #2025 code it brings in.

What I checked:

  • The fix works. On #2027's head (fc1ff55d1), turning on the reform with a situation still raises AttributeError: 'CountryTaxBenefitSystem' object has no attribute 'simulation'. On this branch it runs, for situations with periods keyed by year and with scalar values.

    • The new link at simulation.py:190 matches core 3.32.17, which sets self.tax_benefit_system.simulation = self in its own constructor.
    • Each UK Simulation builds its own CountryTaxBenefitSystem, so the link can't leak between simulations. I built two reformed simulations in one process: they kept their own ESA (£4,000 and £1,000 in 2027), and their system objects were not the same.
  • Ten-year hold. A couple with a non-dependant who reports £4,000 of ESA. With the reform, esa_income is £4,000 in both 2025 and 2027. claimant_or_partner_esa_income is £0 in both years, Income Support eligibility is false, and only the non-dependant is on ESA. Without the reform, esa_income uprates to £4,173.82 in 2027 and the couple's award stays at £0.

  • Claimant and non-dependant both reporting (£4,000 and £3,000). Under the reform, esa_income is £7,000 and the claimant-or-partner award is £4,000 in 2025, 2026 and 2027.

  • Directly entered award. A single adult with esa_income of £5,000 entered directly gets a claimant-or-partner award of £5,000, and the Income Support gate is barred. That is the same as on #2027.

  • Law. SSCBA 1992 s.124(1)(f) and (h), read on legislation.gov.uk:

    (f) he is not entitled to a jobseeker's allowance and, if he is a member of a couple, the other member of the couple is not, and the couple are not, entitled to an income-based jobseeker's allowance

    (h) he is not entitled to an employment and support allowance and, if he is a member of a couple, the other member of the couple is not entitled to an income-related employment and support allowance

    The limbs read the claimant's and partner's income-related awards. The claimant's own contributory ESA and JSA are still barred separately (income_support_eligible.py:126-135), so both limbs are covered.

  • Tests.

    • The seven changed pytest files pass, each run in its own process: 83 tests.
    • YAML: finance/benefit/family and the council tax reduction folder, 511 passed.
    • I also ran main's read-only-cache test (code_health/test_cached_arrays_not_written_in_place.py) against this branch. It fails 6 of 35, exactly as it does on #2027's head (pip_reform.py, uc_reform.py), so the failures come from the stack and not from this PR.
  • Calculator inputs don't change. I ran 9 common API households, with and without roles, against #2027's head: a single earner who rents, a couple with two children, a lone parent, a pensioner couple, ESA or JSA reported, a non-dependant with ESA, and a lone parent on Income Support. household_net_income, UC, IS, HB, PC, ESA, JSA and CTR are identical to the penny.

Findings:

  1. Should fix: the branch conflicts with its own base. #2027 moved to fc1ff55d1, which conflicts with this branch in variables/gov/dwp/claimant_or_partner_esa_income.py and claimant_or_partner_jsa_income.py. Only the documentation strings conflict: #2027 added "Values are compared to within half a penny after rounding to the precision esa_income is stored in". Fix: merge #2027 again, keep this branch's helper-based formula, and move that half-penny sentence into the new documentation (it is already in the claimant_or_partner_award docstring).

  2. Should fix: under the reform, another member's Income Support report leaves household income. CLAIMANT_OR_PARTNER_ONLY = ["income_support"] (reforms/policyengine/disable_simulated_benefits.py:53, used at :104) sets income_support from the claimant's and partner's reports only. A non-dependant's own ESA and JSA stay in esa_income and jsa_income, but their Income Support disappears.

    • Probe, reform on, 2025: a claimant plus a non-dependant who reports £3,000 of Income Support gives income_support £0 and household_benefits £0. The same non-dependant reporting £3,000 of ESA gives household_benefits £3,000.
    • Your eFRS run says no such member reports Income Support, so this changes nothing on the data. It does break the "full awards stay in household income" rule for calculator situations.
    • Fix: either carry the other members' Income Support reports into household income, as ESA and JSA do (for example a separate other-member amount that counts in household_benefits but that the passports don't read), or state the exclusion in the reform docstring and the changelog. Then the excluded_income_support: 0 expectation in test_disable_simulated_benefits.py reads as intended rather than as a loss.
  3. Nit: the eFRS figures were run on 9e88021a4, not the current head. The latest merge of #2027 came after that run. A one-line note that the later merge only brings in #2027 would do. CI hasn't run because the base isn't main, so please let it run once the stack retargets.

  4. Nit: sequencing with #2047. It rewrites the same BENEFITS block in disable_simulated_benefits.py and parses the year as int(simulation.dataset.time_period), which would hit the text-year TypeError this PR fixes. Your body already covers the winter-heating split. Please also keep this PR's int(str(...)[:4]) when resolving. I checked that this branch on its own doesn't count anything twice: pawhp isn't in household_benefits here.

The PR body has aggregates and unweighted benefit-unit counts only. It has no record ids, weights or record-level amounts.

Happy to approve once it merges cleanly onto #2027.

@MaxGhenis
MaxGhenis changed the base branch from legacy-award-readers-claimant-partner to main October 9, 2026 22:23
MaxGhenis and others added 7 commits October 10, 2026 12:17
…mulated-benefits-scoped-awards

Conflicts:
- claimant_or_partner_{esa,jsa}_income documentation: main's wording (young
  persons with their own award, the half-penny comparison) plus this PR's
  (the Income Support gate reads it; disable_simulated_benefits sets it for
  each year).
- income_support_eligible: keep main's Pension Credit bar (s.124(1)(g)) and
  the SPC qualifying age; the ESA and JSA limbs read the claimant-or-partner
  awards as before (> 0).

Main's gate also gained a half-penny rule after #2013's after-merge review
(6eb8db7): an award within half a penny of zero never bars the claim, so
the float residual left by reports that exactly match the tariff income is
no award. To keep that rule while the limbs read the shared variables,
claimant_or_partner_award now gives zero when the stored award or the
claimant's and partner's part of it is within half a penny of zero. Every
reader of the two variables (passports, means tests, the gate) now reads
the residual the same way; on the formula path the gate equals main's in
every case except the declared plain-total one.

The differential test's copy of the replaced code is now main's gate at
e89b8ab (half-penny limbs, has_attained_state_pension_credit_qualifying_age,
on_pension_credit), and pre_switch_limb uses the same half-penny thresholds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 (REQUEST_CHANGES on 9e88021,
~/reviews/disable-simulated-benefits-2026-10-02/subfleet/review_r1.md) and
Vahid Ahmadi's review on 7cd3c34.

1. A clone or branch applying a reform set its parent's inputs: a cloned
   system copies the link to the parent, and a branch shares the parent's
   system. Simulation.clone now links a cloned system to the clone, and
   Simulation.apply_reform links the system to the simulation applying the
   reform while it runs, then restores the link. Regression: the reform
   applied to a clone and to a branch sets their inputs and none of the
   parent's.
2. A reform that neutralized esa_income or jsa_income left the
   claimant-or-partner awards the reform had set as inputs, so passports
   and the Income Support bar outlived the benefit. Neutralizing either
   award now neutralizes its claimant-or-partner part
   (CountryTaxBenefitSystem.variable_parts), whether the reform runs first
   or second, and for an award entered directly. Parameter changes cannot
   remove the reform's amounts by design: they are inputs.
3. Income Support from another member's own report (r1 #3, Vahid #2): left
   out of income_support with the reform on, as it is with the reform off,
   where would_claim_IS and income_support_eligible read only the claimant's
   and partner's reports. Stated in the reform, the changelog and a test
   that checks both modes.
4. Roles: the reform now reads is_claimant_or_partner in each year, holding
   only the amounts at the dataset year, so every reader in a year sees the
   same couple as the award. Regression: a claimant who stops being the
   claimant in a later year. On the Enhanced FRS roles never change between
   years (ages are carried over), so this moves nothing there.
5. Wording: most *_reported variables are uprated (not ssmg_reported or
   council_tax_benefit_reported); the multi-year dataset test keeps its
   roles fixed in every year.

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

No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers the after-merge review of 135b384.

- simulation.py: input_variables is taken when the simulation is built, and
  reset_calculations, derivative and the marginal rate formulas delete every
  variable it does not name. A reform applied afterwards (a Reform class
  passed as reform=, or applied to a clone or branch) supplied inputs it did
  not name, so a reset deleted the claimant-or-partner awards and the
  formula read an outside adult's ESA as the couple's. apply_reform now
  names the variables whose inputs the reform recorded, assigning a new
  list because a clone or branch shares its parent's.
- Test: on all three routes the reform's inputs are named, the parent's
  list is untouched, derivative and a reset leave every value the reform set
  and the Income Support gate as they were. Without the fix each route
  fails (the reset changes six reform-set arrays and flips the gate; the
  derivative is nonzero).
- Reform docstring: the amounts are the same in every year except Income
  Support and the claimant-or-partner awards, which follow who is the
  claimant or partner.
- Changelog: the half-penny rule applies to the claimant's and partner's
  award; a member outside the couple is still read on their own report.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able-simulated-benefits-scoped-awards

No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answers review r3 of 8b278e3.

- simulation.py: apply_reform named only variables with a new input record.
  An input set after the build is recorded but not named in
  input_variables, and a reform that replaces it adds no record, so a reset
  deleted what the reform set: the excluded adult's claimant-or-partner ESA
  went from 0 to 3,000 and the Income Support gate flipped. set_input now
  records, while apply_reform runs, the variables the reform sets on the
  simulation applying it (a ContextVar, so a clone or branch made meanwhile
  records nothing and nested reforms restore the outer record), and
  apply_reform names those that are still inputs of that simulation, as
  well as those with new records.
- The comment and changelog say the marginal rate formulas also keep
  variables without a formula, and that replaced inputs survive too.
- Test: a reform that replaces claimant-or-partner awards set after the
  build, applied to the simulation itself, a clone and a branch (the
  control: a branch's records are new), keeps them through derivative and a
  reset, and does not name an input set after the build that it did not set.

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

Copy link
Copy Markdown
Collaborator Author

Status at head 0d5326898 (scheduled lane, 2026-10-11):

MaxGhenis and others added 4 commits October 11, 2026 18:49
Review r4 (REQUEST_CHANGES on 0d53268) found the set_input tracker both
too broad and too narrow. It named a variable when a reform called
Simulation.set_input, even when core stored nothing (past the variable's
end, a set_input helper skipping months that hold inputs) or the reform
deleted what it stored, and then any surviving input of that variable was
kept through a reset. It missed inputs stored through Holder.set_input,
from another thread, or while another simulation applied a reform.

While apply_reform runs, the simulation's _user_input_keys is now an
_InputKeyLog: a set that also logs every key core's Holder._set adds,
which it does once it has stored an input, on every route. apply_reform
names each variable with a logged key the simulation still holds after
the reform, under a branch name it reads. A reform applied within another
logs for both. The ContextVar and the set_input override are gone.

New test file covers each storing route, each non-storing case r4 found,
clones and branches made during a reform, a raising reform, and a
property test against a model of what the reform leaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build makes a holder for every variable, and a holder keeps the
definition it was made with, so core stored the input for a variable the
reform neutralized and no warning was raised. Neutralize a variable added
after the build, whose holder the reform's write makes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r5 (REQUEST_CHANGES on 5f1c878): core can replace a simulation's
_user_input_keys while a reform is applied to it (a reform applied to its
parent rebuilds a branch's record; rebinding the system rebuilds it), and
the log was then lost, so a later input was not named. And an
_InputKeyLog could not be copied or unpickled, so a simulation could not
be deep-copied or pickled while a reform ran.

_user_input_keys is now a property on Simulation, kept in __dict__ as
core keeps it. While apply_reform applies a reform, a set assigned to it
becomes a log. The active logs are kept by simulation id outside the
simulation, because core's clone copies __dict__. _InputKeyLog reduces to
a plain set for copy and pickle.

Docs say a reset keeps or clears whole variables, and what a raising
reform names. Tests: a branch's reform after its parent applies one, a
record rebuilt mid-reform, copies of the record during a reform; the
property now also checks that a named variable keeps its other inputs.

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

The differential property drew six derandomised populations, and nothing
made one of them include a household that receives PAWHP. Two explicit
examples now always run: a single Scottish pensioner in 2025 and a Scottish
couple on Pension Credit in 2024. The fixed check that used to build its own
population now checks that each example receives PAWHP.

A new check fails if a benefit is both in disable_simulated_benefits'
BENEFITS and excluded from it. winter_fuel_allowance in particular must stay
out of BENEFITS: the winter heating split sets it, and a generic entry as well
would be harmless only while the split runs after the generic loop (relevant
when #2075 meets this branch).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able-simulated-benefits-scoped-awards

Conflict: tax_benefit_system.py's typing import (main added Callable and
Optional; this branch added Tuple). Took the union.

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

Copy link
Copy Markdown
Collaborator Author

A note from #2047 for this PR's generic list, from #2047's review (finding 3), re-run on #2047's merge of main at 1d3abecef.

Under disable_simulated_benefits, a Scottish carer's reported Carer's Allowance is counted twice from 2025. carers_allowance is in BENEFITS, so it is set from carers_allowance_reported in every country. carer_support_payment stays simulated, and carer_support_payment_pre_overlap still pays a Scottish carer with carers_allowance_reported > 0 from 2025. Its overlapping_benefits list does not include carers_allowance.

Probe (synthetic data: two people aged 40, carers_allowance_reported £4,000, care_hours 40, one in Scotland and one in London, dataset year 2025):

Year carers_allowance [Scotland, London] carer_support_payment [Scotland, London]
Reform on 2025 [4000.0, 4000.0] [4331.6, 0.0]
Reform on 2026 [4000.0, 4000.0] [4495.4, 0.0]
Reform off 2025 [0.0, 4331.6] [4331.6, 0.0]
Reform off 2026 [0.0, 4495.4] [4495.4, 0.0]

A suggested fix, following #2047's winter heating split: for a Scottish household from 2025, the report sets carer_support_payment (and its _pre_overlap) and carers_allowance gets 0; elsewhere the report stays carers_allowance. A conservation test like #2047's test_reported_winter_heating_is_conserved would cover it.

Also for whichever of #2075 and #2047 lands second: keep #2047's winter heating split block after the generic loop, take winter_fuel_allowance out of BENEFITS (#2047's test_no_benefit_is_both_listed_and_excluded fails otherwise), and keep one tax_benefit_system.simulation line.

MaxGhenis and others added 2 commits October 13, 2026 03:27
Review r6 (REQUEST_CHANGES on 587c0e7):

1. Main's #2119 closes income-based JSA and Income Support from April 2026
   and then moves a family reporting either off all its legacy benefits
   (legacy_benefits_closed), ending its ESA, JSA and Income Support. Under
   disable_simulated_benefits, which pays every reported award in every
   year, an adult outside the couple was then paid ESA and JSA but not on
   them, since own_report_is_paid reads esa_income_eligible. The reform now
   holds legacy_benefits_closed false for its ten years (NOT_MOVED), as it
   already holds Universal Credit and the legacy awards at their reports, so
   every reader of the closure agrees with the awards it pays. New test:
   with the reform off every test family is moved from 2026; with it on,
   none is, in any of the ten years. The clone-or-branch test now compares
   the parent's ESA with an unreformed simulation's, which the closure ends
   from 2026, instead of expecting the excluded adult's uprated report.

2. From policyengine-core 3.33.0 derivative keeps every input, named or
   not (3.32.x deleted every variable input_variables does not name). The
   comments, the test docstring and the changelog now say reset_calculations
   deletes unnamed variables, as derivative did before 3.33.0.

3. _InputKeyLog docstring: a deep copy or pickle of a simulation logs
   nothing; a shallow copy shares the record, as it shares the holders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…2274) into disable-simulated-benefits-scoped-awards

Conflicts:

- disable_simulated_benefits.py: #2047 (PAWHP in household income) moved
  BENEFITS to a module constant without winter_fuel_allowance and added the
  winter heating split after the benefit loop. Kept the split as #2047 wrote
  it, and the branch's per-year loop, constants (PRE_MINIMUM,
  CLAIMANT_OR_PARTNER_AWARDS, CLAIMANT_OR_PARTNER_ONLY, NOT_MOVED) and
  dataset-year parsing; the split uses the same years range.
- simulation.py: both sides set tax_benefit_system.simulation before the
  structural reforms (kept one, with the branch's comment). Main's #2274
  overrides apply_reform to refresh the capital gains projection; the branch
  overrides it to link the system and name the inputs a reform stores. Git
  kept both definitions, the second hiding the first. They are now one
  method that refreshes the projection after core applies the reform, as
  main's did, inside the branch's logging and linking.
- utils/benefit_unit.py and claimant_or_partner_{esa,jsa}_income.py: #2076
  generalised the scoped-award formula to award_of_members(..., members),
  which the benefit cap readers use. claimant_or_partner_award is now
  award_of_members for the claimant and partner plus the half-penny rule, so
  both variables keep one helper as their formula and the values are
  unchanged. award_of_members reads its tolerance from HALF_PENNY (0.005, as
  before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 13, 2026 15:56
Review r7 (REQUEST_CHANGES on 5366cf0, documentation only):

- Finding 2: the NOT_MOVED comment and the changelog said everything that
  reads the closure agrees with the awards the reform pays. Holding
  legacy_benefits_closed false removes only the closure's own effects; the
  model's other rules still apply, so a family the model has claim Universal
  Credit anyway (would_claim_uc) is still not eligible for Housing Benefit or
  tax credits while the reform pays what it reports. The comment, the reform
  docstring and the changelog now say so. The comment also says the
  migration notice ends a family's legacy awards whether or not it then
  claims Universal Credit, unless DWP sent it to Pension Credit.
- Finding 1 (the PR body's derivative claims) is fixed in the PR body.
- Finding 3 (holding the closure false is defensible) needs no change.

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

#2031 (merged into this branch from main in 6f0148a) has an adult outside
the couple on income-based JSA, on their own claim, only while the scheme is
open (gov.dwp.JSA.income.active, false from 2026). Under
disable_simulated_benefits the reform still pays the JSA they report in every
year, and the claimant and partner stay on the award the reform holds for
them, but the outside member's own status now ends from 2026. A probe of
the merge (a couple with an adult outside it who reports JSA) gives that
adult is_on_income_based_jsa true in 2025 and false in 2026 and 2027, where
test_each_person_is_on_their_own_award expected true in every year.

The reform holds only the move off legacy benefits (NOT_MOVED), and the
model's other rules apply (review r7's qualification). Overriding the scheme's
closure for the reform would need a parameter change, which a branch would
share with its parent, or statuses calculated during construction. So the
test now expects the outside member on the JSA they report only in the years
the model has the scheme open, and checks that 2025 is open and 2026 and 2027
are not. The NOT_MOVED comment, the changelog and test_no_family_is_moved's
comment say so. No change to the model. £0 on the Enhanced FRS either way:
no member outside the couple reports ESA, JSA or Income Support there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2047's winter heating split (merged in 6f0148a) reads
gov.social_security_scotland.pawhp.active through the parameters the reform
class was built from. When the parameter switches the reform on, that is the
applying simulation's own tree (create_structural_reforms_from_parameters is
given self.tax_benefit_system.parameters), so nothing changes there. A
reform class built from other parameters and applied later (the PR's
clone, branch and reform= tests build it from a stand-in that only switches
it on) raised AttributeError: 'types.SimpleNamespace' object has no
attribute 'social_security_scotland' (a probe applying the class to a situation).
The split now reads the switch from the system of the simulation applying
the reform, which is also the tree a later parameter change would be in.
The rule itself (#2047's) is unchanged.

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

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine once main is merged in and the Scottish carer double count is fixed.

@MaxGhenis, this is a follow-up to my review at 7cd3c34df. I looked at what changed up to the head 5366cf0c3.

My earlier points are settled. The base conflict was resolved. Another member's Income Support is now left out on purpose, and the docstring, changelog and a test that checks both modes say so. The eFRS figures were re-run. The #2047 sequencing is written up in the body. The new pieces since then look right to me:

  • the per-year roles;
  • variable_parts neutralization (tax_benefit_system.py:169-181);
  • apply_reform naming the inputs a reform stores;
  • holding legacy_benefits_closed false under the reform (reforms/policyengine/disable_simulated_benefits.py:62-71).

That last hold is consistent with a reform that pays every reported legacy award in every year.

  1. must: the branch conflicts with main again. Against main 714b1438c, git merge-tree reports conflicts in five files:

    • reforms/policyengine/disable_simulated_benefits.py
    • simulation.py
    • utils/benefit_unit.py
    • claimant_or_partner_esa_income.py
    • claimant_or_partner_jsa_income.py

    #2047 and #2261 have both merged.

    • Main's reform now has #2047's winter heating split and int(simulation.dataset.time_period). Keep the split block after the generic loop. Take winter_fuel_allowance out of BENEFITS (it is still at line 30 here). Keep this branch's int(str(...)[:4]) and a single tax_benefit_system.simulation link.
    • #2261 ends an outside member's own ESA or JSA status once legacy benefits close. Please check that its reader and NOT_MOVED still agree after the merge. The new "no family is moved in any of the ten years" test should cover it if it still passes.
  2. should: a Scottish carer's Carer's Allowance is counted twice under the reform. carers_allowance is in BENEFITS (line 13), so it is set from the report in every country, while carer_support_payment stays simulated. I reproduced your 13 October note at this head. Two people aged 40, 40 care hours, carers_allowance_reported £4,000 in 2025, one in Scotland and one in London:

    Year carers_allowance [Scot, Lon] carer_support_payment [Scot, Lon]
    reform on 2025 [4,000, 4,000] [4,331.60, 0]
    reform on 2026 [4,000, 4,000] [4,495.40, 0]
    reform off 2025 [0, 4,331.60] [4,331.60, 0]

    This has been on main since #2047 (main's reform already runs on a numeric dataset year and lists carers_allowance). This PR rewrites the list, though, so please fix it here or file it as an issue. Your suggested split along the lines of the winter heating one, plus a conservation test, would do.

  3. should: overlaps with other open PRs.

    • #2193 adds its own Simulation.apply_reform to simulation.py (binding tax_benefit_system.simulation, then calling core). The two apply_reform methods will conflict. This PR's version also sets that link, so it covers what #2193's does.
    • #2382 adds war_widows_pension to BENEFITS, together with its _reported variable. Main's list and this PR's rewritten list will conflict on that line. Keep the entry when merging, so that test_every_listed_benefit_has_what_the_reform_reads_and_sets covers it.
  4. nit: apply_reform now relies on core internals. It uses _user_input_keys, _get_visible_branch_names and a module-level _reform_logs keyed by id(self), with a property wrapped round core's attribute (simulation.py:304-382). The tests cover it well. But a core release that renames or restructures _user_input_keys would break it silently, unless a test pins that the attribute still exists. A short follow-up issue to move "inputs a later reform sets survive a reset" into policyengine-core would make this lighter.

  5. nit: the PR body's test counts are from d10ef154c ("to be re-run on the head before merge"). The 5366cf0 commit (legacy closure hold) isn't listed under Reviews or Mutants yet.

Repo process: the .fixed.md fragment fits. The axiom: line is present, and the Reproducer: line was added by the Fix evidence check. CI at this head is green on every job.

What I checked

  • Read the body, the two newer comments, and the diff against the merge base 1d3abecef, with the reform, simulation.py and tax_benefit_system.py in full.
  • Ran the 6 changed pytest files at the head: 122 passed.
  • ruff check and ruff format --check on the 13 changed Python files: clean.
  • Ran the Scottish carer probe above at the head.
  • Checked the five conflicts with git merge-tree against main 714b1438c. I didn't attempt the merge.
  • I didn't re-run the YAML folders or the eFRS runs, and I didn't mutation-test the new legacy closure hold.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 14, 2026 07:40
…form

disable_simulated_benefits set carers_allowance from carers_allowance_reported
in every country and left carer_support_payment simulated, so from 2025, when
the model pays a carer in Scotland Carer Support Payment in place of Carer's
Allowance, a Scottish carer was paid the report and a simulated Carer Support
Payment (Vahid Ahmadi's review of 5366cf0, finding 2; #2047's note).

Like #2047's winter heating split, the report now becomes a Scottish carer's
carer_support_payment from CARER_SUPPORT_PAYMENT_FROM (2025, the year both
pre-overlap formulas switch on) and their carers_allowance before, and
everyone else's carers_allowance. The pre-overlap entitlements stay the
model's for both schemes.

New tests (test_disable_simulated_benefits_carers.py): the reported case,
conservation and placement in every year on both simulation classes and the
reform= route, a differential against the model's own scheme choice with the
reform off, household_benefits counting the report once, and a Hypothesis
property.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: COMPLETE
Review severity: REQUEST_CHANGES
Confirmed critical count: 1
Reviewed head: ab800e8bb340eea07299b79038361f874557c192

The new carer-report split has one blocking counterexample. The shared input-lifecycle and claimant/partner benefit paths otherwise passed the scoped checks.

C1 — Route the held carer report using the current year's country (OPEN)

Location: policyengine_uk/reforms/policyengine/disable_simulated_benefits.py:188 (dataset-year country read at168–170, reused in the year loop at191–194).

The new carer split calculates household country once at the dataset year, then uses that fixed geography for all ten years. Both country and region are YEAR-defined, and an explicit situation or multi-year dataset can put the carer in Scotland in a later year. The reform then places the held report under the wrong scheme and loses the Scottish Carer Supplement, despite its declared per-year scheme-placement contract.

Executed at this head: one adult aged40 in2025 and41 in2026,40 weekly care hours in both years, £4,000 carers_allowance_reported in2025, London in2025 and Scotland in2026; no earnings or overlapping-benefit reports. Interpret the later region as a settled ordinary-residence move within the UK, with the qualifying care and other conditions continuing. The reform is enabled before data loading. In2026, the actual country is SCOTLAND and the model's ordinary pre-overlap calculations already select CSP (£4,495.40) and CA (£0), but the reform sets:

2026 output Actual reform Current-year routing control
Carer's Allowance £4,000 £0
Carer Support Payment £0 £4,000
Scottish Carer Supplement £0 £608.40
Household benefits £4,000 £4,608.40
HBAI household net income £4,000 £4,608.40
Household net income £3,819.96 £4,428.35

The control changes only the held report's2026 placement (CA0/CSP4000), retaining the same £4,000 held total, original report and demographic/income inputs. It does not force eligibility. Both worlds have zero income tax; the supplement difference reaches final household income. The reform's new routing therefore loses the full £608.40 modeled annual supplement in this reachable case.

The source reviewer confirmed that SSI2023/302 reg6(1)(a) tests ordinary residence in Scotland on the relevant day, rather than in the survey year. Reg14A supplies the supplement for weeks CSP is payable, with the payable/overlap distinction in reg16. The settled UK-resident move and continuing care/no-overlap conditions establish the scenario premise; the code also demonstrates the model's own current-year scheme choice.

Read and project the carer's country inside the year loop, then retain the dataset-year reported amount under that year's scheme. Add a regression with year-varying region inputs through the supplement and final household income. This finding targets the newly added carer split; it does not extend this review into inherited winter-payment geography.

Validation

  • All 147 cases in the eight changed Python test modules passed. The combined local batch had 1,586 passes (1,340 YAML and 246 Python).
  • It also had 66 configured-capital-gains errors caused by the reviewer harness rejecting a constructor keyword, before the changed code ran. All had the same error in an unchanged constructor branch. One representative passed after correcting the harness; the other 65 were not rerun locally. This is not an all-pass local result.
  • The reviewed head's full hosted CI, including Test / Result, passed. Local validation used an existing Core 3.33.0 environment with Hypothesis 6.168.2; it does not reproduce locked Core 3.33.2 locally.
  • The carer case above reaches final household income. A separate four-household-year SCP control passed; 13 source originals were validated. Ruff and diff checks passed. Private Enhanced FRS results were not replayed.

The axiom: n/a exemption is defensible for this model reform and emulator award mapping; no missing-parity blocker is counted. This review covers the current head rather than the older head still named in the PR body.

MaxGhenis and others added 3 commits October 16, 2026 11:49
juaristi22's review at ab800e8 (C1): the carer split read the household's
country in the dataset year only and used it for all ten years. country is a
YEAR variable, and a situation or multi-year dataset can move a household into
Scotland in a later year; the reform then paid that carer's report as
Carer's Allowance and lost the Scottish Carer Supplement (2026: household
income £4,000 against £4,608.40 for a carer who stayed in Scotland).

Both splits now read country in each of the reform's years. The winter
heating split (from #2047) had the same dataset-year read and gets the same
fix. On a projected single-year dataset, such as the Enhanced FRS, the region
is the same in every year, so nothing changes there.

Tests: a carer and a pensioner who move into or out of Scotland, each year
compared with a household that stayed (through the supplement and household
income), on the scenario and reform= routes; Hypothesis properties with each
household's region drawn per year in a multi-year dataset.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: COMPLETE — re-review at d274f35. Approval; previous C1 resolved, no new findings.

The held carer amount now follows each year's country for CA/CSP placement. Both settled move directions preserve the £4,000 report, add/remove the £608.40 Scottish supplement as appropriate, and carry that difference through household benefits and final income. The related winter routing uses the same year-specific geography. Annual residence remains a model input proxy; this does not certify midweek residence history.

One bounded current-head batch passed all 47 checks: the two full affected modules, selected input/reset/role/neutralization and Income Support controls, plus four independent literal C1 controls covering both scenario/reform routes, request order and reset. Source and code checks were independent. Main e7610ac is integrated; unchanged lifecycle/scoped-award sections retain their earlier reviewed provenance. The trusted-base Fix evidence check passed.

Local Python 3.13.14/Core 3.32.27 differs from locked Core 3.33.2. This is a focused rerun, not a new full-suite/private-FRS result; the earlier larger batch's 66 external harness errors remain recorded. The existing model/emulator-mapping Axiom exemption remains qualified, with no encoded-parity claim.

At the fresh activity check, public feedback was unchanged and no current-head public review claim was present. Four hosted Python shards and required Test/Result remain pending. Coordinate overlaps with #2193/#2382 when either lands; this approval does not establish merge readiness.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: COMPLETE
Review severity: APPROVE
Reviewed head: 871e46de991b37841930e13bc3f08160f4cc70ca.

Incremental review of the merge of main b4dbaf57a: no new findings; C1 remains resolved. Fifteen of 16 branch files are unchanged from the previously approved head. All previous functions in the shared helper retain their ASTs; its added child helper matches main.

Independent policy and code review checked the affected integration. All 14 current-head controls passed, covering clone/branch/reset behavior, scoped Income Support awards, both reform routes and the original carer-country case through final income. Thirty-three unchanged carer/winter properties retain their earlier passing results: 47 distinct cases of retained coverage, with 14 freshly executed. All 14 cached source originals and 13 extracts validated.

The annual residence and qualifying-week limits, held-report semantics, and older private-data/harness qualifications remain. Local execution used Core 3.32.27 rather than locked 3.33.2; no private population or encoded-parity result is claimed. The previously supported emulator-mapping Axiom exemption remains applicable.

Fresh CI check: no current failures; pending Test / Python 1/4, Test / Python 2/4, Test / Python 3/4, Test / Python 4/4, Test / Result (absent). Landing coordination with #2193/#2382 remains; this approval does not establish merge readiness.

Review r11 (Opus and GPT-6.1 Sol) on 871e46d: main's benefit cap
readers (#2076, #2089) called award_of_members on esa_income and
jsa_income. Under disable_simulated_benefits the held total matches
neither of its readings after the dataset year, so another member's held
award was read as the couple's: it raised benefit_cap_welfare_benefits
and could exempt a claimant with an ESA support component who receives
no ESA.

- utils/benefit_unit.py: award_of_couple reads claimant_or_partner_*
  wherever the rule's couple is the claimant and partner, and
  award_of_members for any other couple (a UC couple with is_uc_claimant
  entered). CLAIMANT_OR_PARTNER_AWARDS moves here; the reform imports it.
- benefit_cap_welfare_benefits and both specified-benefit exemptions read
  award_of_couple.
- The reform's docstring says it reads its inputs when it is applied, so
  a region a scenario's after-load modifier or set_input changes later is
  not followed; applying the reform after the change reads it (r11 Sol
  finding 2, Opus nit 4). New test for that route.
- Changelog: the first bullet no longer claims what #2047 shipped (r11
  Opus finding 2); new bullet for the benefit cap.
- New test_disable_simulated_benefits_benefit_cap.py: metamorphic (another
  member's report never changes the cap, reform on), differential against
  main's replaced reader (reform off), the fallback couple, and an entered
  award reaching the cap. Each reader's mutant fails it.

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

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental re-review of 593259fd8d4c: COMPLETE, no new findings; C1 remains resolved.

The new per-benefit-unit scoped ESA/JSA routing preserves the different-couple fallback and the documented application-time lifecycle. Independent policy/source and code/test reviews agree. Accepted 119 distinct cases: all 114 repository cases passed initially; one reviewer total omitted unchanged Child Benefit, and that corrected diagnostic alone passed. The original failed diagnostic is retained separately. Fourteen cached source originals and 13 extracts passed integrity checks; prior policy qualifications remain.

Required hosted CI is still pending. Coordination with #2193/#2382 and integration before landing remain. The annual-residence, held-cash and private-data limitations are unchanged; this approval does not certify encoded parity or merge readiness.

Local validation used Core 3.32.27 rather than locked 3.33.2. Tests ran against captured main 1bdc7a0; main subsequently advanced to 3f8d4bd with release packaging/version-only changes, separately checked as outside this review scope. No tests on the new main combination are claimed.

Review r12 nits on 593259f (both reviewers; no code change requested):
- award_of_members' docstring said readers read award_of_couple; only the
  benefit cap does, the means tests, passports and Income Support gate read
  the claimant_or_partner_* variables directly.
- award_of_couple's fallback also covers an entered is_uc_assessed_claimant
  or more than two members entered as claimant or partner, and still
  misreads another member's held award under the reform; the docstring and
  changelog say so.
- The metamorphic invariant in the new test's docstring is for a couple
  that is the claimant and partner.
- variable_parts is built from CLAIMANT_OR_PARTNER_AWARDS rather than
  repeating the map (same value).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 17, 2026

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: COMPLETE
Review severity: APPROVE
Reviewed head: c2e069da969e782af04e814c309e2691f8c3bae4.

Incremental re-review: no new findings; C1 remains resolved. The shared award-to-part mapping expands to the same two neutralization targets, and the other executable ASTs are unchanged after docstrings. The fallback qualifications match the existing claimant and UC-couple selectors.

Eleven current-head checks passed, covering neutralization, directly supplied awards, final statuses and cap routing. Prior evidence remains valid for unchanged behavior: 119 retained cases with five overlaps give 125 distinct selected cases. All 14 cached official source originals and 13 extracts passed integrity checks.

Local execution used minimum-supported Core 3.32.27 rather than locked 3.33.2. Private FRS, population and encoded-parity claims were not rerun or certified. Current-head CI passed, including required Lint and Test / Result. Coordination with #2193/#2382 and integration with current main remain landing gates; this approval does not establish merge readiness.

Merged via the queue into main with commit 5ee7f94 Oct 17, 2026
17 of 18 checks passed
@MaxGhenis
MaxGhenis deleted the disable-simulated-benefits-scoped-awards branch October 17, 2026 16:58
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

For the legacy award reader switches (task_6c880fe4; launchd com.maxghenis.pe-uk-reader-switches-after-2027, switch is-gate): the Income Support gate switch and its differential test are on main in this PR's merge commit 5ee7f94 (merged 2026-10-17 16:58:50Z, head c2e069d).

  • policyengine_uk/variables/gov/dwp/income_support_eligible.py: the limbs read benunit("claimant_or_partner_esa_income", period) > 0 (line 163) and benunit("claimant_or_partner_jsa_income", period) > 0 (line 168).
  • Differential test: policyengine_uk/tests/test_income_support_gate_reads_claimant_or_partner_awards.py::test_the_switched_gate_matches_the_replaced_code. It keeps the replaced inline limbs as a reference and compares them on Hypothesis-drawn families. The same file covers awards entered in the situation, set after calculating, set on a branch and deleted.
  • This PR also switched the three benefit cap readers (benefit_cap_welfare_benefits, is_uc_benefit_cap_exempt_specified_benefit, is_housing_benefit_benefit_cap_exempt_specified_benefit) to award_of_couple in policyengine_uk/utils/benefit_unit.py, tested in test_disable_simulated_benefits_benefit_cap.py.

So is-gate needs only its marker. It does not need a switch.

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.

disable_simulated_benefits raises before setting any benefit

3 participants