Conversation
SSCBA 1992 s.124(1)(c) bars Income Support when the claimant is engaged in remunerative work or the other member of a couple is: paid work of 16 hours a week or more for the claimant (IS Regs 1987 reg 5(1)) and 24 for the partner (reg 5(1A)). A person within Sch 1B para 4, a carer, is not treated as engaged in it (reg 6(4)(c)). The partner's threshold applies to whoever is not the claimant, so the gate tests it for each candidate claimant. s.124(1)(f) bars it when the claimant is entitled to a jobseeker's allowance of either kind, or the partner (or the couple) to income-based JSA. The partner's contribution-based JSA does not bar it. Income-based JSA follows the income-related ESA pattern: the award on the claimant's and partner's reported amounts after the JSA capital test, through a new income_related_jsa_award helper that jsa_income also uses, or a jsa_income entered directly, taken to be theirs. - Parameters gov.dwp.income_support.eligibility.remunerative_work claimant_hours (24, 16 from 7 April 1992) and partner_hours (24, 16 from 7 April 1992, 24 from 7 October 1996), from legislation.gov.uk point-in-time texts of reg 5. - income_support_remunerative_work_hours: weekly_hours, enterable directly to leave out the employments reg 6(1) excludes (reg 5(6)). - 17 YAML cases hand-computed from the law; 6 fail on the previous gate. - The Hypothesis differential oracle now reads (c) and (f), and both properties draw hours, JSA and direct jsa_income. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s.124(1)(c) and (f) only bar a claim, so raising any claimant's or partner's hours or JSA cannot turn an ineligible family eligible. The new Hypothesis property builds each family twice, the second time with more paid work or JSA, and checks eligibility never rises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2013 (b1977d9) primes half the generated families at the edge of eligibility and draws a direct esa_income half the time. Resolve against this branch's JSA generators: a direct jsa_income is now also drawn half the time, and a primed claimant has no paid work or JSA, so it sits at the edge of eligibility under (c) and (f) as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r1 of #2025 (finding 1): the gate detected a direct jsa_income through Simulation.input_variables. That list is fixed at construction and ignores the period, so it missed an input set later with set_input or on a branch, and treated an input for one year as an input for every year. An excluded member's report then counted in a year with no direct input. entered_directly (policyengine_uk/utils/inputs.py) reads core's record of explicit inputs, (variable, branch, period) in _user_input_keys, for the branches the simulation can see, and requires the value still be stored. #2013 adopts the same helper for esa_income. Also from the review: - jsa_income returns zero before reading reports when income-based JSA is not active, as before the helper was introduced (finding 4); - income_support_remunerative_work_hours says weekly_hours is a usual week, not the reg 5(2)/(3B) average for fluctuating hours (finding 3); - YAML cases for a partner at 23 hours, inferred roles with an excluded grandparent, two caring award holders with contribution-based JSA, and a direct jsa_income for another year; pytest regressions for late, branch and other-year inputs (finding 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of #2025: entered_directly read core's _user_input_keys, but in policyengine-core 3.32.9 that record drifts from storage. - clone() copies the holders' stored arrays but shares the record, so an input set on a clone, or on a parent after a branch was made, was recorded against arrays it never reached. An excluded adult's report then barred Income Support on the original or the earlier branch. - delete_arrays() removes arrays but keeps their record, so a formula result calculated later for the same period looked like an input. The UK Simulation (and so Microsimulation) now gives each clone its own copy of the record and drops the entries for deleted arrays. Both changes only remove stale or foreign entries; they also correct what core's own _invalidate_all_caches and to_input_dataframe read from the record. entered_directly now asks about the stored value the simulation reads: the first visible branch (own, parents', default) that stores one, as the holder reads. It no longer falls back silently when core's record is missing. test_entered_directly_lifecycle.py covers both awards (jsa_income, esa_income) with an excluded adult reporting the award. Six of its sixteen cases fail without the Simulation changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r2 of #2025: reg 5(2) takes expected weekly hours or, for fluctuating hours, a complete recognisable cycle or the five weeks before the claim (or a more accurate period), and reg 5(7) counts paid meal breaks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions
Review r3 of #2025 (finding 7): a school-year cycle leaves out school holidays and other periods the person is not required to work; reg 5(2)(b)(ii)'s five weeks run to the claim or a superseding decision; paid refreshment time counts with meal breaks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2013 (97b7a1c) replaces entered_directly with a value rule for income-related ESA, deletes policyengine_uk/utils/inputs.py, restores policyengine_uk/simulation.py (no clone/delete_arrays overrides), and adds IS Sch 1B para 2 (a single claimant with a placed child). Review r3 of #2025 showed provenance via core's _user_input_keys is unsound in policyengine-core 3.32.9: core-built simulations skip the UK overrides, disk clones share files, default-named branches and ETERNITY diverge, input-free simulations crashed, and delete_arrays became O(keys). Income-based JSA now follows the same rule. jsa_income counts as the couple's award only when it differs from both the award on everyone's reported amounts after the capital test and their plain total (which disable_simulated_benefits sets); otherwise the claimant's and partner's reports decide. An entered award, or a reform that removes or replaces jsa_income, therefore applies, whatever the lifecycle that stored it. Conflicts: the gate's documentation and its ESA/JSA block, and the property test (both sides kept: #2013's shapes, placed children and para 2; this branch's work and JSA limbs; direct values {0, 4000} for both awards, with explained_by_reports taking the JSA capital rules). test_income_support_direct_inputs.py and test_entered_directly_lifecycle.py are rewritten to the value rule, including the raw reported total and a neutralised jsa_income. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions
The award on the reported amounts is whatever screen jsa_income applies (jsa_income_eligible), so the comment stays true when that screen gains conditions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions
… boundary Review r4 of #2025: - jsa_income is stored as float32, while the report sum and the helper's award are float64. With large reports (£65,536.01 + £65,536.00) the formula's own award looked entered and barred the claim. Both references are now cast to the stored dtype before comparison. - Under the value convention, another member's report can change how a directly entered award is read. The variable documentation and the property module now state that boundary and the half-penny tolerance instead of claiming invariance for every input. test_income_support_direct_inputs.py pins the intended transitions: an excluded adult's report equal to a direct award, and a claimant's report that explains it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gates' into is-remunerative-work-jsa-conditions # Conflicts: # policyengine_uk/tests/test_income_support_eligibility_properties.py # policyengine_uk/variables/gov/dwp/income_support_eligible.py
…d direct-award modes Review r5 of #2025 (APPROVE WITH NITS): - The documentation and property module describe the convention for a stored esa_income or jsa_income however it was supplied (entered or replaced by a reform), compared within half a penny after rounding to the stored precision. - test_the_half_penny_tolerance_endpoints pins £100.004 (explained) and £100.006 (the couple's) against a direct £100. - test_direct_awards_match_the_reference_deterministically checks both direct-award modes against the oracle for a fixed carer; it kills a mutant that ignores direct ESA, which the 25-example differential let survive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2013 (0953c8a) limits Sch 1B para 2 to placed children under 16 and guards the ESA bar so a stored zero never bars the claim. The documentation conflict is resolved with wording covering both awards. JSA gets the same guard: a stored jsa_income of zero within half a penny of a sub-penny award on the reports no longer bars the claim. test_a_stored_zero_never_bars_the_claim pins it for an entered zero and a removed (neutralised) award; it fails on the previous gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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
This was referenced Oct 2, 2026
…rtner' into disable-simulated-benefits-scoped-awards
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the
disable_simulated_benefitsreform (gov.contrib.policyengine.disable_simulated_benefits), which raised an error before setting any benefit, and gives the claimant's and partner's income-related ESA and income-based JSA one reading, shared by theclaimant_or_partner_*variables and both income-related limbs ofincome_support_eligible.Stacked on #2027, with #2025 merged in (both are stacked on #2013). The diff against #2027 therefore also shows #2025's changes. This PR's own change is commit 1f68925; the merge commits only bring in #2025 and #2027's later head.
What was wrong
The reform raised for three separate reasons, each hiding the next:
Simulation.__init__does not call core's constructor, sotax_benefit_system.simulationwas never set and the reform'sself.simulationraisedAttributeError. The public service budget reform (adjust_budgets) reads the same attribute.range(time_period, time_period + 10)raisedTypeErroron text.<name>_reportedvariable: Attendance Allowance, both DLA components and both PIP components. Since Move disability benefit reported amount mapping to UK data #1656 the data gives their receipt as a rate category and the model pays that category's rate, so there is no reported amount to copy.Once it runs, a second defect appears in later years. The reform holds each benefit at its dataset-year amount for ten years, while the
*_reportedvariables are uprated.claimant_or_partner_esa_income,claimant_or_partner_jsa_income(#2027) and the Income Support limbs (#2013, #2025) decide whose awardesa_income/jsa_incomeholds 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, was taken to be the claimant's or partner's.What changes
tax_benefit_system.simulationbefore applying structural reforms, as core's constructor does.claimant_or_partner_esa_incomeandclaimant_or_partner_jsa_incomefor each of the ten years, from the claimant's and partner's reports in the dataset year (is_claimant_or_partner);income_supportfrom the claimant's and partner's reports alone. In the modelincome_supportis the claimant's family's award, and the passports read it as theirs, so another member's report would otherwise passport the claimant.claimant_or_partner_awardinpolicyengine_uk/utils/benefit_unit.pyis the formula of bothclaimant_or_partner_*variables.claimant_or_partner_esa_income > 0and the income-based JSA limb (s.124(1)(f)) isclaimant_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.Why the limbs read the variables and do not call the helper
A helper recomputes the claimant's and partner's award from
esa_incomeand the reports. It cannot see a value the reform set onclaimant_or_partner_esa_income, so limbs that called it would keep the later-year defect. Reading the variable gives the same result as the old limb convention(award > 0) & (~as_reported | scoped > 0): the variable is zero when the award is, the scoped award when the reports explain it, and the award itself otherwise. With the limbs reading the variables, nothing needs the as-reported masks, so the helper returns the award alone.Behaviour changes to note
esa_income/jsa_incomeequals 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.esa_income/jsa_incomeafter calculating the gate has to clearclaimant_or_partner_*as well asincome_support_eligible. The three test helpers that recalculate the gate do so.Invariants
For every benefit unit and each of the reform's ten years:
esa_incomeandjsa_incomeequal the sum of every member's dataset-year report, andclaimant_or_partner_*equals the sum of the claimant's and partner's (conservation);0 <= claimant_or_partner_* <= esa_income / jsa_income(bounds), in the reform and in the formula (to the rule's half penny);income_support_eligibleis false whenever either claimant-or-partner award is positive (the limbs only bar);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 (copied from #2025 at 081ec14), except in the plain-total case above, which is checked against its own expectation.
Overlap with #2047
#2047 (PAWHP in household income, on #2038) carries the same simulation link and AA/DLA/PIP removal so its own stack works. Whichever lands second keeps both PRs' rules: this PR's claimant-or-partner scoping, and #2047's winter-heating split. Under that split,
winter_fuel_allowanceis not in BENEFITS, and the one dataset-year Winter Fuel Payment report is set aspawhpin Scotland while PAWHP is active and aswinter_fuel_allowanceotherwise. Without it, Scottish households would be counted twice.Enhanced FRS
Real runs on a private copy of the Enhanced FRS 2024-25, 2025-2030. Base
2cb91b036(#2027 at f551e54 with #2025 at 081ec14 merged, without this PR) against this PR's head9e88021a4. Aggregates only.income_support_eligible, both claimant-or-partner awards, every passport and household net income. No benefit unit holds a plain-totalesa_incomeorjsa_income, and no member other than the claimant or partner reports ESA, JSA or Income Support (74 benefit units have such a member).AttributeError: 'CountryTaxBenefitSystem' object has no attribute 'simulation'; on this branch it runs. In each year 2025-2030: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. Whether it should use each year's reported amounts instead is a separate question (follow-up task).
Tests
test_disable_simulated_benefits.py(30 with the file below):is_on_*status in each year, and shows that a value-only reading would take the excluded adult's ESA as the couple's in later years;test_income_support_gate_reads_claimant_or_partner_awards.py, the differential test against the replaced code:tests/policy/baseline/finance/benefit/familyand.../gov/local_authorities/council_tax_reduction, 509 passed.Simulation(no link);attendance_allowanceback in the list.axiom: n/a: model reform
🤖 Generated with Claude Code