Repository navigation
Conversation
…lags Unflagged inputs (calculators, policyengine.py, the APIs) kept the under-20 limit because survey childless couples carry no flags. Once the dataset supplies is_claimant_or_partner from the FRS adult table (policyengine-uk-data), the presumption only applies to inputs without roles, so it can follow the FRS base rate: among co-resident HBAI adults (younger 20+), pairs 16-19 years apart are mostly couples (61%) and pairs 20+ apart mostly not (12% at 20-24, under 1% at 25+). - New parameter presumed_child.minimum_age_gap_at_any_age (20): a member at least that much younger is presumed the claimant's child at any age. A member flagged as a parent can still be the partner. - YAML: 50 + 25 unflagged is now single; the 20-year boundary (19 vs 20, and at 90/70); a flagged partner 25 years younger stays the partner; an unflagged 60/38 couple with a child is a lone parent (documented trade-off) unless roles are supplied. The under-20 boundary case moves to an 18-year gap so it still tests the age limit. - Properties: the reference rule follows; the FRS-shape differential now says the change touches only childless couples 20+ years apart (intended), and supplied roles are always kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…imant-partner-unflagged-gap The any-age 20-year gap is OR'ed onto #2040's presumption. It changes two of #2040's documented limitations where the adult is 20+ years younger (a flagged lone parent with an adult child and a younger child; a flagged grandparent with a daughter and grandchild): both are now read correctly. A 16-19-year case keeps the documented limitation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exhaustive over two unflagged adults aged 20-90 (head the elder): the younger is the partner exactly when under 20 years younger. The Hypothesis properties rarely draw an exact 20-year adult pair, so threshold mutants (21, 19, strict >) were caught only by one YAML boundary case; this test kills all three on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riants - The 70/45 Lifetime ISA and 79/54 Pension Credit couples supply is_claimant_or_partner and assert it, keeping their capital and eligibility expectations (the new presumption would otherwise split them). - The valid-family property now checks membership against the reference rule exactly and states the one intended exception (an unflagged couple whose head is 20+ years older); module docstring updated. - Deterministic test: a 70/45 couple with each head order, inferred and with supplied roles. YAML: a younger head 20 years below the other adult keeps the couple. - Docstring and differential comment: the split applies when the elder is the claimant. - Reference HBAI helper: an identified parent must be 16 or over, as in is_hbai_dependent_child; the under-16-parent regression now also checks the reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI on d8fc744: two property tests build couples with random ages and no roles, so the 20-year presumption split the wide-gap ones (30/67, 78/49, 93/35) and their independent expectations failed. Their generators now supply is_claimant_or_partner (true for the adults, false for children in the UC test), since the units are couples by construction. The strict xfail for #1942 keeps failing only for its own reason. Review r2: the changelog, test comments and YAML header no longer claim survey datasets supply the role. Only datasets that carry it (the enhanced FRS once policyengine-uk-data#524 ships) are unaffected; the bundle's populace_uk_2023 and 2023-24 FRS overlays carry no role. The FRS-shape differential exempts only units whose head is 20+ years older, and the head-order test checks every person. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to claimant-partner-unflagged-gap
#2040's docstring example (a flagged 50 with a 25-year-old becomes a couple when a 12-year-old is added) does not hold with the any-age gap: a member 20+ years younger stays the claimant's child either way. The example now uses a 16-19-year gap (38 and 20), checked against the model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merging main brought ten YAML cases (benefit cap for pension-age claimants, CTR pensioner status, the HB earnings disregard) that enter a mixed-age couple aged 70 and 50 without roles. Exactly 20 years apart, the younger partner is presumed the claimant's child, so each case now supplies is_claimant_or_partner for the two partners. Expectations are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full YAML suite on the result: 1,935 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_mixed_age_couple_properties and the P6 test in test_benefit_cap_and_ctr_pension_age_properties build couples with random ages (for example 67 and 25) and no roles, so the 20-year presumption split them. Both now supply is_claimant_or_partner for the two partners. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lagged-gap One conflict: main also supplies claimant/partner roles in the UC State Pension property generator; kept main's code and widened its comment to mention partners 20 or more years younger. Full YAML suite: 1,985 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full YAML suite on the result: 2,006 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's new test_council_tax_reduction_pensioner_earnings_disregard_properties builds two-adult families with independently drawn ages and no roles; the 20-year presumption splits the wide-gap ones. The adults now supply is_claimant_or_partner (children false). 7 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR CI on 73a7b90: test_scottish_carer_supplement_is_not_means_tested_income drew a pension-age couple 25 years apart (67 and 92) with no roles, so the 20-year presumption split it. Every member of these units is an adult of the couple, so each now supplies is_claimant_or_partner. 3 passed; it was the only failure among 2,693 Python tests and 2,006 YAML cases in that run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Thanks Max. The rule itself is right and tightly scoped. At 12962b0 I checked it against the docstring and the YAML: gap >= 20 ORed onto #2040's rule (is_claimant_or_partner.py:99-108), with a flagged younger member still taking the partner slot through other_parent. Ages 50/31 stay a couple, 50/30 and 90/70 split, and the boundary uses >=. At the head, the six changed YAML files pass (32 + 24 + 13 + 11 + 8 + 30 cases), and so do the seven changed property files (36 passed, 1 xfail), each run in its own process. WRA 2012 s.39(1) and SSCBA 1992 s.137(1) define a couple by marriage, civil partnership or living together "as if they were a married couple or civil partners", with no age condition, so this is purely a presumption for role-less input. The parameter files say that, and they don't cite statute.
I compared calculator-style situations for 2026 (ages only, one benefit unit and one household) on main b5b0923 and on this branch with main merged in. In every case below, results are identical to the penny once the adults are flagged is_parent, roles are supplied, or the younger adult is the head. With ages only, they move as follows:
| Ages (no roles) | main | this PR |
|---|---|---|
| 50, 30 | couple, UC £8,003.64 | single, UC £5,098.80; net income −£2,904.85 |
| 50, 30, younger earns £20k | couple, UC £0 | single, UC £5,098.80; net income +£4,846.80 |
| 60, 38, child 10 | couple, UC £12,226.20 | lone parent, UC £9,321.36 |
| 45, 24, child 2 | couple, UC £11,650.92 | lone parent, UC £8,746.08; net income −£2,904.84 |
| 70, 48 | mixed-age couple on UC £8,003.64 | single pensioner on Pension Credit £12,376.00; net income +£4,372.36 |
| 67, 45, rent £9,600 | UC £17,003.64 | PC £12,376.00 + HB £9,600.00; net income +£4,972.36 |
| 67, 45, rent £9,600, younger earns £15k | UC £8,989.26 | PC £12,376.00 + HB £9,600.00; net income +£12,734.74 |
-
Blocking: main has added role-less mixed-age couples that this rule now splits. I merged origin/main (b5b0923) into the head locally. The full YAML suite then gives 2,444 passed and 4 failed. Each failure is a fixture added on main after your last merge (325d585) that enters a 20+ year-gap couple without roles:
finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml:240(70/40)finance/benefit/family/housing_benefit/pension_age_premiums.yaml:281(50/70)finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml:62(70/45;uc_earned_incomecomes out 0 against an expected £12,879.60)gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml:544(Case 23, 50/70;pension_creditcomes out £12,376 against an expected 0)
Alsotests/test_council_tax_reduction_carer_income_properties.py:155-165, a generator added on main that draws working-age couples aged 25 to 60 without roles. Hypothesis fails it on a 25/57 pair (full pytest on the merged tree: 2,725 passed, 1 failed).
All five pass on main. Fix: merge main and supplyis_claimant_or_partner: truefor both partners in these four YAML cases and in that generator, as you did for the others. That merge also clears the current red Test job: the failure iscode_health/test_cached_arrays_not_written_in_place.py(core 3.32.16'sput(..., derived=...)), which main fixed at 4ac62cb and which passes after the merge (35 passed). The only textual conflict is a comment intests/test_uc_state_pension_properties.py(about line 114 after the merge). Resolve just that hunk and don't take--oursfor the whole file: main's #1947 rework of invariants 2 and 4 auto-merges into the same file, and taking your side whole makestest_tax_on_state_pension_does_not_reduce_partners_earned_incomefail. With the hunk-only resolution, the file passes (5 passed, 1 xfail).
-
Blocking (merge gate): don't merge until policyengine-app-v2#1240 is merged and deployed. Today the app sends "you" and a declared partner with ages only, and #1240 is still a draft. A merge to main publishes to PyPI by itself, so as soon as the API picks the release up, a real mixed-age couple entered in the calculator (67 and 45) moves from UC to Pension Credit plus full HB, and the partner's earnings drop out of the means test (+£12,735 above). The body already says #1240 "should land before or with this PR"; I'd make that a hard gate in the PR description and hold the merge until #1240 is live. Households saved earlier with ages only also need re-saving (or a backfill) before they pick up roles.
-
Should fix: the split reading leaves the "adult child" uncharged. Once the 45-year-old is presumed the claimant's child, they are an adult in the claimant's benefit unit but not a claimant or partner, and main charges no non-dependant deduction for that. For the 67/45/£15k case,
housing_benefit_non_dep_deductionsis £0 and HB is the full £9,600. The same people in separate benefit units get a £3,346.20 deduction and HB of £6,253.80. So the PR's intended reading (parent plus adult child) is costed more generously than the law would cost it, until in-unit non-dependant deductions land (#2017, and #2077 for local CTR). Either sequence this after #2017, or add a sentence to the docstring (is_claimant_or_partner.py:39-46) and the changelog saying the presumed adult child is not yet charged as a non-dependant. -
Nit: pin the young-child case.
45, 24, child 2with ages only now reads as a lone parent with an adult child, and UC drops by £2,904.84. #2040's young-child exception only fires when the claimant is flagged, so it doesn't help unflagged input. The body's evidence is for adult pairs without conditioning on a young child in the unit, so I'm not claiming this is wrong. But it's a common calculator shape, so it deserves a YAML case inbenefit_unit_structure.yaml(next to the 60/38/10 trade-off case at line 502) so the behaviour is explicit. -
Nit: overlaps. Once main is in,
test_uc_state_pension_properties.pyis also touched by #2081, #1963, #1999 and #2131, andtest_child_and_adult_definitions_properties.pyby #2030 and #2016. Whichever lands second should keep the supplied roles in the generators.
The PR body is clean on disclosure: survey-household counts suppressed under 10 and weighted totals only, with no record ids, weights or record-level amounts.
The review of 94041e9 found that holding every family's own awards still let property 4 fail through the same channel, and hid cross-family reads of awards: - In Newham, when the head's family stops claiming Universal Credit, the non-dependant moves into the sharer's claim and raises its uc_maximum_amount, which the four _legacy councils use as a UC family's applicable amount. The sharer's council award moved from 241.67 to 417.02 with its awards held. - Holding every family's awards also gives a perturbed 85-year-old head the award of the 47-year-old it replaced, and a mutant that read another family's UC survived. Property 4 now holds, with each family's CTR means test, the two household allocations through which other families reach a family's own awards: uc_non_dependants_counted (UC Regs 2013 Sch 4 para 9(2)) and has_non_dependant_for_severe_disability_premium. Each family's awards are computed. The body passes on CI's population, its minimal household and the reviewer's counterexample, and catches all five mutants the reviewer and author built. The generator also sets is_claimant_or_partner on every family's adults, so wide-gap couples stay couples once #2082 presumes a member 20 or more years younger to be a child. build() keeps each held value's own type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and exemption (#2015) * Assess each Council Tax Reduction claim under its own family's scheme and exemption In a shared-rent household every family liable for the rent claims, but the pensioner scheme and the non-dependant exemption were picked once per household from the head's family. SI 2012/2885 reg 3 (and SI 2012/2886 Sch para 3, SI 2013/3029 reg 3, SSI 2021/249 reg 3, SSI 2012/319 reg 12) define "pensioner" per person and partner, and the exemption applies where "the applicant or his partner" is blind or gets a listed benefit (SI 2012/2885 Sch 1 para 8(6) and equivalents). - council_tax_reduction_pensioner (BenUnit) replaces the household flag; the name and entity match #1944 and #1966, which extend the same definition. - council_tax_reduction_applicant_has_non_dep_exemption (BenUnit) replaces the household exemption; the councils' schemes apply it per claim. - council_tax_reduction_scheme_supported is per family, so each family falls back to its reported reduction only where its own scheme is unmodelled. - Councils' person-level non-dependant deductions no longer depend on the head's family; each claimant's own scheme decides whether to use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Reconcile reported and simulated reductions within a household With the scheme chosen per family, one household can hold a simulated claim and a family whose scheme is not simulated. Review r1 found that a family that cannot claim then kept its reported reduction beside a head whose simulated award already covered the whole bill: £3,000 against £1,800. - A family that cannot claim keeps a reported reduction only where no claim in its household is simulated (new household flag council_tax_reduction_household_has_simulated_claim). Without sharers this is the base behaviour. - A jointly liable claimant's reported reduction is limited to its own share of the council tax (SI 2012/2885 Sch 1 para 7(3)-(4)), the most any scheme could give it. Tests: three YAML cases; the property tests now generate reported reductions, check the fallback and the household bound, and toggle one claimant's exempting benefit to show it leaves the other claim unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Set a claim's scheme and exemption from the applicant and their partner #2009 now names the liable people directly. A household head can be liable without being the claimant or partner of their family (a grandparent heading a family formed around a young couple), so "the applicant or his partner" (SI 2012/2885 reg 3, Sch 1 para 8(6)) is not always the family's claimant and partner. New person-level council_tax_reduction_applicant_or_partner: the liable member, and the other member of the couple where the liable member is the claimant or partner, whatever the partner's age; in a family that cannot claim, its claimant and partner. The pensioner flag and the applicant exemption read it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use #2078's CTR applicant-or-partner variable #2078 (also stacked on #2009) adds is_council_tax_reduction_applicant_or_partner and council_tax_reduction_head_applies_alone for the CTR means test. They define the same people as this PR's council_tax_reduction_applicant_or_partner: the claimant and partner where the liable person is one of them, the liable head alone otherwise, and the claimant and partner in a family that cannot claim. Take #2078's two files byte for byte, so whichever PR lands second merges without conflict, and point the pensioner flag and the applicant exemption at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Cap a reported reduction at the claimant's share only beside a simulated claim Review r2 found that capping every jointly liable claimant's reported reduction at its share could discard a lawful report: the model's divisor (#2006's joint-liability share) counts students that SI 2012/2885 Sch 1 para 7(5) leaves out. The cap exists to stop a reported claim and a simulated claim together exceeding the bill, so it now applies only where another claim in the household is simulated, on the share that claim uses. Where nothing is simulated, reports are kept as reported, as on the base. Tests: - YAML: a jointly liable claimant keeps its whole report where nothing is simulated; a pension-age grandmother in working-age applicants' family does not make their claim a pensioner's (an intended change under reg 3(1): the base paid £1,800 on the pensioner scheme). - Properties: the fallback oracle follows the narrowed cap, and property 4 compares council_tax_benefit wherever the household's reconciliation is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Apply the reported-reduction cap only beside a simulated claim that pays On the payer build, the only remaining change came from heads whose reported reduction was capped beside a sharer whose simulated claim paid nothing. With no simulated reduction competing for the bill there is nothing to reconcile, so the cap now applies only where the simulated claims in the household pay something. The household bound still holds: each simulated award is within its share, and a capped report within its own. Tests: a YAML case where the simulated claim beside a report pays nothing; the fallback oracle, the bound and property 4's comparison follow the paid condition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test the per-family pensioner flag against #1944's reg 3 conditions council_tax_reduction_pensioner is now #1944's SI 2012/2885 reg 3 test on this PR's members: the qualifying age for State Pension Credit, and no Income Support, income-based JSA, income-related ESA or Universal Credit award. Property 1's oracle computes that directly. A family's own Universal Credit award depends on the rent it pays, which changes when it lives alone, so property 2 compares the families whose own awards are the same either way (and checks there are some). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Address the after-merge review: student share, head's own awards, P5 - council_tax_benefit: the share leaves out only people in higher education, so where a jointly liable person it counts is in other education (who may be a student SI 2012/2885 Sch 1 para 7(5) leaves out), the share may be too small; a report is then kept rather than capped. - council_tax_reduction_pensioner: where the household head applies alone, the benefit unit's Income Support, income-based JSA, income-related ESA and Universal Credit are its claimant and partner's, not the applicant's (as #2078 does for council_tax_reduction_relevant_income_based_benefit). - Two YAML regressions: a report beside a paying simulated claim where a further-education student is counted in the share, and a grandmother head who stays a pensioner when the young parents get Income Support. - Property test: the alone simulation is given each family's own awards instead of filtering; some generated adults are further-education students, and properties 5 and 6 follow the new rule. - test_benefit_cap_and_ctr_pension_age_properties P5 compares against a reform of the cap exception alone, so the CTR pensioner test (which also sets the applicable amount) is held fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hold the means test fixed in property 4 CI found that swapping the other claiming families' ages and disability benefits can move a family's simulated award through its applicable amount: the model's severe disability premium counts the other adults in the household (has_non_dependant_for_severe_disability_premium), joint occupiers included. That is main's means test, not the scheme or the exemption, so the perturbed run is given each family's applicable amount and income from the unperturbed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Read the student test and in_FE for the cap's student guard The re-review of 38364ca found that the guard missed in_FE, and that current_education's age default (tertiary at 18 and 19) turns it on with no education input. The guard now uses the model's student test for non-dependants (_legacy.is_full_time_student_non_dep) or in_FE, for a jointly liable person the share counts. The age default is kept deliberately: in the enhanced FRS current_education is an observed input, and the same test already treats an 18- or 19-year-old non-dependant as a student. The documentation says so, including that part-time students and students para 75(2) brings back in are treated the same way. Regressions: the student shown by in_FE (fails on 38364ca), and a jointly liable sharer with no education input at 19 (report kept) and 20 (capped). The property generator makes students by current_education or by in_FE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hold each family's own awards fixed in property 4 CI on d988725 (run 37383946508) failed property 4 on a Newham household: a working-age head (47) claiming Universal Credit, a sharer couple aged 100 and 63 with earnings, and a non-dependant. Perturbing the head to 85 moves the non-dependant into the sharer's claim (uc_non_dependants_counted: the household's non-dependants count in one claim only, UC Regs 2013 Sch 4 para 9(2), the head family's where it claims). The sharer's size criteria rise from one bedroom to two, its LHA cap from 9,000 to 10,860 and its award from 0 to 1,288.29, and reg 3(1)(a)(ii) then makes it not a pensioner. That is the family's own award, which the pensioner test is meant to follow, so the change is intended. Property 4 now holds each family's own UC, IS, JSA and ESA awards fixed, as property 2 already does, along with its means test. The same population fails on a9466b8, so this predates the merge of main; CI on a9466b8 did not generate it. With the awards held, a household-level pensioner flag still fails property 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hold the household allocations, not the awards, in property 4 The review of 94041e9 found that holding every family's own awards still let property 4 fail through the same channel, and hid cross-family reads of awards: - In Newham, when the head's family stops claiming Universal Credit, the non-dependant moves into the sharer's claim and raises its uc_maximum_amount, which the four _legacy councils use as a UC family's applicable amount. The sharer's council award moved from 241.67 to 417.02 with its awards held. - Holding every family's awards also gives a perturbed 85-year-old head the award of the 47-year-old it replaced, and a mutant that read another family's UC survived. Property 4 now holds, with each family's CTR means test, the two household allocations through which other families reach a family's own awards: uc_non_dependants_counted (UC Regs 2013 Sch 4 para 9(2)) and has_non_dependant_for_severe_disability_premium. Each family's awards are computed. The body passes on CI's population, its minimal household and the reviewer's counterexample, and catches all five mutants the reviewer and author built. The generator also sets is_claimant_or_partner on every family's adults, so wide-gap couples stay couples once #2082 presumes a member 20 or more years younger to be a child. build() keeps each held value's own type. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Hold the CTR capital in property 4 and pin its counterexamples Pension Credit's guarantee credit sets a family's CTR capital to nil, and the model lets other families reach that guarantee credit twice: the severe disability addition's residence condition counts every other adult in the household, and pension_credit_assessable_capital divides the household's savings among its pension-age adults. Property 4 held the CTR applicable amount and income but not the capital, so a head reaching pension age, or gaining Attendance Allowance, could move a pension-age sharer's award from nil to its £900 share of an £1,800 bill (review r5, finding 1). Hold council_tax_reduction_assessable_capital with the rest of the means test, name the two Pension Credit routes in the docstring, and pin as Hypothesis examples the four populations that broke earlier versions of the property: CI's Universal Credit non-dependant move, the uc_maximum_amount move, and the two Pension Credit routes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
One conflict (UC State Pension property comment): kept main's code, which already supplies the roles, and widened the comment to the 20-year gap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main added more tests that build a couple 20 or more years apart without is_claimant_or_partner, which the any-age presumption reads as a parent and child. A role audit (the formula logging every case where this rule and the rule without the any-age gap disagree) over the YAML suite and the non-microsimulation pytest suite found them. - YAML: 26 cases in 13 files (mixed-age HB, UC, WTC, Pension Credit, benefit cap and CTR couples) give both partners the role and assert relation_type COUPLE. - Hypothesis helpers: the benefit cap and CTR pension-age families, the HB income-related passport families, the HB LHA order end-to-end households (with a 90/68 example), the HB earnings disregard couples (with a 60/40 example) and the severe disability premium families (with a 60/40 example) supply the roles: the drawn adults true, children and young persons false, and a non-dependant in their own benefit unit true (from 16 in the SDP test, as its carer reference reads them). The benefit cap test also asserts every drawn couple stays a couple. Expectations and oracles are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles. Checks failing on both base and head (1)
Counted-once checkEach of 87 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
CI on 37f9d59 failed 15 property tests in eight files whose generators draw couples 20 or more years apart without is_claimant_or_partner, which the any-age presumption reads as a parent and child. The role audit also found DfE extended childcare, whose oracle reads both drawn adults as the couple, passing only on its derandomized draws. - Roles supplied (drawn adults true, children and dependants false): Pension Credit earnings disregard and war pensions, CTR carer income, HB pension-age cohorts, UC claimant income, UC company owners (all four builders), the reg 89(1)(a) qualifying-age couples, and DfE extended childcare. - The legacy carer premium test checks the model's inferred roles against its generator, so it keeps inference: an older partner is now drawn under 20 years above the head, with a 40/59 boundary example. Expectations and oracles are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mixed-age couple in is_uc_benefit_cap_exempt_earnings.yaml (76 and 40, two children, no parent flags) is a couple by construction; the any-age presumption would read the 40-year-old as the claimant's child. Both partners now supply is_claimant_or_partner and the case asserts relation_type COUPLE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r6 found three property tests that fail on valid draws of couples 20 or more years apart (passing CI only on their derandomized examples), and others whose pinned examples or shapes no longer reach their stated path. - Roles supplied, with wide-gap examples: Pension Credit prescribed benefits (90/67) and HB savings-credit-only (80/100, 100/80). - Roles supplied: UC minimum income floor (its 66/40 example), trading losses (the pinned 57/31 family), the HB UC passport, PAWHP household income, pension-age SDP and SDP AFCS. - CTR claimant invariant 6 holds every claimant-or-partner role at the model's reading of the drawn ages before raising the other adults, so the age presumption cannot move them. - UC claimant income and means-test income check the model's own reading of the claimants, so they keep inference: their generators now draw couples under 20 years apart, in place of supplied roles. - Comments corrected where they described the old presumption (CTR claimant family scheme, CTR applicant, benefit cap P1-P5); changelog notes that uk-data has supplied the roles since 1.58.0. Expectations and oracles are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's new Pension Credit statutory pay and targeted childcare guarantee credit cases, and the UC claimant elements properties, hold couples 20 or more years apart without flags. Under this PR's age presumption the younger reads as the elder's child, so each case now sets is_claimant_or_partner: true for both adults, as the PR's own tests do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_pension_credit_statutory_pay_properties.py draws its adults independently, from 67 to 95 and from 40 for invariant 2's partners, with no roles. Under this PR's age presumption a partner 20 or more years younger reads as the elder's child, so invariant 2 held trivially for that person (0 = 0) instead of testing the couple. Every person the builder makes is the claimant or the partner, so situation() now says so, as the UC claimant elements properties do. Found by the delta review at 22d449b (finding 3a). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #2039. Follow-up to #2040 (merged 3 October), kept separate because it changes calculator results for unflagged inputs, which is a methodology call (Max ruled d842: adopt).
What changes
is_claimant_or_partnergains one more presumption for households entered without relationships. A member at least 20 years younger than the claimant is presumed to be their child at any age, flagged or not (new parameterhousehold.demographic.benefit_unit.presumed_child.minimum_age_gap_at_any_age= 20). It is OR'ed onto #2040's rule (now on main), which is unchanged: 16+ years younger and either under 20, or below a flagged claimant whose flag no young child explains.A member flagged
is_parentstill takes priority as the partner, so a flagged couple 25 years apart stays a couple. With flags, it also fixes two cases #2040 documents as limitations, provided the adult is 20+ years younger:At 16-19 years apart the limitation stays (38/20/12 is still read as a couple, YAML case).
Why 20 years
Once a dataset supplies
is_claimant_or_partner(PolicyEngine/policyengine-uk-data#524 does, from the FRS adult table), the presumption no longer touches the microsimulation. It only applies to inputs without roles, so it can follow how often a large age gap means a couple. Measured on the enhanced FRS 2024-25 with the survey's own roles (couple = both in one benefit unit as claimant and partner), co-resident HBAI adults with the younger aged 20+:Shares are of weighted pairs (each pair takes its household's weight). #2040's evidence cited 61% / 12% / 0.6% / 0.2% from the released eFRS snapshot using benefit-unit membership alone; the 20-year boundary is the same either way.
Below 20 years apart, most such pairs are couples; from 20, very few are. The table sets couple pairs against every other pair, so it does not show which other relationship those are. Reading the younger adult as the claimant's child is the modelling presumption adopted here. The table describes co-resident adults in the population, not calculator submissions.
Trade-off and who it reaches
An unflagged couple 20+ years apart entered without roles, whose elder is the claimant (the head, or the eldest adult when no adult head is given), is now read as a single claimant plus a non-member adult. A younger explicit head keeps the couple: the gap is measured below the claimant.
is_claimant_or_partnerfor "you" and a declared partner, once the API runs a policyengine-uk release containing Replace generic child and adult flags with each programme's legal definitions #1896. With it, a declared partner keeps the couple whatever the gap. Without it, a declared partner 20+ years younger would be read as a non-member adult.uk.calculate_household, policyengine-api, household-api: pass households through. Callers that send ages only get the presumption. Supplyingis_claimant_or_partner(or flagging both partnersis_parent) keeps the couple. The docstring says so. A suppliedis_marrieddoes not:relation_typethen reads single whileis_marriedstays true.Impact (real microsimulation runs)
Aggregate impact CI report (certified data, which carries the role since policyengine-uk-data#524): comment, last updated for 916ea84 (run)
(The one check failing on both sides is
sign:vat, negative VAT for 76 units, unrelated to this PR.) The runs below remove the role from the data to show what the presumption does where a dataset has none.Seeded build of policyengine-uk-data#524's branch (enhanced FRS 2024-25 with
is_claimant_or_partnerfrom the adult table), simulated for 2025 and 2026. Latest run: main at 493f0d2 (which includes #2040) against this branch at 34c83a9. Two earlier runs, at earlier heads of both, gave the same figures. Cells under 10 survey households are suppressed; SPI-synthetic and donor copies don't count toward the 10.Role supplied (the data this PR should ship with): base (main) vs branch identical. Every compared array is identical in 2025 and 2026 across the versions run. The arrays are: person
is_claimant_or_partner, income tax and NI; benefit-unit UC, HB, Pension Credit, Child Benefit, CTC, WTC, IS, benefit cap andrelation_type; household CTR, net income, HBAI net income BHC/AHC, benefits, tax and BHC/AHC poverty.Role dropped (the evaluated enhanced FRS 2024-25 build, without the column):
relation_typechanges in those 33 households. UC, Pension Credit, CTR, household net income and income tax change in fewer than 10 survey households (suppressed).Calculator-style input: each eFRS household entered as one benefit unit with no roles, flags or heads (2025). Truth is the survey claimant and partner of the eldest member's own benefit unit, because the model's claimant is the eldest adult.
Where the rules disagree, this PR is right in 612 survey households (382k weighted) and main in 72 (146k weighted).
Sequencing (Max ruled d842: adopt; d871: merge on gates)
tests/certified_dataset.yaml: enhanced FRS 2024-25 from policyengine-uk-data 1.58.1), which the Aggregate impact job runs on, comes after it.Simulation()needs one explicitly or viaPOLICYENGINE_UK_DEFAULT_DATASET). policyengine.py's bundle manifest pins model and data together. On policyengine.py main (e125aa280, bundle uk-6.10.0), the UK default ismicrocosm_uk_2024_25, the 19 October Microcosm UK build.is_claimant_or_partnerfrom the FRS adult table since 6900190ee (8 October, microcosm#1095), and the build postdates that commit.enhanced_frs_2024_25,frs_2024_25and their_tinyversions are at 1.58.1, after Set default basic income threshold to £0 #524. Three non-default UK overlays carry nois_claimant_or_partner:populace_uk_2023(policyengine/populace-uk-private), andenhanced_frs_2023_24andfrs_2023_24(policyengine-uk-data at 655dd07e). Forfrs_2023_24this is inferred from the shared revision, not observed in a run.enhanced_frs_2023_24only households whose ids are raw FRS 2023-24 serial numbers count (the file doesn't flag its copies).populace_uk_2023: roles change in 18 source households (about 25k weighted households). Benefit and income amounts move in fewer than 10 (suppressed).enhanced_frs_2023_24: roles change in 18 survey households (64 households in the file, counting its unflagged synthetic and donor copies; about 13k weighted). Amounts move in fewer than 10 (suppressed).is_claimant_or_partner. A separate task adds the role to microcosm's populace UK build.is_claimant_or_partnerwill see unflagged couples 20+ years apart split. How many depends on the dataset: 33 survey households in the evaluated 2024-25 build, and 18 in the 2023-24 overlay.Invariants
Executed as Hypothesis properties in
test_child_and_adult_definitions_properties.py, plus the exhaustive age × education × flag grid against the updated referenceclaimants_or_partners:is_claimant_or_partnerroles are always kept, andis_couplefollows them.Mutation check (local log; run at 0448203, 31 YAML cases and 13 properties):
>, fails the 20-year YAML boundary case and the new exhaustive pair test.Tests
benefit_unit_structure.yaml, 32 cases):is_claimant_or_partner, or draws only couples the presumption reads as couples. Expectations and oracles are unchanged. Without either, the presumption would split these couples, as intended.relation_type: COUPLE.is_claimant_or_partnerthe model inferred before the change of age. Frozen roles keep the raised ages from changing who is in the benefit unit.benefit_unit_structure.yaml,is_uc_assessed_claimant.yamland the child-and-adult definition properties.uc_LCWRA_elementfor each rebalancing year (2026 to 2032) when the simulation is built (scenarios/uc_reform.py), and roles are inferred in those years.test_unflagged_adult_pairs_follow_the_20_year_gap(2,556 unflagged pairs aged 20-90), and a 70/45 couple under each head order, inferred and with supplied roles. The reference HBAI helper now needs an identified parent aged 16+, asis_hbai_dependent_childdoes; the under-16-parent regression also checks the reference.ruff formatandruff checkon the changed Python files.axiom: n/a: a PolicyEngine presumption for households entered without relationships, not a statutory rule. Benefit law defines the claimant and partner by relationship, not age.
🤖 Generated with Claude Code