From 613326be14b748956f3337afcf574a315eb29ffe Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:54:44 -0400 Subject: [PATCH 1/9] Presume a member 20+ years younger is the claimant's child, without flags 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 --- changelog.d/2039-unflagged-gap.changed.md | 1 + .../presumed_child/age_limit.yaml | 2 +- .../presumed_child/minimum_age_gap.yaml | 2 +- .../minimum_age_gap_at_any_age.yaml | 6 + .../demographic/benefit_unit_structure.yaml | 121 +++++++++++++++++- ..._child_and_adult_definitions_properties.py | 86 +++++++++---- .../demographic/is_claimant_or_partner.py | 26 ++-- 7 files changed, 203 insertions(+), 41 deletions(-) create mode 100644 changelog.d/2039-unflagged-gap.changed.md create mode 100644 policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml diff --git a/changelog.d/2039-unflagged-gap.changed.md b/changelog.d/2039-unflagged-gap.changed.md new file mode 100644 index 000000000..209a2e5a0 --- /dev/null +++ b/changelog.d/2039-unflagged-gap.changed.md @@ -0,0 +1 @@ +- Without any parent flag, a member at least 20 years younger than the claimant is now presumed to be their child at any age (`presumed_child.minimum_age_gap_at_any_age`), so ages 50 and 25 entered without relationships are a single claimant and an adult child, not a couple. Members 16 to 19 years younger stay partners from age 20. Survey datasets supply `is_claimant_or_partner`, so the presumption only applies to inputs without it. diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml index c8544b2ab..739370ef6 100644 --- a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/age_limit.yaml @@ -1,4 +1,4 @@ -description: Age below which a benefit-unit member much younger than the claimant is presumed to be the claimant's child rather than their partner, when relationships are not supplied. The limit does not apply when the claimant is flagged as a parent. This is a PolicyEngine modelling presumption, not a statutory rule. +description: Age below which a benefit-unit member much younger than the claimant is presumed to be the claimant's child rather than their partner, when relationships are not supplied. The limit does not apply when the claimant is flagged as a parent, nor to a member younger by at least the minimum age gap at any age. This is a PolicyEngine modelling presumption, not a statutory rule. values: 0001-01-01: 20 metadata: diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml index 560de05e3..d8d17ed80 100644 --- a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap.yaml @@ -1,4 +1,4 @@ -description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner, when relationships are not supplied. It applies at any age when the claimant is flagged as a parent, and otherwise only under the presumed child age limit. This is a PolicyEngine modelling presumption, not a statutory rule. +description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner, when relationships are not supplied. It applies at any age when the claimant is flagged as a parent, and otherwise only under the presumed child age limit; a larger gap (the minimum age gap at any age) applies at any age without flags. This is a PolicyEngine modelling presumption, not a statutory rule. values: 0001-01-01: 16 metadata: diff --git a/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml new file mode 100644 index 000000000..c8fea14d8 --- /dev/null +++ b/policyengine_uk/parameters/household/demographic/benefit_unit/presumed_child/minimum_age_gap_at_any_age.yaml @@ -0,0 +1,6 @@ +description: Minimum age gap below the claimant at which a younger member is presumed to be the claimant's child rather than their partner at any age, even when the claimant is not flagged as a parent, when relationships are not supplied. A member flagged as a parent can still be the partner. This is a PolicyEngine modelling presumption, not a statutory rule. +values: + 0001-01-01: 20 +metadata: + unit: year + label: presumed child minimum age gap at any age diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml index d64b7fab0..c323ffc53 100644 --- a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml +++ b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml @@ -373,8 +373,10 @@ is_claimant_or_partner: [true, true, false] # Below a claimant flagged as a parent, a member at least 16 years younger is -# presumed their child at any age. Without flags, the under-20 limit stays: -# survey childless couples carry no flags and include large age gaps. +# presumed their child at any age. Without flags, a member is presumed the +# claimant's child under 20 at a 16-year gap, or at any age at a 20-year gap: +# co-resident FRS adults 16-19 years apart are mostly couples, those 20 or more +# apart mostly not. Survey datasets supply is_claimant_or_partner instead. - name: A flagged lone parent with an adult son is single period: 2025 @@ -397,8 +399,7 @@ relation_type: SINGLE is_couple: false -- name: Without a parent flag, a 25-year gap is still read as a couple - # Documented limitation: indistinguishable from a survey childless couple. +- name: Without flags, a member 25 years younger is presumed the claimant's child period: 2025 absolute_error_margin: 0 input: @@ -414,8 +415,114 @@ household: members: [older, younger] output: - is_claimant_or_partner: [true, true] + is_claimant_or_partner: [true, false] + relation_type: SINGLE + is_couple: false + +- name: Without flags, the 20-year gap at any age is a boundary + period: 2025 + absolute_error_margin: 0 + input: + people: + head_a: + age: 50 + gap_19: + age: 31 + head_b: + age: 50 + gap_20: + age: 30 + head_c: + age: 90 + gap_20_at_70: + age: 70 + benunits: + unit_a: + members: [head_a, gap_19] + unit_b: + members: [head_b, gap_20] + unit_c: + members: [head_c, gap_20_at_70] + households: + household_a: + members: [head_a, gap_19] + household_b: + members: [head_b, gap_20] + household_c: + members: [head_c, gap_20_at_70] + output: + is_claimant_or_partner: [true, true, true, false, true, false] + +- name: A flagged partner 25 years younger stays the partner + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 60 + is_parent: true + partner: + age: 35 + is_parent: true + child: + age: 5 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + is_claimant_or_partner: [true, true, false] + is_couple: true + +- name: An unflagged couple 22 years apart with a child is assessed as a lone parent + # Known trade-off of the unflagged presumption: calculators that send ages + # only cannot tell this couple from a parent living with an adult child. + # Supplying is_claimant_or_partner keeps the couple (next case). + period: 2025 + absolute_error_margin: 0 + input: + people: + older: + age: 60 + younger: + age: 38 + child: + age: 10 + benunits: + benunit: + members: [older, younger, child] + households: + household: + members: [older, younger, child] + output: + is_claimant_or_partner: [true, false, false] + is_lone_parent: true + +- name: Supplied roles keep an unflagged couple 22 years apart + period: 2025 + absolute_error_margin: 0 + input: + people: + older: + age: 60 + is_claimant_or_partner: true + younger: + age: 38 + is_claimant_or_partner: true + child: + age: 10 + is_claimant_or_partner: false + benunits: + benunit: + members: [older, younger, child] + households: + household: + members: [older, younger, child] + output: is_couple: true + is_lone_parent: false - name: A flagged parent keeps an unflagged partner close in age, not the adult son period: 2025 @@ -493,11 +600,11 @@ input: people: head_a: - age: 40 + age: 37 aged_19: age: 19 head_b: - age: 40 + age: 38 aged_20: age: 20 benunits: diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index 88d8d24fa..bdff80b1a 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -11,9 +11,10 @@ the claimant-or-partner presumption follow their documented assumptions. An unflagged member at least 16 years younger than a claimant flagged as a parent is never their partner, at any age; flagging the claimant as a - parent never adds a partner; and benefit units shaped like the FRS's - (one or two adults, all flagged as parents when there are dependants) - keep the earlier under-20 presumption exactly. + parent never adds a partner. On benefit units shaped like the FRS's (one + or two adults, all flagged as parents when there are dependants) the + any-age 20-year gap changes only childless couples 20 or more years apart + (intended), and supplied claimant/partner roles are always kept. 2. HBAI types partition everyone. Any benefit unit with one head has at most two claimants/partners, all of them HBAI adults, and at least one if it has an HBAI adult; valid families (one or two adults aged 20+ and @@ -220,15 +221,16 @@ def hbai_dependent_child(p, family): ) -def claimants_or_partners(family, flagged_parent_lifts_age_limit=True): +def claimants_or_partners(family, any_age_gap=20): # is_claimant_or_partner's documented rule. The claimant is the head if an # HBAI adult, else the eldest HBAI adult. The partner is the eldest other # flagged parent, else the eldest other adult not presumed the claimant's # child: at least 16 years younger and either under 20 or below a claimant - # flagged as a parent. If the claimant is not a flagged parent but two or - # more others are, the two eldest of those are the couple instead. Age ties - # go to the earlier member. flagged_parent_lifts_age_limit=False is the - # earlier rule (under 20 only), kept for the FRS-shape differential test. + # flagged as a parent, or at least 20 years younger at any age. If the + # claimant is not a flagged parent but two or more others are, the two + # eldest of those are the couple instead. Age ties go to the earlier + # member. any_age_gap=None is the earlier rule without the any-age gap, + # kept for the differential test. n = len(family) adult = [not hbai_dependent_child(p, family) for p in family] ages = [p["age"] for p in family] @@ -243,12 +245,15 @@ def claimants_or_partners(family, flagged_parent_lifts_age_limit=True): if len(other_parents) >= 2 and not parent[claimant]: couple = sorted(other_parents, key=lambda i: (-ages[i], i))[:2] return [i in couple for i in range(n)] - no_age_limit = flagged_parent_lifts_age_limit and parent[claimant] + + def presumed_child(i): + gap = ages[claimant] - ages[i] + if any_age_gap is not None and gap >= any_age_gap: + return True + return (ages[i] < 20 or parent[claimant]) and gap >= 16 + pool = other_parents or [ - i - for i in adults - if i != claimant - and not ((ages[i] < 20 or no_age_limit) and ages[claimant] - ages[i] >= 16) + i for i in adults if i != claimant and not presumed_child(i) ] partner = max(pool, key=lambda i: (ages[i], -i)) if pool else None return [i == claimant or i == partner for i in range(n)] @@ -864,16 +869,53 @@ def frs_shaped_units(draw): return families +def survey_roles(family): + # The FRS adult table: everyone in a unit without dependants (nobody is + # flagged), otherwise the adults flagged as parents. + flagged = any(p["is_parent"] for p in family) + return [p["is_parent"] or not flagged for p in family] + + +def childless_couple_20_years_apart(family): + return ( + len(family) == 2 + and all(survey_roles(family)) + and abs(family[0]["age"] - family[1]["age"]) >= 20 + ) + + @PROPERTY_SETTINGS @given(frs_shaped_units()) -def test_frs_shaped_units_keep_the_under_20_presumption(families): - # Differential: on FRS-shaped units the rule equals the earlier, age-limited - # one, so the change cannot move the enhanced FRS microsimulation. +def test_frs_shaped_units_change_only_for_childless_couples_20_years_apart(families): + # Differential against the rule without the any-age gap. On FRS-shaped + # units the two differ only in childless two-adult units 20 or more years + # apart, which carry no flags: there the younger adult is presumed the + # claimant's child (intended). Survey datasets supply is_claimant_or_partner, + # so the microsimulation does not depend on this presumption. sim = simulate(families) - expected = [ - c - for family in families - for c in claimants_or_partners(family, flagged_parent_lifts_age_limit=False) - ] + expected = [c for family in families for c in claimants_or_partners(family)] assert_values(sim, "is_claimant_or_partner", expected) - assert expected == [c for f in families for c in claimants_or_partners(f)] + for family in families: + if not childless_couple_20_years_apart(family): + assert claimants_or_partners(family) == claimants_or_partners( + family, any_age_gap=None + ) + + +@PROPERTY_SETTINGS +@given(frs_shaped_units()) +def test_supplied_claimant_and_partner_roles_are_kept(families): + # policyengine-uk-data supplies is_claimant_or_partner from the FRS adult + # table: the head and any partner, whatever their ages and gap. Supplied + # roles override the presumption, so the FRS couples it would split stay + # couples, and every unit keeps one claimant and at most one partner. + roles = [role for family in families for role in survey_roles(family)] + sim = simulate(families) + sim.set_input("is_claimant_or_partner", YEAR, roles) + assert_values(sim, "is_claimant_or_partner", roles) + is_couple = sim.calculate("is_couple", YEAR) + offset = 0 + for i, family in enumerate(families): + n_roles = sum(roles[offset : offset + len(family)]) + assert is_couple[i] == (n_roles == 2) + offset += len(family) diff --git a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py index 847e56e5b..d798ac652 100644 --- a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py +++ b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py @@ -22,15 +22,19 @@ class is_claimant_or_partner(Variable): parent (`is_parent`) if there is one, otherwise the eldest other adult who is not presumed to be the claimant's child. A member at least 16 years younger than the claimant is presumed to be their child if they - are under 20 or the claimant is flagged as a parent (a PolicyEngine - presumption for households entered without relationships). So a lone + are under 20 or the claimant is flagged as a parent, and a member at + least 20 years younger is presumed their child at any age (PolicyEngine + presumptions for households entered without relationships). So a lone parent flagged `is_parent` who is the claimant and lives with an unflagged son or daughter at least 16 years younger is single, and the son or daughter is neither claimant nor partner. - - Without any parent flag, a member aged 20 or over is never presumed a - child. Survey benefit units carry no flags for childless couples, and - those include large age gaps, so the model cannot tell such a couple - from a parent and adult child. + - Without any parent flag, a member aged 20 or over is presumed a child + only when at least 20 years younger. In the Family Resources Survey, + co-resident adults 20 or more years apart are mostly in separate benefit + units, while those 16 to 19 years apart are mostly couples. A couple 20 + or more years apart entered without flags or roles is therefore assessed + as a single claimant; supply `is_claimant_or_partner` for such a couple. + Survey datasets can supply it from the survey's own benefit units. - Flags follow the Family Resources Survey convention: in a benefit unit with children, both members of the couple are flagged as parents. If only the claimant of a couple with children is flagged, a partner 16 or @@ -82,10 +86,12 @@ def formula(person, period, parameters): person.get_rank(person.benunit, -age, condition=other_parent) < 2 ) # Below a flagged parent the age limit does not apply: a much younger - # unflagged member is their child, whatever their age. - presumed_child = ((age < p.age_limit) | claimant_is_parent) & ( - claimant_age - age >= p.minimum_age_gap - ) + # unflagged member is their child, whatever their age. Without flags, + # only a larger gap overrides the age limit. + gap = claimant_age - age + presumed_child = ( + ((age < p.age_limit) | claimant_is_parent) & (gap >= p.minimum_age_gap) + ) | (gap >= p.minimum_age_gap_at_any_age) other_adult = adult & ~claimant & ~presumed_child pool = where(person.benunit.any(other_parent), other_parent, other_adult) partner = pool & (person.get_rank(person.benunit, -age, condition=pool) == 0) From 04482030aef340b671429dfba37c5933d59997cf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 12:09:36 -0400 Subject: [PATCH 2/9] Test every unflagged adult pair against the 20-year gap 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 --- .../test_child_and_adult_definitions_properties.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index dce08537b..323c3a6d3 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -934,3 +934,15 @@ def test_supplied_claimant_and_partner_roles_are_kept(families): n_roles = sum(roles[offset : offset + len(family)]) assert is_couple[i] == (n_roles == 2) offset += len(family) + + +def test_unflagged_adult_pairs_follow_the_20_year_gap(): + # Exhaustive over unflagged two-adult units, both 20 to 90, head the + # elder: the younger is the partner exactly when under 20 years younger. + pairs = [(a, b) for a in range(20, 91) for b in range(20, a + 1)] + families = [[person(a, is_benunit_head=True), person(b)] for a, b in pairs] + sim = simulate(families) + claimants = sim.calculate("is_claimant_or_partner", YEAR) + expected = [c for a, b in pairs for c in (True, a - b < 20)] + np.testing.assert_array_equal(claimants, expected) + assert expected == [c for f in families for c in claimants_or_partners(f)] From d8fc7444f73e73757934e94237f35de18b885068 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 15:20:15 -0400 Subject: [PATCH 3/9] Address review r1: supply roles in wide-gap couple fixtures, fix invariants - 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 --- .../gov/dwp/lifetime_isa_capital.yaml | 5 ++ .../is_pension_credit_eligible.yaml | 5 ++ .../demographic/benefit_unit_structure.yaml | 22 ++++++++ ..._child_and_adult_definitions_properties.py | 54 +++++++++++++++---- .../demographic/is_claimant_or_partner.py | 9 ++-- 5 files changed, 81 insertions(+), 14 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml index 259462514..21513b759 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml @@ -337,15 +337,19 @@ pension_credit_assessable_capital: [4_000, 0] - name: Pension Credit counts a younger partner's Lifetime ISA after the charge + # The couple is 25 years apart, so the roles are supplied: without them the + # 45-year-old would be presumed the claimant's child. period: 2025 absolute_error_margin: 0 input: people: claimant: age: 70 + is_claimant_or_partner: true partner: age: 45 lifetime_isa_balance: 10_000 + is_claimant_or_partner: true benunits: couple: members: [claimant, partner] @@ -353,6 +357,7 @@ household: members: [claimant, partner] output: + is_claimant_or_partner: [true, true] pension_credit_assessable_capital: 7_500 - name: A dependent young person's Lifetime ISA does not count against the parent's claim diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml index 00a7b4b38..25527b2b1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/is_pension_credit_eligible.yaml @@ -160,17 +160,22 @@ is_pension_credit_eligible: true - name: Case 4, mixed-age couple without protected exception is ineligible. + # The couple is 25 years apart, so the roles are supplied: without them the + # 54-year-old would be presumed the claimant's child. period: 2026 input: people: person1: age: 79 + is_claimant_or_partner: true person2: age: 54 + is_claimant_or_partner: true benunits: benunit: members: [person1, person2] output: + is_claimant_or_partner: [true, true] is_pension_credit_eligible: false - name: Case 5, child in benefit unit does not block pension-age adult eligibility. diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml index 6ab26fa96..0ddcccfde 100644 --- a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml +++ b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml @@ -454,6 +454,28 @@ output: is_claimant_or_partner: [true, true, true, false, true, false] +- name: A younger head 20 years below the other adult keeps the couple + # The gap is measured below the claimant (the head), so an elder partner is + # never presumed the head's child. + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 50 + is_benunit_head: true + elder: + age: 70 + benunits: + benunit: + members: [head, elder] + households: + household: + members: [head, elder] + output: + is_claimant_or_partner: [true, true] + is_couple: true + - name: A flagged partner 25 years younger stays the partner period: 2025 absolute_error_margin: 0 diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index 323c3a6d3..7cd5ef605 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -14,12 +14,15 @@ at least 16 years younger explains the flag; flagging the claimant as a parent never adds a partner. On benefit units shaped like the FRS's (one or two adults, all flagged as parents when there are dependants) the - any-age 20-year gap changes only childless couples 20 or more years apart - (intended), and supplied claimant/partner roles are always kept. + any-age 20-year gap changes only childless couples whose claimant is 20 or + more years older than the other adult (intended), and supplied + claimant/partner roles are always kept. 2. HBAI types partition everyone. Any benefit unit with one head has at most two claimants/partners, all of them HBAI adults, and at least one if it has an HBAI adult; valid families (one or two adults aged 20+ and - dependants) have exactly their adults. Couple/single and + dependants) have exactly their adults, except an unflagged couple whose + head is 20+ years older than the other adult (the any-age presumption, + intended), and always when the roles are supplied. Couple/single and couple/lone-parent/single-person partition benefit units. Heads aged 16+ are claimants or partners, including when explicitly younger than others, unless two other members are flagged parents and the head is not. @@ -217,7 +220,7 @@ def hbai_dependent_child(p, family): return True return ( p["age"] < 20 - and any(member["is_parent"] for member in family) + and any(member["is_parent"] and member["age"] >= 16 for member in family) and (non_advanced_education(p) or p["is_in_approved_training"]) ) @@ -558,7 +561,16 @@ def test_valid_family_partition_and_adding_a_child_preserves_claimants( for family in families: old = claimants[old_offset : old_offset + len(family)] new = claimants[new_offset : new_offset + len(family)] - assert sum(old) == sum(p["age"] >= 20 for p in family) + np.testing.assert_array_equal(old, claimants_or_partners(family)) + # Every adult is selected, except in an unflagged couple whose head + # (the claimant) is 20+ years older: the any-age presumption (intended). + adults = [p for p in family if p["age"] >= 20] + wide_gap = ( + len(adults) == 2 + and not adults[0]["is_parent"] + and adults[0]["age"] - adults[1]["age"] >= 20 + ) + assert sum(old) == len(adults) - wide_gap assert sum(old) in (1, 2) np.testing.assert_array_equal(new, old) assert not claimants[new_offset + len(family)] @@ -762,9 +774,13 @@ def test_parent_marked_under_16_entrant_preserves_existing_claimants(): person(20, is_benunit_head=True), person(18, current_education="POST_SECONDARY"), ] - sim = simulate([family, family + [person(0, is_parent=True)]]) + entrant = family + [person(0, is_parent=True)] + sim = simulate([family, entrant]) actual = sim.calculate("is_claimant_or_partner", YEAR) np.testing.assert_array_equal(actual[:2], actual[2:4]) + # The reference ignores a parent flag under 16, as the model does. + expected = claimants_or_partners(family) + claimants_or_partners(entrant) + np.testing.assert_array_equal(actual, expected) @st.composite @@ -903,10 +919,11 @@ def childless_couple_20_years_apart(family): @given(frs_shaped_units()) def test_frs_shaped_units_change_only_for_childless_couples_20_years_apart(families): # Differential against the rule without the any-age gap. On FRS-shaped - # units the two differ only in childless two-adult units 20 or more years - # apart, which carry no flags: there the younger adult is presumed the - # claimant's child (intended). Survey datasets supply is_claimant_or_partner, - # so the microsimulation does not depend on this presumption. + # units the two can differ only in childless two-adult units 20 or more + # years apart, which carry no flags: there, if the claimant (the head) is + # the elder, the younger adult is presumed their child (intended); a + # younger head keeps the couple. Survey datasets supply + # is_claimant_or_partner, so the microsimulation does not depend on this. sim = simulate(families) expected = [c for family in families for c in claimants_or_partners(family)] assert_values(sim, "is_claimant_or_partner", expected) @@ -946,3 +963,20 @@ def test_unflagged_adult_pairs_follow_the_20_year_gap(): expected = [c for a, b in pairs for c in (True, a - b < 20)] np.testing.assert_array_equal(claimants, expected) assert expected == [c for f in families for c in claimants_or_partners(f)] + + +def test_wide_gap_couples_by_head_order_and_with_supplied_roles(): + # An unflagged couple 25 years apart: with the elder as head (the claimant) + # the younger is presumed their child; with the younger as head the elder + # is the partner. Supplied roles keep the couple either way. + elder_head = [person(70, is_benunit_head=True), person(45)] + younger_head = [person(45, is_benunit_head=True), person(70)] + families = [elder_head, younger_head, elder_head, younger_head] + sim = simulate(families) + inferred = sim.calculate("is_claimant_or_partner", YEAR) + np.testing.assert_array_equal(inferred[:4], [True, False, True, True]) + assert claimants_or_partners(elder_head) == [True, False] + assert claimants_or_partners(younger_head) == [True, True] + supplied = simulate(families) + supplied.set_input("is_claimant_or_partner", YEAR, [True] * 8) + np.testing.assert_array_equal(supplied.calculate("is_couple", YEAR), [True] * 4) diff --git a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py index efef6965d..bb8c30710 100644 --- a/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py +++ b/policyengine_uk/variables/household/demographic/is_claimant_or_partner.py @@ -39,10 +39,11 @@ class is_claimant_or_partner(Variable): only when at least 20 years younger. In the Family Resources Survey, co-resident adults 20 or more years apart are mostly in separate benefit units, while those 16 to 19 years apart are mostly couples. A couple 20 - or more years apart entered without roles is therefore assessed as a - single claimant unless the younger is flagged as a parent; supply - `is_claimant_or_partner` for such a couple. Survey datasets can supply - it from the survey's own benefit units. + or more years apart entered without roles, whose elder is the claimant + (the head, or the eldest adult when no adult head is given), is + therefore assessed as a single claimant unless the younger is flagged as + a parent; supply `is_claimant_or_partner` for such a couple. Survey + datasets can supply it from the survey's own benefit units. - Flags follow the Family Resources Survey convention: in a benefit unit with children, both members of the couple are flagged as parents. When relationships are known, supply this variable for everyone (true for From f43e5ec3b8cf2c355c231b2adc5ed5eb89da42ec Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 07:48:35 -0400 Subject: [PATCH 4/9] Supply roles in couple-generating property tests; correct dataset claims CI on d8fc7444f: 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 --- changelog.d/2039-unflagged-gap.changed.md | 2 +- .../demographic/benefit_unit_structure.yaml | 2 +- ..._child_and_adult_definitions_properties.py | 31 ++++++++++--------- ...sion_credit_reported_capital_properties.py | 8 ++++- .../tests/test_uc_state_pension_properties.py | 10 ++++-- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/changelog.d/2039-unflagged-gap.changed.md b/changelog.d/2039-unflagged-gap.changed.md index 209a2e5a0..e29169cef 100644 --- a/changelog.d/2039-unflagged-gap.changed.md +++ b/changelog.d/2039-unflagged-gap.changed.md @@ -1 +1 @@ -- Without any parent flag, a member at least 20 years younger than the claimant is now presumed to be their child at any age (`presumed_child.minimum_age_gap_at_any_age`), so ages 50 and 25 entered without relationships are a single claimant and an adult child, not a couple. Members 16 to 19 years younger stay partners from age 20. Survey datasets supply `is_claimant_or_partner`, so the presumption only applies to inputs without it. +- Without any parent flag, a member at least 20 years younger than the claimant is now presumed to be their child at any age (`presumed_child.minimum_age_gap_at_any_age`), so ages 50 and 25 entered without relationships are a single claimant and an adult child, not a couple. Members 16 to 19 years younger stay partners from age 20. The presumption only applies where `is_claimant_or_partner` is not supplied: a dataset that carries it (such as policyengine-uk-data's enhanced FRS once it supplies the FRS roles) is unaffected, while datasets without it are not. diff --git a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml index 0ddcccfde..2d5ebe020 100644 --- a/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml +++ b/policyengine_uk/tests/policy/baseline/household/demographic/benefit_unit_structure.yaml @@ -377,7 +377,7 @@ # years younger already explains the flag. Without flags, a member is presumed # the claimant's child under 20 at a 16-year gap, or at any age at a 20-year # gap: co-resident FRS adults 16-19 years apart are mostly couples, those 20 or -# more apart mostly not. Survey datasets supply is_claimant_or_partner instead. +# more apart mostly not. Datasets and users can supply is_claimant_or_partner instead. - name: A flagged lone parent with an adult son is single period: 2025 diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index 7cd5ef605..810fa9acc 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -907,12 +907,12 @@ def survey_roles(family): return [p["is_parent"] or not flagged for p in family] -def childless_couple_20_years_apart(family): - return ( - len(family) == 2 - and all(survey_roles(family)) - and abs(family[0]["age"] - family[1]["age"]) >= 20 - ) +def childless_couple_split_by_any_age_gap(family): + # A childless two-adult unit whose head (the claimant) is 20+ years older. + if len(family) != 2 or not all(survey_roles(family)): + return False + head, other = sorted(family, key=lambda p: not p["is_benunit_head"]) + return head["age"] - other["age"] >= 20 @PROPERTY_SETTINGS @@ -922,13 +922,14 @@ def test_frs_shaped_units_change_only_for_childless_couples_20_years_apart(famil # units the two can differ only in childless two-adult units 20 or more # years apart, which carry no flags: there, if the claimant (the head) is # the elder, the younger adult is presumed their child (intended); a - # younger head keeps the couple. Survey datasets supply - # is_claimant_or_partner, so the microsimulation does not depend on this. + # younger head keeps the couple. A dataset that carries + # is_claimant_or_partner (policyengine-uk-data#524's enhanced FRS) does not + # depend on this; datasets without the role do. sim = simulate(families) expected = [c for family in families for c in claimants_or_partners(family)] assert_values(sim, "is_claimant_or_partner", expected) for family in families: - if not childless_couple_20_years_apart(family): + if not childless_couple_split_by_any_age_gap(family): assert claimants_or_partners(family) == claimants_or_partners( family, any_age_gap=None ) @@ -937,8 +938,8 @@ def test_frs_shaped_units_change_only_for_childless_couples_20_years_apart(famil @PROPERTY_SETTINGS @given(frs_shaped_units()) def test_supplied_claimant_and_partner_roles_are_kept(families): - # policyengine-uk-data supplies is_claimant_or_partner from the FRS adult - # table: the head and any partner, whatever their ages and gap. Supplied + # policyengine-uk-data#524 supplies is_claimant_or_partner from the FRS + # adult table: the head and any partner, whatever their ages and gap. Supplied # roles override the presumption, so the FRS couples it would split stay # couples, and every unit keeps one claimant and at most one partner. roles = [role for family in families for role in survey_roles(family)] @@ -971,12 +972,12 @@ def test_wide_gap_couples_by_head_order_and_with_supplied_roles(): # is the partner. Supplied roles keep the couple either way. elder_head = [person(70, is_benunit_head=True), person(45)] younger_head = [person(45, is_benunit_head=True), person(70)] - families = [elder_head, younger_head, elder_head, younger_head] + families = [elder_head, younger_head] sim = simulate(families) inferred = sim.calculate("is_claimant_or_partner", YEAR) - np.testing.assert_array_equal(inferred[:4], [True, False, True, True]) + np.testing.assert_array_equal(inferred, [True, False, True, True]) assert claimants_or_partners(elder_head) == [True, False] assert claimants_or_partners(younger_head) == [True, True] supplied = simulate(families) - supplied.set_input("is_claimant_or_partner", YEAR, [True] * 8) - np.testing.assert_array_equal(supplied.calculate("is_couple", YEAR), [True] * 4) + supplied.set_input("is_claimant_or_partner", YEAR, [True] * 4) + np.testing.assert_array_equal(supplied.calculate("is_couple", YEAR), [True] * 2) diff --git a/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py index cf5fef6f3..2f8810671 100644 --- a/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py +++ b/policyengine_uk/tests/test_pension_credit_reported_capital_properties.py @@ -77,9 +77,15 @@ def simulate(people, units, household, overrides=None): "members": unit["members"], "pension_credit_reported_capital": {year: value}, } + # Every unit is a single adult or a couple whatever the age gap, so the + # roles are supplied rather than left to the age presumption (which reads + # a member 20+ years younger as the claimant's child). situation = { "people": { - name: {k: {year: v} for k, v in person.items()} + name: { + **{k: {year: v} for k, v in person.items()}, + "is_claimant_or_partner": {year: True}, + } for name, person in people.items() }, "benunits": benunits, diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py index 9b6802274..d3cec9c8e 100644 --- a/policyengine_uk/tests/test_uc_state_pension_properties.py +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -102,7 +102,13 @@ def situation(units, year, income_variable="state_pension", pension_bump=0.0): names = [] for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" - person = {"age": {year: age}, "state_pension": {year: 0.0}} + # The adults are the couple whatever their age gap, so the roles + # are supplied rather than left to the age presumption. + person = { + "age": {year: age}, + "state_pension": {year: 0.0}, + "is_claimant_or_partner": {year: True}, + } if j == 0: amount = unit["state_pension"] + pension_bump person[income_variable] = {year: amount} @@ -112,7 +118,7 @@ def situation(units, year, income_variable="state_pension", pension_bump=0.0): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { From 45f349789665deaf245ae5d054ad18fe2ecbafdb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 14:36:57 -0400 Subject: [PATCH 5/9] Merge #2040's head fd5e9474; restate invariant 4 for the any-age 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 --- .../tests/test_child_and_adult_definitions_properties.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index 19e1f1b16..8cbb15efc 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -36,8 +36,10 @@ the added child carries a parent marker (an identified parent must be aged 16 or over; the minimised counterexample that found this is kept as a regression test). It does not hold below a flagged claimant whose flag - nothing explains: a flagged 50 with a 25-year-old is single, and adding a - 12-year-old explains the flag and makes them a couple. + nothing explains, for a member 16 to 19 years younger: a flagged 38 with a + 20-year-old is single, and adding a 12-year-old explains the flag and + makes them a couple. A member 20 or more years younger stays the + claimant's child either way (the any-age gap). Before/after families share one Simulation to avoid repeated model construction, but occupy distinct households and benefit units. From d88ac0dc9f61f060127853d365d998bb870c828e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 14:57:49 -0400 Subject: [PATCH 6/9] Supply roles in main's 70/50 mixed-age couple fixtures 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 --- .../housing_benefit_applicable_income_disregard.yaml | 2 ++ .../gov/dwp/benefit_cap/benefit_cap_pension_age.yaml | 6 ++++++ .../council_tax_reduction_pensioner.yaml | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml index 95d447790..00e8f1a8e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml @@ -460,8 +460,10 @@ people: claimant: age: 70 + is_claimant_or_partner: true partner: age: 50 + is_claimant_or_partner: true employment_income: 3_000 benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml index b0232bd97..2baa03827 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml @@ -21,8 +21,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true child_1: age: 4 child_2: @@ -54,8 +56,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true employment_income: 20_000 child_1: age: 4 @@ -118,8 +122,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true jsa_income_reported: 4_000 benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml index 444244a94..a9026ff94 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml @@ -19,9 +19,11 @@ people: older: age: 70 + is_claimant_or_partner: true council_tax_benefit_reported: 700 younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -130,8 +132,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -161,8 +165,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true son: age: 30 employment_income: 30_000 @@ -261,8 +267,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -285,8 +293,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true benunits: benunit: members: [older, younger] @@ -318,8 +328,10 @@ people: older: age: 70 + is_claimant_or_partner: true younger: age: 50 + is_claimant_or_partner: true child: age: 9 benunits: From 8de59f799e7628eb0be2a68fde5943771c545afc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 15:19:36 -0400 Subject: [PATCH 7/9] Supply roles in two more couple-generating property tests 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 --- .../test_benefit_cap_and_ctr_pension_age_properties.py | 7 +++++-- policyengine_uk/tests/test_mixed_age_couple_properties.py | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py index ea0ece7cc..129d7b85b 100644 --- a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py @@ -322,8 +322,11 @@ def run(older_is_pension_age): names = [f"p{i}_a", f"p{i}_b"] + [ f"p{i}_c{k}" for k in range(c["children"]) ] - people[names[0]] = {"age": {PERIOD: older}} - people[names[1]] = {"age": {PERIOD: c["working_age"]}} + # The two adults are the couple whatever their age gap, so the + # roles are supplied rather than left to the age presumption. + couple = {"is_claimant_or_partner": {PERIOD: True}} + people[names[0]] = {"age": {PERIOD: older}, **couple} + people[names[1]] = {"age": {PERIOD: c["working_age"]}, **couple} for k, name in enumerate(names[2:]): people[name] = {"age": {PERIOD: 2 + 3 * k}} benunits[f"b{i}"] = { diff --git a/policyengine_uk/tests/test_mixed_age_couple_properties.py b/policyengine_uk/tests/test_mixed_age_couple_properties.py index a6b08165e..43c388f1a 100644 --- a/policyengine_uk/tests/test_mixed_age_couple_properties.py +++ b/policyengine_uk/tests/test_mixed_age_couple_properties.py @@ -158,6 +158,9 @@ def cases(draw): people["dependant"] = {"age": 18, "current_education": "UPPER_SECONDARY"} people["older"]["is_parent"] = True for name, person in people.items(): + # The older and younger members are the couple whatever their age gap, + # so the roles are supplied rather than left to the age presumption. + person["is_claimant_or_partner"] = name != "dependant" if name == "dependant": continue if person["age"] >= 67: From 73a7b904b3f5c5234f8aa2cb3cba681ac90e750a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 16:27:24 -0400 Subject: [PATCH 8/9] Supply roles in the CTR pensioner earnings disregard generator 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 --- ...x_reduction_pensioner_earnings_disregard_properties.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py index 34b3c49c5..a176c79c6 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py @@ -116,11 +116,17 @@ def situation(units, year, earnings_bump=0.0, no_income_related_benefits=False): "weekly_hours": {year: adult["weekly_hours"]}, "is_disabled_for_benefits": {year: adult["disabled"]}, "state_pension": {year: adult["state_pension"]}, + # The adults are the claimant and partner whatever their age + # gap, so the roles are supplied rather than presumed. + "is_claimant_or_partner": {year: True}, } names.append(name) for k, child_age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: child_age}} + people[name] = { + "age": {year: child_age}, + "is_claimant_or_partner": {year: False}, + } names.append(name) benunits[f"b{i}"] = { "members": names, From 12962b09fd9751fd4d6e238b5d474cae4b139561 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 17:22:54 -0400 Subject: [PATCH 9/9] Supply roles in the Pension Credit carer income generator PR CI on 73a7b904b: 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 --- .../tests/test_pension_credit_carer_income_properties.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py index ac8cd7262..c4e766391 100644 --- a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py +++ b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py @@ -142,6 +142,9 @@ def situation(families): "state_pension": {YEAR: state_pension}, # All private pension goes to the first adult. "private_pension_income": {YEAR: float(pension) * (j == 0)}, + # The adults are the claimant and partner whatever their + # age gap, so the roles are supplied rather than presumed. + "is_claimant_or_partner": {YEAR: True}, } if is_carer: if fam["by_hours"]: