diff --git a/changelog.d/pc-sda-statutory-conditions.fixed.md b/changelog.d/pc-sda-statutory-conditions.fixed.md new file mode 100644 index 0000000000..6946ce3bdf --- /dev/null +++ b/changelog.d/pc-sda-statutory-conditions.fixed.md @@ -0,0 +1 @@ +- Apply the State Pension Credit severe disability addition's residence and carer conditions (SPC Regs 2002 Sch I paras 1-2 and reg 6(5)): no other resident aged 18 or over, ignoring qualifying-benefit recipients, blind people and qualifying young persons; and a carer benefit reduces or removes the addition only when it is paid for caring for a qualifying claimant or partner, so a claimant's own Carer's Allowance no longer removes it. Pension-age Housing Benefit and Council Tax Reduction follow once their severe disability premium reads this variable. diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/other_resident_age_limit.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/other_resident_age_limit.yaml new file mode 100644 index 0000000000..10217f7c64 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/severe_disability/other_resident_age_limit.yaml @@ -0,0 +1,11 @@ +description: Age from which another person normally residing with the claimant stops the Pension Credit severe disability addition, unless the person is one whose presence is ignored. +values: + 2002-01-01: 18 +metadata: + unit: year + label: Pension Credit severe disability other resident age limit + reference: + - title: The State Pension Credit Regulations 2002 sch. I para. 1(1)(a)(ii), (b)(ii) and (c)(iii) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 sch. 3 para. 6(2)(a)(ii) and (b)(iii) + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_residence_and_carers.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_residence_and_carers.yaml new file mode 100644 index 0000000000..75e75aafbd --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/severe_disability_residence_and_carers.yaml @@ -0,0 +1,945 @@ +# State Pension Credit Regulations 2002 (SI 2002/1792) reg 6(4)-(5) and +# Sch I paras 1-2: the residence condition (para 1(1)(a)(ii), (b)(ii) and +# (c)(iii), with the persons ignored under para 2) and the carer conditions +# (para 1(1)(a)(iii), (b)(ii) and (c)(iv)). 2026-27 weekly rates: one rate +# 86.05 (4,474.60 a year), two rates 172.10 (8,949.20 a year). +# Carer benefits are attributed as documented on +# num_severe_disability_addition_qualifiers_cared_for. + +- name: A single claimant on Attendance Allowance who lives alone gets one rate + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + meets_severe_disability_addition_residence_condition: true + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: An adult son living in the household stops the addition + # Para 1(1)(a)(ii): no person aged 18 or over normally resides with the + # claimant, other than a para 2 person. The son is his own benefit unit. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + son: + age: 45 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + households: + household: + members: [claimant, son] + output: + is_counted_resident_for_severe_disability_addition: [false, true] + meets_severe_disability_addition_residence_condition: [false, true] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: An adult son who receives the PIP daily living component is ignored + # Para 2(2)(a). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + son: + age: 45 + pip_dl_category: STANDARD + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + households: + household: + members: [claimant, son] + output: + is_counted_resident_for_severe_disability_addition: [false, false] + # The son also qualifies in his own right and lives with no counted adult. + severe_disability_minimum_guarantee_addition: [4_474.60, 4_474.60] + +- name: A blind adult son is ignored + # Para 2(2)(b). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: LOWER + son: + age: 45 + is_blind: true + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + households: + household: + members: [claimant, son] + output: + severe_disability_minimum_guarantee_addition: [4_474.60, 0] + +- name: A resident aged 17 is under the age limit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + grandchild: + age: 17 + benunits: + claimant_unit: + members: [claimant] + grandchild_unit: + members: [grandchild] + households: + household: + members: [claimant, grandchild] + output: + severe_disability_minimum_guarantee_addition: [4_474.60, 0] + +- name: An 18-year-old qualifying young person in the claimant's benefit unit is ignored + # Para 2(2)(f): a qualifying young person within reg 4A. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_parent: true + aa_category: HIGHER + young_person: + age: 18 + is_in_non_advanced_education: true + benunits: + benunit: + members: [claimant, young_person] + households: + household: + members: [claimant, young_person] + output: + is_qualifying_young_person_for_pension_credit: [false, true] + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: A 19-year-old in the benefit unit who is not a qualifying young person counts + # Neither claimant, partner nor qualifying young person, so they are a + # person aged 18 or over residing with the claimant. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + is_parent: true + aa_category: HIGHER + adult_child: + age: 19 + benunits: + benunit: + members: [claimant, adult_child] + households: + household: + members: [claimant, adult_child] + output: + is_claimant_or_partner: [true, false] + meets_severe_disability_addition_residence_condition: false + severe_disability_minimum_guarantee_addition: 0 + +- name: A couple who both qualify lose both rates to an adult lodger + # Para 1(1)(b)(ii); a commercial lodger who is not a close relative would be + # ignored under para 2(5), which the model cannot identify. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + partner: + age: 72 + aa_category: LOWER + lodger: + age: 30 + benunits: + couple: + members: [claimant, partner] + lodger_unit: + members: [lodger] + households: + household: + members: [claimant, partner, lodger] + output: + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: A single claimant's own Carer's Allowance does not stop the addition + # Para 1(1)(a)(iii) bars only a carer benefit paid in respect of caring for + # the claimant; no one is their own carer. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + country: ENGLAND + output: + receives_carer_benefit: true + num_severe_disability_addition_qualifiers_cared_for: 0 + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: Carer's Allowance paid to another household member stops a single claimant's addition + # The carer receives PIP daily living, so para 2(2)(a) ignores them for the + # residence condition; their Carer's Allowance is treated as caring for the + # claimant (para 1(1)(a)(iii)). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + daughter: + age: 50 + pip_dl_category: STANDARD + care_hours: 35 + carers_allowance_reported: 1 + benunits: + claimant_unit: + members: [claimant] + daughter_unit: + members: [daughter] + households: + household: + members: [claimant, daughter] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 0] + severe_disability_minimum_guarantee_addition: [0, 4_474.60] + +- name: A couple who both qualify get one rate when the claimant receives Carer's Allowance + # The claimant is treated as caring for the qualifying partner: a carer + # benefit for one only of the partners (para 1(1)(b)(ii)), so reg 6(5)(a). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + partner: + age: 72 + aa_category: HIGHER + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: 1 + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: A couple who both qualify get one rate when another household member receives Carer's Allowance + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + partner: + age: 72 + aa_category: HIGHER + daughter: + age: 50 + pip_dl_category: STANDARD + care_hours: 35 + carers_allowance_reported: 1 + benunits: + couple: + members: [claimant, partner] + daughter_unit: + members: [daughter] + households: + household: + members: [claimant, partner, daughter] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 0] + severe_disability_minimum_guarantee_addition: [4_474.60, 4_474.60] + +- name: A couple get nothing when carer benefits are paid for both partners + # The partner cares for the claimant and the daughter for the partner: + # para 1(1)(b)(ii) fails, and so does (c)(iv). + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + partner: + age: 72 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + daughter: + age: 50 + pip_dl_category: STANDARD + care_hours: 35 + carers_allowance_reported: 1 + benunits: + couple: + members: [claimant, partner] + daughter_unit: + members: [daughter] + households: + household: + members: [claimant, partner, daughter] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [2, 0] + severe_disability_minimum_guarantee_addition: [0, 4_474.60] + +- name: A qualifying partner's own Carer's Allowance does not stop the blind-partner route + # Para 1(1)(c)(iv) bars a carer benefit for the qualifying partner. The + # qualifying partner's own allowance cannot be for the blind partner, who + # receives no qualifying benefit, so it is for someone else. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + partner: + age: 72 + is_blind: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: 0 + severe_disability_minimum_guarantee_addition: 4_474.60 + +- name: The blind-partner route also needs no other adult resident + # Para 1(1)(c)(iii). + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + partner: + age: 72 + is_blind: true + son: + age: 40 + benunits: + couple: + members: [claimant, partner] + son_unit: + members: [son] + households: + household: + members: [claimant, partner, son] + output: + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: Two pensioner benefit units sharing a household each count against the other + # Two sisters, each her own benefit unit; only one receives Attendance + # Allowance, so she is ignored for her sister but her sister counts for her. + period: 2026 + absolute_error_margin: 0 + input: + people: + sister_one: + age: 80 + aa_category: HIGHER + sister_two: + age: 78 + benunits: + unit_one: + members: [sister_one] + unit_two: + members: [sister_two] + households: + household: + members: [sister_one, sister_two] + output: + meets_severe_disability_addition_residence_condition: [false, true] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: Residents in another household do not count + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + neighbour: + age: 45 + benunits: + claimant_unit: + members: [claimant] + neighbour_unit: + members: [neighbour] + households: + claimant_household: + members: [claimant] + neighbour_household: + members: [neighbour] + output: + severe_disability_minimum_guarantee_addition: [4_474.60, 0] + +- name: The addition carries into the minimum guarantee + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + son: + age: 45 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + households: + household: + members: [claimant, son] + output: + # Standard minimum guarantee only: 238.00 x 52 = 12,376. + minimum_guarantee: [12_376, 12_376] + +- name: A resident on the DLA care component at the middle rate or on AFIP is ignored, but not one on the lowest rate or on PIP mobility only + # Para 2(2)(a) lists DLA care at the highest or middle rate, PIP daily + # living and Armed Forces Independence Payment; other awards do not count. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant_1: + age: 80 + aa_category: HIGHER + resident_1: + age: 40 + dla_sc_category: MIDDLE + claimant_2: + age: 80 + aa_category: HIGHER + resident_2: + age: 40 + armed_forces_independence_payment: 1_000 + claimant_3: + age: 80 + aa_category: HIGHER + resident_3: + age: 40 + dla_sc_category: LOWER + claimant_4: + age: 80 + aa_category: HIGHER + resident_4: + age: 40 + pip_m_category: ENHANCED + benunits: + unit_1: + members: [claimant_1] + resident_unit_1: + members: [resident_1] + unit_2: + members: [claimant_2] + resident_unit_2: + members: [resident_2] + unit_3: + members: [claimant_3] + resident_unit_3: + members: [resident_3] + unit_4: + members: [claimant_4] + resident_unit_4: + members: [resident_4] + households: + household_1: + members: [claimant_1, resident_1] + household_2: + members: [claimant_2, resident_2] + household_3: + members: [claimant_3, resident_3] + household_4: + members: [claimant_4, resident_4] + output: + is_counted_resident_for_severe_disability_addition: + [false, false, false, false, false, true, false, true] + # The middle-rate and AFIP residents qualify in their own right too. + severe_disability_minimum_guarantee_addition: + [4_474.60, 4_474.60, 4_474.60, 4_474.60, 0, 0, 0, 0] + +- name: A 19-year-old who started their course at 19 is not a qualifying young person and counts + # Reg 4A: the education must have started, or been enrolled on or accepted, + # before 19. + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + is_parent: true + aa_category: HIGHER + young_person: + age: 19 + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 19 + benunits: + benunit: + members: [claimant, young_person] + households: + household: + members: [claimant, young_person] + output: + is_qualifying_young_person_for_pension_credit: [false, false] + severe_disability_minimum_guarantee_addition: 0 + +- name: A qualifying young person in another benefit unit is ignored for Pension Credit + # Para 2(2)(f) ignores any qualifying young person within reg 4A, not only + # the claimant's own. The young person's parent receives the PIP daily + # living component, so para 2(2)(a) ignores her too. (Housing Benefit counts + # a young person of another family as a non-dependant: HB(SPC) reg 3(2)(a).) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + aa_category: HIGHER + parent: + age: 45 + is_parent: true + pip_dl_category: STANDARD + grandchild: + age: 18 + is_in_non_advanced_education: true + benunits: + claimant_unit: + members: [claimant] + parent_unit: + members: [parent, grandchild] + households: + household: + members: [claimant, parent, grandchild] + output: + severe_disability_minimum_guarantee_addition: [4_474.60, 4_474.60] + +- name: A third adult in the couple's own benefit unit counts unless their presence is ignored + # Supplying is_claimant_or_partner replaces the formula for everyone, so + # it is set for each member. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant_1: + age: 78 + aa_category: HIGHER + is_parent: true + is_claimant_or_partner: true + partner_1: + age: 76 + aa_category: HIGHER + is_parent: true + is_claimant_or_partner: true + adult_1: + age: 45 + is_claimant_or_partner: false + claimant_2: + age: 78 + aa_category: HIGHER + is_parent: true + is_claimant_or_partner: true + partner_2: + age: 76 + aa_category: HIGHER + is_parent: true + is_claimant_or_partner: true + adult_2: + age: 45 + pip_dl_category: STANDARD + is_claimant_or_partner: false + benunits: + couple_1: + members: [claimant_1, partner_1, adult_1] + couple_2: + members: [claimant_2, partner_2, adult_2] + households: + household_1: + members: [claimant_1, partner_1, adult_1] + household_2: + members: [claimant_2, partner_2, adult_2] + output: + severe_disability_minimum_guarantee_addition: [0, 8_949.20] + +- name: One carer's award covers one person, not every benefit unit in the household + # SSCBA 1992 s.70(7): one carer's allowance per carer. Two single pensioners + # on Attendance Allowance share a house with a 17-year-old who receives + # Carer's Allowance. The award is attributed to the elder pensioner only. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + younger: + age: 70 + aa_category: HIGHER + elder: + age: 72 + aa_category: HIGHER + carer: + age: 17 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + younger_unit: + members: [younger] + elder_unit: + members: [elder] + carer_unit: + members: [carer] + households: + household: + members: [younger, elder, carer] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [0, 1, 0] + severe_disability_minimum_guarantee_addition: [4_474.60, 0, 0] + +- name: Two carers in the household cover two benefit units + period: 2026 + absolute_error_margin: 0 + input: + people: + younger: + age: 70 + aa_category: HIGHER + elder: + age: 72 + aa_category: HIGHER + carer_1: + age: 17 + care_hours: 35 + carers_allowance_reported: 1 + carer_2: + age: 16 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + younger_unit: + members: [younger] + elder_unit: + members: [elder] + carer_unit_1: + members: [carer_1] + carer_unit_2: + members: [carer_2] + households: + household: + members: [younger, elder, carer_1, carer_2] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 1, 0, 0] + severe_disability_minimum_guarantee_addition: [0, 0, 0, 0] + +- name: Two single pensioners who each receive Carer's Allowance care for each other + period: 2026 + absolute_error_margin: 0 + input: + people: + first: + age: 72 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + second: + age: 70 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + benunits: + first_unit: + members: [first] + second_unit: + members: [second] + households: + household: + members: [first, second] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 1] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: A resident who has just attained 18 and is not a qualifying young person counts + # Para 1(1)(a)(ii): "no person who has attained the age of 18". + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 75 + aa_category: HIGHER + grandchild: + age: 18 + benunits: + claimant_unit: + members: [claimant] + grandchild_unit: + members: [grandchild] + households: + household: + members: [claimant, grandchild] + output: + is_counted_resident_for_severe_disability_addition: [false, true] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: The addition is never negative, even with more claimant or partner flags than a couple has + # Supplied inputs: three members flagged as claimant or partner, all on + # Attendance Allowance and Carer's Allowance. + period: 2026 + absolute_error_margin: 0 + input: + people: + first: + age: 75 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + is_claimant_or_partner: true + second: + age: 74 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + is_claimant_or_partner: true + third: + age: 73 + aa_category: HIGHER + care_hours: 35 + carers_allowance_reported: 1 + is_claimant_or_partner: true + benunits: + benunit: + members: [first, second, third] + households: + household: + members: [first, second, third] + country: ENGLAND + output: + severe_disability_minimum_guarantee_addition: 0 + +- name: A carer whose own disabled child qualifies is treated as caring for the child + # The daughter receives Carer's Allowance; her child receives the DLA care + # component at the middle rate, so she is attributed to her child, not to + # the grandmother in another benefit unit (each award is for one person). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + aa_category: HIGHER + daughter: + age: 45 + is_parent: true + pip_dl_category: STANDARD + care_hours: 35 + carers_allowance_reported: 1 + child: + age: 10 + dla_sc_category: MIDDLE + benunits: + grandmother_unit: + members: [grandmother] + daughter_unit: + members: [daughter, child] + households: + household: + members: [grandmother, daughter, child] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [0, 0] + severe_disability_minimum_guarantee_addition: [4_474.60, 4_474.60] + +- name: A second carer in a pensioner's unit is attributed to a pensioner in another unit + # Each person has only one carer (SSCBA 1992 s.70(7ZA)), so the second of + # two young carers in the first pensioner's unit cares for the second + # pensioner, as when the carers are in units of their own (next case). + period: 2026 + absolute_error_margin: 0 + input: + people: + first: + age: 80 + aa_category: HIGHER + carer_1: + age: 17 + care_hours: 35 + carers_allowance_reported: 1 + carer_2: + age: 16 + care_hours: 35 + carers_allowance_reported: 1 + second: + age: 78 + aa_category: HIGHER + benunits: + first_unit: + members: [first, carer_1, carer_2] + second_unit: + members: [second] + households: + household: + members: [first, carer_1, carer_2, second] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 1] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: When two benefit units' eldest members are the same age, the one whose eldest member comes first in person order takes a carer + period: 2026 + absolute_error_margin: 0.01 + input: + people: + listed_first: + age: 75 + aa_category: HIGHER + listed_second: + age: 75 + aa_category: HIGHER + carer: + age: 17 + care_hours: 35 + carers_allowance_reported: 1 + benunits: + first_unit: + members: [listed_first] + second_unit: + members: [listed_second] + carer_unit: + members: [carer] + households: + household: + members: [listed_first, listed_second, carer] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 0, 0] + severe_disability_minimum_guarantee_addition: [0, 4_474.60, 0] + +- name: A carer who is the only qualifying member of their own unit cares for someone in another unit + # No one is their own carer. The 17-year-old receives the DLA care + # component at the middle rate and Carer's Allowance; her blind father + # (ignored as a resident) receives no qualifying benefit, so she has no one + # to care for in her own unit and is attributed to the grandmother. + period: 2026 + absolute_error_margin: 0 + input: + people: + grandmother: + age: 80 + aa_category: HIGHER + father: + age: 45 + is_parent: true + is_blind: true + daughter: + age: 17 + dla_sc_category: MIDDLE + care_hours: 35 + carers_allowance_reported: 1 + benunits: + grandmother_unit: + members: [grandmother] + father_unit: + members: [father, daughter] + households: + household: + members: [grandmother, father, daughter] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 0] + severe_disability_minimum_guarantee_addition: [0, 0] + +- name: The two young carers cover both pensioners when each is in their own unit too + # The same people as the case with both carers in the first pensioner's + # unit, with the carers now in benefit units of their own. + period: 2026 + absolute_error_margin: 0 + input: + people: + first: + age: 80 + aa_category: HIGHER + carer_1: + age: 17 + care_hours: 35 + carers_allowance_reported: 1 + carer_2: + age: 16 + care_hours: 35 + carers_allowance_reported: 1 + second: + age: 78 + aa_category: HIGHER + benunits: + first_unit: + members: [first] + carer_unit_1: + members: [carer_1] + carer_unit_2: + members: [carer_2] + second_unit: + members: [second] + households: + household: + members: [first, carer_1, carer_2, second] + country: ENGLAND + output: + num_severe_disability_addition_qualifiers_cared_for: [1, 0, 0, 1] + severe_disability_minimum_guarantee_addition: [0, 0, 0, 0] diff --git a/policyengine_uk/tests/severe_disability_addition_reference.py b/policyengine_uk/tests/severe_disability_addition_reference.py new file mode 100644 index 0000000000..69728dcaa1 --- /dev/null +++ b/policyengine_uk/tests/severe_disability_addition_reference.py @@ -0,0 +1,512 @@ +"""Independent reference: Pension Credit severe disability additional amount. + +Test oracle for test_severe_disability_addition_properties.py. It was written +from the law text and a written specification by a separate agent that did not +see the implementation under test; the only later edit is rule 3 (which unit's +claimant or partner a carer in another benefit unit is treated as caring for), +marked below. + +State Pension Credit Regulations 2002 (SI 2002/1792): + * regulation 6(4) and 6(5) (law/spc_reg6.txt) + * Schedule I, Part I, paragraph 1(1)(a)-(c) (law/spc_sch1_p1.txt) + * Schedule I, Part I, paragraph 2 (law/spc_sch1_p2.txt) + * Schedule I, Part I, paragraph 3 (law/spc_sch1_p3.txt) + +Written from the law text and the differential-test spec only. It does not +import or read the PolicyEngine implementation under test. + +``reference_rates(household)`` returns, for every benefit unit, the number of +single-rate severe disability additional amounts: 0, 1 (reg 6(5)(a), £86.05) +or 2 (reg 6(5)(b), £172.10). + +Input format:: + + household = { + "benunits": {bu_id: {"claimant_or_partner": [pid, ...], + "others": [pid, ...]}}, + "people": {pid: {"age": int, + "qualifying_benefit": bool, + "blind": bool, + "qualifying_young_person": bool, + "receives_carer_benefit": bool}}, + } + +A unit with two ``claimant_or_partner`` entries is a couple; with one, a single +claimant. ``others`` are unit members who are neither claimant nor partner. + +Modelling rules (from the spec; each is applied literally below): + +1. Qualifying benefit. A person satisfies para 1(1)(a)(i), (b)(i) and (c)(i) + iff ``qualifying_benefit``. +2. Residence. Para 1(1)(a)(ii), (b)(ii) and (c)(iii) fail if anyone in the + household other than this unit's claimant/partner is aged 18 or over and is + not a para 2 person. Modelled para 2 persons: qualifying benefit (2(2)(a)), + blind (2(2)(b)), qualifying young person (2(2)(f)). No other exceptions. +3. Carers [as amended]. Nobody cares for themselves, each award is for one + person, and each person has at most one carer. Within their own unit, + carers care for qualifying claimants/partners first and then other + qualifying members (such as a disabled child). Every carer left over cares + for a qualifying claimant/partner of another unit who is not yet cared + for; the units take them in turn, eldest member first. This rule is + implemented as explicit person-to-person matching, independently of the + implementation's counting. +4. Heads. Para 1(1)(a) applies to singles; (b) and then (c) apply to couples. + (c) applies only where (b) does not, and (c)(iv) needs no carer for the + partner to whom (c)(i) applies. If both partners qualify, either may be the + (c)(i) person, and the other must be blind. Then reg 6(5): 2 if (b) is + satisfied and nobody cares for either partner; otherwise 1 if any of (a), + (b) or (c) is satisfied; otherwise 0. + +Not modelled (outside the spec): reg 6(2)-(3) prisoners and members of +religious orders; para 1(2) deemed receipt (backdated awards, payments in lieu, +hospital patients, carer benefit before first payment); para 1(3) 28-week run-on +for regained sight; para 2(2)(c), (d), (e), 2(3)-(7); para 3 (what counts as +"residing with"). Everyone in ``people`` counts as normally residing with every +benefit unit in the household. +""" + +from __future__ import annotations + +from itertools import combinations +from typing import Any, Hashable, Mapping, Sequence + +Person = Mapping[str, Any] +PersonId = Hashable + +# Sch I para 1(1)(a)(ii), (b)(ii), (c)(iii): "no person who has attained the age of 18". +# A person "has attained the age of 18" on their 18th birthday, so the test is age >= 18. +ADULT_AGE = 18 + + +# --------------------------------------------------------------------------- +# Rule 1: the qualifying disability benefit +# --------------------------------------------------------------------------- + + +def qualifies(person: Person) -> bool: + """Sch I para 1(1)(a)(i), (b)(i) and (c)(i): receipt of a qualifying benefit. + + Law, para 1(1)(a)(i): "he is in receipt of attendance allowance , pension age + disability payment , the care component of disability living allowance at the + highest or middle rate [...] the daily living component of personal + independence payment at the standard or enhanced rate [...] or armed forces + independence payment". + + (b)(i) asks that "both partners are in receipt of" the same list and (c)(i) + that "either the claimant or his partner is in receipt of" it. + + Spec rule 1: a person qualifies iff ``qualifying_benefit``. + """ + return bool(person["qualifying_benefit"]) + + +# --------------------------------------------------------------------------- +# Rule 2: the residence (non-dependant) condition +# --------------------------------------------------------------------------- + + +def is_paragraph_2_person(person: Person) -> bool: + """Sch I para 2: residents whose presence is ignored for the residence test. + + Law, para 2(1): "For the purposes of paragraph 1(1)(a)(ii), (b)(ii) and + (c)(iii), this paragraph applies to the persons specified in the following + sub-paragraphs." + + Modelled heads of para 2(2), "A person who--": + (a) "is in receipt of attendance allowance , pension age disability payment , + the care component of disability living allowance at the highest or middle + rate [...] or armed forces independence payment;" -> qualifying_benefit + (b) "is certified as severely sight impaired or blind by a consultant + ophthalmologist;" -> blind + (f) "is a person who is a qualifying young person within the meaning of + regulation 4A or child as defined in section 40 of the 2012 Act." + -> qualifying_young_person + (A section 40 "child" is under 16, so the age test already ignores + them.) + + Spec rule 2: no other exceptions. 2(2)(c)-(e) and 2(3)-(7) are not modelled. + """ + return bool( + person["qualifying_benefit"] # para 2(2)(a) + or person["blind"] # para 2(2)(b) + or person["qualifying_young_person"] # para 2(2)(f) + ) + + +def is_disqualifying_resident(person: Person) -> bool: + """A resident who breaks the residence condition. + + Law, para 1(1)(a)(ii): "no person who has attained the age of 18 is normally + residing with the claimant, nor is the claimant normally residing with such a + person, other than a person to whom paragraph 2 applies". + """ + return person["age"] >= ADULT_AGE and not is_paragraph_2_person(person) + + +def residence_condition_met( + people: Mapping[PersonId, Person], + claimant_or_partner: Sequence[PersonId], +) -> bool: + """Sch I para 1(1)(a)(ii) (single), (b)(ii) first limb and (c)(iii) (couple). + + Law, para 1(1)(a)(ii): "no person who has attained the age of 18 is normally + residing with the claimant, nor is the claimant normally residing with such a + person, other than a person to whom paragraph 2 applies". + + Law, para 1(1)(b)(ii) and (c)(iii): "no person who has attained the age of 18 + is normally residing with the partners, nor are the partners normally residing + with such a person, other than a person to whom paragraph 2 applies". + + Spec rule 2: everyone in the household except this unit's claimant/partner is + tested. That covers "others" in this unit and every member of any other unit. + A partner is never "residing with the partners" in this sense, so an adult + partner without a qualifying benefit does not break the condition. + """ + own = set(claimant_or_partner) + return not any( + is_disqualifying_resident(person) + for pid, person in people.items() + if pid not in own + ) + + +# --------------------------------------------------------------------------- +# Rule 3: who is cared for (carer's allowance, carer support payment, UC carer element) +# --------------------------------------------------------------------------- + + +def _members(unit: Mapping[str, Any]) -> list: + return list(unit["claimant_or_partner"]) + list(unit.get("others", [])) + + +def _best_within_unit(people, unit): + """[Rule 3 as amended, within a unit] Match carers to people they care for. + + Each carer cares for at most one person in their own unit who receives a + qualifying benefit, never themselves; each such person has at most one + carer. Among all such matchings, take one that cares for the most + qualifying claimants/partners, and then the most people. Enumerated by + brute force (units are small). Returns (the number of qualifying + claimant/partners cared for, the number of carers left unmatched). + """ + cp = list(unit["claimant_or_partner"]) + members = _members(unit) + carers = [p for p in members if people[p]["receives_carer_benefit"]] + targets = [p for p in members if qualifies(people[p])] + best_key = None + + def search(i, used, pairs): + nonlocal best_key + if i == len(carers): + covered = {t for _, t in pairs} + key = (len(covered & set(cp)), len(covered)) + if best_key is None or key > best_key: + best_key = key + return + carer = carers[i] + search(i + 1, used, pairs) # this carer cares for no one in the unit + for t in targets: + if t != carer and t not in used: + search(i + 1, used | {t}, pairs + [(carer, t)]) + + search(0, frozenset(), []) + cp_count, total = best_key + return cp_count, len(carers) - total + + +def _feasible(carer_units, demand): + """Can each unit receive demand[unit] carers, one person each, from carers + who are not in that unit? Bipartite matching (augmenting paths).""" + slots = [u for u, n in demand.items() for _ in range(n)] + match = {} + + def assign(c, seen): + for s_i, u in enumerate(slots): + if s_i in seen or carer_units[c] == u: + continue + seen.add(s_i) + if s_i not in match or assign(match[s_i], seen): + match[s_i] = c + return True + return False + + return sum(assign(c, set()) for c in range(len(carer_units))) == len(slots) + + +def household_carer_assignments(household: Mapping[str, Any]) -> dict: + """Every allowed set of cared-for claimant/partners, for each unit. + + The carer conditions are para 1(1)(a)(iii), (b)(ii) second limb, (c)(iv) and + reg 6(5)(b). Each asks whether a person "is entitled to and in receipt of an + allowance under section 70 of the 1992 Act (carer's allowance) or carer support + payment , or has an award of universal credit which includes the carer + element, in respect of caring for" a named person. SSCBA 1992 s.70(7): "No + person shall be entitled for the same day to (a) more than one allowance + under this section", so each award is for one person. + + Rule 3 [as amended; person-to-person matching, not the implementation's + counting]: + * Nobody cares for themselves, and each person has at most one carer. + * Within their own unit, carers care for qualifying claimants/partners + first and then other qualifying members (such as a disabled child), + as many as possible (``_best_within_unit``). + * Every carer left over cares for a qualifying claimant/partner of + another unit who is not yet cared for. The units, eldest member first + (ties in person order, the order people are listed in the input), + each take the most such carers that can still be placed without + taking any from an earlier unit (``_feasible``). + + Rule 3 does not say which partner a carer takes when both qualify and + neither is yet cared for. This returns every allowed set, and + ``reference_rates`` asserts that the answer is the same for all of them. + """ + people = household["people"] + units = household["benunits"] + position = {pid: i for i, pid in enumerate(people)} + qualifying, cared_within, carer_units = {}, {}, [] + for bu_id, unit in units.items(): + qualifying[bu_id] = [ + p for p in unit["claimant_or_partner"] if qualifies(people[p]) + ] + cp_count, left_over = _best_within_unit(people, unit) + cared_within[bu_id] = cp_count + carer_units += [bu_id] * left_over + + def eldest_key(bu_id): + members = _members(units[bu_id]) + oldest = max(people[p]["age"] for p in members) + first = min(position[p] for p in members if people[p]["age"] == oldest) + return (-oldest, first) + + from_other_units = {} + for bu_id in sorted(units, key=eldest_key): + needed = len(qualifying[bu_id]) - cared_within[bu_id] + for k in range(needed, -1, -1): + if _feasible(carer_units, {**from_other_units, bu_id: k}): + from_other_units[bu_id] = k + break + + result = {} + for bu_id, unit in units.items(): + cared_count = cared_within[bu_id] + from_other_units[bu_id] + cp = list(unit["claimant_or_partner"]) + # A claimant/partner caring for a qualifying partner fixes who is cared + # for; otherwise any of the qualifying claimant/partners may be. + must = frozenset( + other + for p in cp + if people[p]["receives_carer_benefit"] + for other in cp + if other != p and qualifies(people[other]) + ) + if len(must) > cared_count: + must = frozenset() + rest = [q for q in qualifying[bu_id] if q not in must] + result[bu_id] = [ + must | frozenset(choice) + for choice in combinations(rest, cared_count - len(must)) + ] + return result + + +# --------------------------------------------------------------------------- +# Rule 4: the three heads of para 1(1) and reg 6(5) +# --------------------------------------------------------------------------- + + +def head_a_satisfied( + people: Mapping[PersonId, Person], + claimant: PersonId, + residence_ok: bool, + cared_for: frozenset, +) -> bool: + """Sch I para 1(1)(a): "in the case of a claimant who has no partner--" + + (i) "he is in receipt of attendance allowance , [...] or armed forces + independence payment; and" + (ii) "no person who has attained the age of 18 is normally residing with the + claimant, [...] other than a person to whom paragraph 2 applies; and" + (iii) "no person is entitled to and in receipt of an allowance under section 70 + of the 1992 Act (carer's allowance) or carer support payment , or has an + award of universal credit which includes the carer element, in respect of + caring for him;" + """ + return ( + qualifies(people[claimant]) # (a)(i) + and residence_ok # (a)(ii) + and claimant not in cared_for # (a)(iii) + ) + + +def head_b_satisfied( + people: Mapping[PersonId, Person], + partners: Sequence[PersonId], + residence_ok: bool, + cared_for: frozenset, +) -> bool: + """Sch I para 1(1)(b): "in the case of a claimant who has a partner--" + + (i) "both partners are in receipt of attendance allowance , [...] or armed + forces independence payment; and" + (ii) "no person who has attained the age of 18 is normally residing with the + partners, [...] other than a person to whom paragraph 2 applies; and + either a person is entitled to, and in receipt of, an allowance under + section 70 of the 1992 Act or carer support payment , or has an award of + universal credit which includes the carer element, in respect of caring + for one only of the partners or, as the case may be, no person is + entitled to, and in receipt of, such an allowance [...] in respect of + caring for either partner;" + + The carer limb of (b)(ii) holds when zero or one partner is cared for. It + fails only when both are. + """ + both_qualify = all(qualifies(people[pid]) for pid in partners) # (b)(i) + partners_cared_for = sum(1 for pid in partners if pid in cared_for) + carer_limb = partners_cared_for <= 1 # (b)(ii): "one only" or "either" (none) + return both_qualify and residence_ok and carer_limb + + +def head_c_satisfied( + people: Mapping[PersonId, Person], + partners: Sequence[PersonId], + residence_ok: bool, + cared_for: frozenset, +) -> bool: + """Sch I para 1(1)(c): "in the case of a claimant who has a partner and to whom + head (b) does not apply--" + + (i) "either the claimant or his partner is in receipt of attendance + allowance , [...] or armed forces independence payment; and" + (ii) "the other partner is certified as severely sight impaired or blind by a + consultant ophthalmologist; and" + (iii) "no person who has attained the age of 18 is normally residing with the + partners, [...] other than a person to whom paragraph 2 applies; and" + (iv) "no person is entitled to and in receipt of an allowance under section 70 + of the 1992 Act or carer support payment , or has an award of universal + credit which includes the carer element, in respect of caring for the + person to whom head (c) (i) above applies." + + The caller checks "to whom head (b) does not apply". Where both partners + qualify, either may be the (c)(i) person (spec rule 4), so (c) holds if any + ordered choice works. + """ + first, second = partners + for c_i_person, other_partner in ((first, second), (second, first)): + if ( + qualifies(people[c_i_person]) # (c)(i) + and people[other_partner]["blind"] # (c)(ii) + and residence_ok # (c)(iii) + and c_i_person not in cared_for # (c)(iv) + ): + return True + return False + + +def rates_for_unit( + people: Mapping[PersonId, Person], + claimant_or_partner: Sequence[PersonId], + cared_for: frozenset, +) -> int: + """Reg 6(4) and 6(5) for one benefit unit and one carer assignment. + + Law, reg 6(4): "Except in a case to which paragraph (3) applies, an amount + additional to that prescribed in paragraph (1) shall be applicable under + paragraph (5) if the claimant is treated as being a severely disabled person in + accordance with paragraph 1 of Part I of Schedule I." + + Law, reg 6(5): "The additional amount applicable is-- + (a) except where paragraph (b) applies, £86.05 per week if paragraph 1(1)(a), + (b) or (c) of Part I of Schedule I is satisfied; or + (b) £172.10 per week if paragraph 1(1)(b) of Part I of Schedule I is satisfied + otherwise than by virtue of paragraph 1(2)(b) of that Part and no one is + entitled to and in receipt of an allowance under section 70 of the 1992 + Act or carer support payment , or has an award of universal credit which + includes the carer element under regulation 29 of the Universal Credit + Regulations 2013, in respect of caring for either partner." + + The 1(2)(b) proviso (hospital patients deemed in receipt) is outside the model: + ``qualifying_benefit`` means actual receipt, so the proviso always holds. Reg + 6(3) (prisoners, religious orders) is not modelled either. + """ + residence_ok = residence_condition_met(people, claimant_or_partner) + + if len(claimant_or_partner) == 1: + (claimant,) = claimant_or_partner + return 1 if head_a_satisfied(people, claimant, residence_ok, cared_for) else 0 + + partners = list(claimant_or_partner) + b = head_b_satisfied(people, partners, residence_ok, cared_for) + # (c) applies only "to whom head (b) does not apply". + c = (not b) and head_c_satisfied(people, partners, residence_ok, cared_for) + nobody_cares_for_either = not any(pid in cared_for for pid in partners) + if b and nobody_cares_for_either: + return 2 # reg 6(5)(b) + if b or c: + return 1 # reg 6(5)(a) + return 0 + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +_PERSON_FIELDS = ( + "age", + "qualifying_benefit", + "blind", + "qualifying_young_person", + "receives_carer_benefit", +) + + +def _validate(household: Mapping[str, Any]) -> None: + people = household["people"] + seen: dict = {} + for bu_id, unit in household["benunits"].items(): + cp = list(unit["claimant_or_partner"]) + if len(cp) not in (1, 2): + raise ValueError( + f"benefit unit {bu_id!r} needs 1 or 2 claimant_or_partner, got {len(cp)}" + ) + for pid in cp + list(unit.get("others", [])): + if pid not in people: + raise ValueError(f"benefit unit {bu_id!r} names unknown person {pid!r}") + if pid in seen: + raise ValueError( + f"person {pid!r} appears in benefit units {seen[pid]!r} and {bu_id!r}" + ) + seen[pid] = bu_id + for pid, person in people.items(): + missing = [f for f in _PERSON_FIELDS if f not in person] + if missing: + raise ValueError(f"person {pid!r} is missing {missing}") + + +def reference_rates(household: Mapping[str, Any]) -> dict: + """Number of single-rate SDA amounts (0, 1 or 2) for every benefit unit. + + The household is everyone in ``household["people"]``. Each benefit unit is + tested against every other member of the household (spec rules 2 and 3). + """ + _validate(household) + people = household["people"] + result = {} + assignments = household_carer_assignments(household) + for bu_id, unit in household["benunits"].items(): + claimant_or_partner = list(unit["claimant_or_partner"]) + outcomes = { + rates_for_unit(people, claimant_or_partner, cared_for) + for cared_for in assignments[bu_id] + } + # Rule 3 leaves open which qualifying partner an outside carer takes. The + # law makes that choice immaterial: (b) depends only on how many partners + # are cared for; (c) can be reached with the residence condition met only + # when both partners are cared for, and then (c)(iv) fails for either choice. + # Fail loudly if that reasoning is ever wrong. + if len(outcomes) != 1: + raise AssertionError( + f"benefit unit {bu_id!r}: result depends on carer assignment: {outcomes}" + ) + result[bu_id] = outcomes.pop() + return result diff --git a/policyengine_uk/tests/test_severe_disability_addition_properties.py b/policyengine_uk/tests/test_severe_disability_addition_properties.py new file mode 100644 index 0000000000..c03142839b --- /dev/null +++ b/policyengine_uk/tests/test_severe_disability_addition_properties.py @@ -0,0 +1,281 @@ +"""Property-based tests for the Pension Credit severe disability addition. + +State Pension Credit Regulations 2002 reg 6(4)-(5) and Sch I paras 1-2, as +encoded in severe_disability_minimum_guarantee_addition and the variables it +reads. Households are generated with a pension-age benefit unit and up to two +other benefit units, each single or a couple and possibly with dependants, +each person drawn with a disability benefit (or none), blindness, a carer +benefit and, for 16- to 19-year-olds, non-advanced education. is_claimant_or_partner is +set from the generated roles, as the survey data supply it. + +Invariants, for every benefit unit in every generated household: + +1. Differential: the number of rates equals that of an independent reference + implementation written from the law text + (severe_disability_addition_reference.py). +2. Structural: the addition is 0, 1 or 2 times the weekly rate times 52, and + 2 only for a couple. The qualifying-benefit flag matches the Sch I para + 1(1)(a)(i) list for the drawn benefit categories. +3. Residence, metamorphic: adding another adult resident who is not a para 2 + person removes the addition for every other benefit unit. Adding a resident + whose presence is ignored and who receives no carer benefit leaves every + existing unit's addition unchanged if the resident does not qualify (a + blind person, a qualifying young person or anyone under 18), and never + lowers it if they do (a qualifying-benefit recipient, to whom a carer in + the household may then be attributed instead). +4. Carers, metamorphic: giving someone a carer benefit never increases any + unit's addition. +5. Symmetry: swapping the drawn attributes of a couple's two partners leaves + their addition unchanged. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from severe_disability_addition_reference import reference_rates + +from policyengine_uk import Simulation + +YEAR = 2026 +WEEKS_IN_YEAR = 52 +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Drawn benefit category -> whether Sch I para 1(1)(a)(i) lists it. +BENEFITS = { + "none": ({}, False), + "aa_lower": ({"aa_category": "LOWER"}, True), + "aa_higher": ({"aa_category": "HIGHER"}, True), + "dla_care_lowest": ({"dla_sc_category": "LOWER"}, False), + "dla_care_middle": ({"dla_sc_category": "MIDDLE"}, True), + "dla_care_highest": ({"dla_sc_category": "HIGHER"}, True), + "pip_dl_standard": ({"pip_dl_category": "STANDARD"}, True), + "pip_dl_enhanced": ({"pip_dl_category": "ENHANCED"}, True), +} + + +@st.composite +def person(draw, min_age, max_age): + age = draw(st.integers(min_age, max_age)) + return dict( + age=age, + benefit=draw(st.sampled_from(sorted(BENEFITS))), + blind=draw(st.booleans()), + carer=draw(st.booleans()), + # Only meaningful at 16 to 19 (reg 4A); started before 19. + education=16 <= age <= 19 and draw(st.booleans()), + ) + + +@st.composite +def household(draw): + focal = dict( + claimants=[draw(person(66, 95)) for _ in range(draw(st.integers(1, 2)))], + dependants=draw(st.lists(person(0, 19), max_size=2)), + ) + others = [ + dict( + claimants=[draw(person(18, 90)) for _ in range(draw(st.integers(1, 2)))], + dependants=draw(st.lists(person(0, 19), max_size=1)), + ) + for _ in range(draw(st.integers(0, 2))) + ] + return [focal] + others + + +def build(households): + """One Simulation for many households; returns the situation and a map.""" + people, benunits, hh = {}, {}, {} + layout = [] + for h, units in enumerate(households): + hh_members, hh_layout = [], [] + for b, unit in enumerate(units): + names = [] + for role, group in (("cp", unit["claimants"]), ("dep", unit["dependants"])): + for j, p in enumerate(group): + name = f"h{h}b{b}{role}{j}" + record = { + "age": {YEAR: p["age"]}, + "is_blind": {YEAR: p["blind"]}, + "is_claimant_or_partner": {YEAR: role == "cp"}, + "is_in_non_advanced_education": {YEAR: p["education"]}, + "age_started_or_accepted_current_education_or_training": { + YEAR: min(p["age"], 18) + }, + "care_hours": {YEAR: 35 if p["carer"] else 0}, + "carers_allowance_reported": {YEAR: 1 if p["carer"] else 0}, + } + for variable, value in BENEFITS[p["benefit"]][0].items(): + record[variable] = {YEAR: value} + people[name] = record + names.append((name, role, p)) + benunits[f"h{h}b{b}"] = {"members": [n for n, _, _ in names]} + hh_members += [n for n, _, _ in names] + hh_layout.append(names) + hh[f"h{h}"] = {"members": hh_members, "country": {YEAR: "ENGLAND"}} + layout.append(hh_layout) + return {"people": people, "benunits": benunits, "households": hh}, layout + + +def simulate(households): + situation, layout = build(households) + simulation = Simulation(situation=situation) + + def calc(variable): + return np.asarray(simulation.calculate(variable, YEAR)) + + rate = float( + simulation.tax_benefit_system.parameters( + str(YEAR) + ).gov.dwp.pension_credit.guarantee_credit.severe_disability.addition + ) + return { + "layout": layout, + "rates": calc("severe_disability_minimum_guarantee_addition") + / (rate * WEEKS_IN_YEAR), + "qualifies": calc("receives_severe_disability_addition_qualifying_benefit"), + "carer": calc("receives_carer_benefit"), + "is_couple": calc("is_couple"), + } + + +def rates_by_household(result): + """Split the benefit-unit rates array into one list per household.""" + out, i = [], 0 + for hh_layout in result["layout"]: + out.append(list(result["rates"][i : i + len(hh_layout)])) + i += len(hh_layout) + return out + + +def reference_household(result, h): + """The reference's input for household h, from the simulation's inputs.""" + person_index = {} + k = 0 + for layout in result["layout"]: + for names in layout: + for name, _, _ in names: + person_index[name] = k + k += 1 + people, benunits = {}, {} + for b, names in enumerate(result["layout"][h]): + benunits[b] = {"claimant_or_partner": [], "others": []} + for name, role, p in names: + i = person_index[name] + people[name] = { + "age": p["age"], + "qualifying_benefit": BENEFITS[p["benefit"]][1], + "blind": p["blind"], + # Reg 4A, from the inputs: 16 to 19, in non-advanced + # education started before 19, no benefits in own right. + "qualifying_young_person": 16 <= p["age"] <= 19 and p["education"], + "receives_carer_benefit": bool(result["carer"][i]), + } + key = "claimant_or_partner" if role == "cp" else "others" + benunits[b][key].append(name) + return {"people": people, "benunits": benunits} + + +@PROPERTY_SETTINGS +@given(st.lists(household(), min_size=1, max_size=6)) +def test_addition_matches_the_reference_and_is_bounded(households): + result = simulate(households) + rates = rates_by_household(result) + np.testing.assert_allclose(result["rates"], np.round(result["rates"]), atol=1e-9) + assert np.all(np.isin(np.round(result["rates"]), [0, 1, 2])) + assert np.all(np.round(result["rates"])[~result["is_couple"].astype(bool)] <= 1) + # The qualifying-benefit flag follows the para 1(1)(a)(i) list. + expected_qualifies = [ + BENEFITS[p["benefit"]][1] + for layout in result["layout"] + for names in layout + for _, _, p in names + ] + np.testing.assert_array_equal(result["qualifies"], expected_qualifies) + for h, household_units in enumerate(households): + reference = reference_household(result, h) + expected = reference_rates(reference) + actual = [int(round(x)) for x in rates[h]] + assert actual == [expected[b] for b in range(len(household_units))], ( + reference, + actual, + ) + + +# (resident, whether they receive a para 1(1)(a)(i) qualifying benefit) +IGNORED_RESIDENTS = [ + ( + dict( + age=50, benefit="pip_dl_standard", blind=False, carer=False, education=False + ), + True, + ), + (dict(age=50, benefit="aa_lower", blind=False, carer=False, education=False), True), + (dict(age=50, benefit="none", blind=True, carer=False, education=False), False), + (dict(age=18, benefit="none", blind=False, carer=False, education=True), False), + (dict(age=17, benefit="none", blind=False, carer=False, education=False), False), +] +COUNTED_RESIDENT = dict( + age=30, benefit="none", blind=False, carer=False, education=False +) + + +@PROPERTY_SETTINGS +@given(household(), st.sampled_from(range(len(IGNORED_RESIDENTS)))) +def test_residence_condition_is_metamorphic(original, ignored_index): + ignored, ignored_qualifies = IGNORED_RESIDENTS[ignored_index] + with_ignored = original + [dict(claimants=[ignored], dependants=[])] + with_counted = original + [dict(claimants=[COUNTED_RESIDENT], dependants=[])] + result = simulate([original, with_ignored, with_counted]) + base, ignored_rates, counted_rates = rates_by_household(result) + n = len(original) + if ignored_qualifies: + assert all(a >= b - 1e-9 for a, b in zip(ignored_rates[:n], base)) + else: + np.testing.assert_allclose(ignored_rates[:n], base, atol=1e-9) + np.testing.assert_allclose(counted_rates[:n], 0, atol=1e-9) + + +@PROPERTY_SETTINGS +@given(household(), st.data()) +def test_a_carer_benefit_never_increases_the_addition(original, data): + candidates = [ + (u, group, j) + for u, unit in enumerate(original) + for group in ("claimants", "dependants") + for j, p in enumerate(unit[group]) + if not p["carer"] and p["age"] >= 16 + ] + if not candidates: + return + u, group, j = data.draw(st.sampled_from(candidates)) + changed = [ + dict( + claimants=[dict(p) for p in unit["claimants"]], + dependants=[dict(p) for p in unit["dependants"]], + ) + for unit in original + ] + changed[u][group][j]["carer"] = True + result = simulate([original, changed]) + before, after = rates_by_household(result) + assert all(a <= b + 1e-9 for a, b in zip(after, before)), (before, after) + + +@PROPERTY_SETTINGS +@given(household()) +def test_swapping_partners_leaves_a_couples_addition_unchanged(original): + focal = original[0] + if len(focal["claimants"]) != 2: + return + first, second = focal["claimants"] + keys = ["benefit", "blind", "carer"] + swapped_first = {**first, **{k: second[k] for k in keys}} + swapped_second = {**second, **{k: first[k] for k in keys}} + swapped = [dict(focal, claimants=[swapped_first, swapped_second])] + original[1:] + result = simulate([original, swapped]) + before, after = rates_by_household(result) + assert before[0] == after[0] diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_counted_resident_for_severe_disability_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_counted_resident_for_severe_disability_addition.py new file mode 100644 index 0000000000..6387fde7a8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_counted_resident_for_severe_disability_addition.py @@ -0,0 +1,43 @@ +from policyengine_uk.model_api import * + + +class is_counted_resident_for_severe_disability_addition(Variable): + value_type = bool + entity = Person + label = ( + "Counts as another resident for the Pension Credit severe disability addition" + ) + documentation = ( + "Aged 18 or over and not a person whose presence is ignored under " + "State Pension Credit Regulations 2002 Sch I para 2. Modelled " + "exceptions: a recipient of a qualifying disability benefit (para " + "2(2)(a)), a person certified blind or severely sight impaired (para " + "2(2)(b)), and a qualifying young person within regulation 4A (para " + "2(2)(f); a child under 16 is below the age limit anyway). Not " + "modelled, because the data do not identify them: sight regained " + "within 28 weeks (2(2)(c)), a carer engaged by a charity and their " + "partner (2(2)(d)-(e)), a carer in their first 12 weeks in the " + "household (2(3)-(4)), commercial lodgers and landlords who are not " + "close relatives (2(5)), joint occupiers and their partners (2(6)-(7)), " + "and people who share only a bathroom, lavatory or communal area or " + "are separately liable to the landlord (para 3). Each unmodelled " + "exception ignores a person, so leaving it out can only withhold the " + "addition. Whether the person resides with a given claimant is decided " + "by meets_severe_disability_addition_residence_condition." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/3", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.pension_credit.guarantee_credit + adult = person("age", period) >= p.severe_disability.other_resident_age_limit + ignored = ( + person("receives_severe_disability_addition_qualifying_benefit", period) + | person("is_blind", period) + | person("is_qualifying_young_person_for_pension_credit", period) + ) + return adult & ~ignored diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/meets_severe_disability_addition_residence_condition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/meets_severe_disability_addition_residence_condition.py new file mode 100644 index 0000000000..a140b66627 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/meets_severe_disability_addition_residence_condition.py @@ -0,0 +1,31 @@ +from policyengine_uk.model_api import * + + +class meets_severe_disability_addition_residence_condition(Variable): + value_type = bool + entity = BenUnit + label = "Meets the Pension Credit severe disability addition residence condition" + documentation = ( + "No person aged 18 or over, other than one whose presence is ignored, " + "normally resides with the claimant (and partner). Everyone else in the " + "household counts, including a member of the benefit unit who is " + "neither the claimant, the partner nor a qualifying young person. " + "The pension-age Housing Benefit severe disability premium applies a " + "similar test to non-dependants (HB(SPC) Regs 2006 Sch 3 para " + "6(2)(a)(ii), (b)(iii) and (6)), which also counts a young person of " + "another family." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6", + ) + + def formula(benunit, period, parameters): + person = benunit.members + counted = person("is_counted_resident_for_severe_disability_addition", period) + claimant_or_partner = person("is_claimant_or_partner", period) + counted_in_household = benunit.max(person.household.sum(counted)) + counted_claimants_or_partners = benunit.sum(counted & claimant_or_partner) + return counted_in_household == counted_claimants_or_partners diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/num_severe_disability_addition_qualifiers_cared_for.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/num_severe_disability_addition_qualifiers_cared_for.py new file mode 100644 index 0000000000..da7e25d637 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/num_severe_disability_addition_qualifiers_cared_for.py @@ -0,0 +1,102 @@ +from policyengine_uk.model_api import * + + +class num_severe_disability_addition_qualifiers_cared_for(Variable): + value_type = int + entity = BenUnit + label = ( + "Qualifying claimants or partners with a carer benefit paid for caring for them" + ) + documentation = ( + "How many of the claimant and partner who receive a qualifying " + "disability benefit have someone entitled to and in receipt of " + "Carer's Allowance or Carer Support Payment in respect of caring for " + "them. The data do not say whom a carer cares for. A carer benefit " + "needs the person cared for to receive a qualifying disability " + "benefit, and each award is for one person, who has only one such " + "carer (SSCBA 1992 s.70(7) and (7ZA)), so each carer is attributed as " + "follows. No one is their own carer. A carer cares for someone in " + "their own benefit unit who receives a qualifying benefit if there is " + "one: a qualifying claimant or partner first, then another member such " + "as a disabled child, each such person having one carer. Every other " + "carer in the household, including one left over in their own unit, " + "is allocated to a qualifying claimant or partner not yet cared for in " + "another benefit unit, one each, taking the units in descending order " + "of their eldest member's age (ties in person order, the order people " + "are listed in the input or dataset). Carers outside the household, " + "and Universal Credit awards that include the carer element, are not " + "counted. Reading Universal Credit here would be circular, because the " + "Universal Credit non-dependant deduction exemption reads Pension " + "Credit; a Universal Credit carer in the household already stops the " + "addition through the residence condition unless their presence is " + "ignored. Receipt is receives_carer_benefit, so it inherits " + "carers_allowance, which does not apply the overlapping-benefit rule " + "that stops Carer's Allowance being paid alongside a higher State " + "Pension." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/6", + "https://www.legislation.gov.uk/ukpga/1992/4/section/70", + ) + + def formula(benunit, period, parameters): + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + receives = person( + "receives_severe_disability_addition_qualifying_benefit", period + ) + qualifies = claimant_or_partner & receives + dependant_receives = ~claimant_or_partner & receives + carer = person("receives_carer_benefit", period) + qualifying = benunit.sum(qualifies) + receiving_dependants = benunit.sum(dependant_receives) + # People in the carer's own unit the carer could be caring for. + qualifiers_in_own_unit = benunit.project(qualifying) - qualifies + dependants_in_own_unit = ( + benunit.project(receiving_dependants) - dependant_receives + ) + cares_for_a_qualifier = carer & (qualifiers_in_own_unit > 0) + cares_within_unit = carer & ( + (qualifiers_in_own_unit + dependants_in_own_unit) > 0 + ) + # Qualifying claimants and partners first, one carer each. + cared_for_within = min_(qualifying, benunit.sum(cares_for_a_qualifier)) + not_yet_cared_for = qualifying - cared_for_within + placed_within = min_( + benunit.sum(cares_within_unit), qualifying + receiving_dependants + ) + # Every other carer benefit is for someone in another unit. + outside_here = benunit.sum(carer) - placed_within + outside_members = where( + person.get_rank(person.benunit, -person("age", period)) == 0, + benunit.project(outside_here), + 0, + ) + outside_in_household = benunit.max(person.household.sum(outside_members)) + # Order the household's units by their eldest member (ties by person order) + # and give each unit, in turn, carers from the other units. + age = person("age", period) + representative = person.get_rank(person.benunit, -age) == 0 + order = person.get_rank(person.household, -age, condition=representative) + unit_order = benunit.max(where(representative, order, -1)) + allocated_in_household = np.zeros_like(outside_here) + cared_for_from_other_units = np.zeros_like(outside_here) + for rank in range(int(np.max(unit_order, initial=-1)) + 1): + available = max_( + 0, + min_( + outside_in_household - outside_here, + outside_in_household - allocated_in_household, + ), + ) + allocated = where(unit_order == rank, min_(not_yet_cared_for, available), 0) + cared_for_from_other_units = cared_for_from_other_units + allocated + allocated_by_representative = where( + representative, benunit.project(allocated), 0 + ) + allocated_in_household = allocated_in_household + benunit.max( + person.household.sum(allocated_by_representative) + ) + return cared_for_within + cared_for_from_other_units diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/receives_severe_disability_addition_qualifying_benefit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/receives_severe_disability_addition_qualifying_benefit.py new file mode 100644 index 0000000000..bbd168150c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/receives_severe_disability_addition_qualifying_benefit.py @@ -0,0 +1,26 @@ +from policyengine_uk.model_api import * + + +class receives_severe_disability_addition_qualifying_benefit(Variable): + value_type = bool + entity = Person + label = "Receives a benefit qualifying for the Pension Credit severe disability addition" + documentation = ( + "Receives Attendance Allowance, the care component of Disability " + "Living Allowance at the middle or highest rate, the daily living " + "component of Personal Independence Payment, or Armed Forces " + "Independence Payment. The same list qualifies a claimant or partner " + "and makes another resident's presence ignored. The Scottish " + "equivalents (Pension Age Disability Payment, Adult Disability " + "Payment, Scottish Adult DLA) are not modelled separately." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.pension_credit.guarantee_credit + return add(person, period, p.severe_disability.relevant_benefits) > 0 diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py index 212c027744..9e8d299a53 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/severe_disability_minimum_guarantee_addition.py @@ -3,6 +3,33 @@ class severe_disability_minimum_guarantee_addition(Variable): label = "Severe disability-related increase" + documentation = ( + "The additional amount for a severely disabled claimant (State Pension " + "Credit Regulations 2002 reg 6(4)-(5) and Sch I paras 1-3). A single " + "claimant qualifies if they receive a qualifying disability benefit, " + "no other resident aged 18 or over counts, and no carer benefit is " + "paid for caring for them (para 1(1)(a)). A couple qualify if both " + "partners receive a qualifying benefit, no other resident counts, and " + "a carer benefit is paid for at most one of them (para 1(1)(b)); or, " + "failing that, if one partner receives a qualifying benefit, the other " + "is certified blind or severely sight impaired, no other resident " + "counts, and no carer benefit is paid for the first (para 1(1)(c)). " + "The amount is two rates for a couple under (b) with no carer benefit " + "paid for either partner, and one rate otherwise (reg 6(5)). The " + "pension-age Housing Benefit severe disability premium (HB(SPC) Regs " + "2006 Sch 3 paras 6 and 12(1)) has nearly the same conditions and the " + "same amounts, but counts an 18- or 19-year-old young person of " + "another family as a non-dependant, and needs the claimant, not " + "either partner, to qualify where the other partner is blind. Not " + "modelled: the hospital-patient deeming (para 1(2)(b)-(bd)) " + "and backdating (para 1(2)(a), (c)); sight regained within 28 weeks " + "(para 1(3)); polygamous marriages; and the unmodelled para 2 and 3 " + "exceptions and carer attribution described on " + "is_counted_resident_for_severe_disability_addition and " + "num_severe_disability_addition_qualifiers_cared_for. Blindness " + "(is_blind) is not in the survey data, so in microsimulation the " + "blind-partner route never applies and a blind resident is counted." + ) entity = BenUnit definition_period = YEAR value_type = float @@ -10,49 +37,37 @@ class severe_disability_minimum_guarantee_addition(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/1", "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/2", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/I/paragraph/3", "https://www.legislation.gov.uk/uksi/2002/1792/regulation/6", ) def formula(benunit, period, parameters): - # Count qualifying claimants/partners; children and qualifying young - # people are ignored under Sch I para 2(2)(f). - # Sch I para 1(1): a single claimant qualifies if no carer benefit is - # paid for them (a). A couple qualifies if both partners receive a - # qualifying benefit and a carer benefit is paid for at most one of - # them (b), or if one does, the other is blind and no carer benefit is - # paid for the first (c). Reg 6(5): the double amount applies under (b) - # when no carer benefit is paid for either partner, otherwise the - # single amount. - # Retained simplifications: the household-level non-dependant - # residence test is not modelled; the person cared for is not - # identified, so each carer benefit paid in the benefit unit is taken - # to be for a different claimant or partner, never for its recipient - # (so a qualifying claimant's own carer benefit does not bar them); - # a UC carer element paid to someone outside the benefit unit is not - # seen; underlying entitlement alone does not count. severe_disability = parameters( period ).gov.dwp.pension_credit.guarantee_credit.severe_disability person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) - qualifies = add(person, period, severe_disability.relevant_benefits) > 0 - qualifier = claimant_or_partner & qualifies - qualifying = benunit.sum(qualifier) + qualifies = person( + "receives_severe_disability_addition_qualifying_benefit", period + ) + qualifying = benunit.sum(claimant_or_partner & qualifies) other_partner_blind = benunit.any( claimant_or_partner & ~qualifies & person("is_blind", period) ) - receives_carer_benefit = person("receives_carer_benefit", period) - carers = benunit.sum(receives_carer_benefit) - # Heads (a) and (c): carer benefits paid to anyone but the qualifying - # claimant or partner. - carers_for_qualifier = benunit.sum(receives_carer_benefit & ~qualifier) - amounts = where( - benunit("is_couple", period), - where( - qualifying >= 2, - max_(2 - carers, 0), - (qualifying == 1) & other_partner_blind & (carers_for_qualifier == 0), - ), - (qualifying >= 1) & (carers_for_qualifier == 0), + cared_for = benunit( + "num_severe_disability_addition_qualifiers_cared_for", period + ) + # Para 1(1)(b) with reg 6(5): two rates, less one for each partner a + # carer benefit is paid for (none if paid for both). Para 1(1)(c): one + # rate. Para 1(1)(a): one rate. + couple_rates = where( + qualifying >= 2, + max_(2 - cared_for, 0), + (qualifying == 1) & other_partner_blind & (cared_for == 0), + ) + single_rates = (qualifying >= 1) & (cared_for == 0) + rates = where(benunit("is_couple", period), couple_rates, single_rates) + resides_alone = benunit( + "meets_severe_disability_addition_residence_condition", period ) - return amounts * severe_disability.addition * WEEKS_IN_YEAR + return rates * resides_alone * severe_disability.addition * WEEKS_IN_YEAR