From a9e1b4ff42f997e069d6fc09f6cd7cf6a6f27415 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:23:50 -0400 Subject: [PATCH 1/3] Give each benefit-unit member aged 16 or over a bedroom in the LHA size criteria Universal Credit: a qualifying young person the renter is responsible for has a bedroom (UC Regs 2013 Sch 4 para 10(1)(b)), and any other member aged 16 or over is a non-dependant with one (para 9(2), 10(1)(c)), except a qualifying young person no one is responsible for (para 9(2)(g)). Housing Benefit: every member aged 16 or over other than the claimant or partner is a person who is not a child (HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b); reg 2(1)). Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 39 +++++++++++++++---- .../housing_benefit_LHA_allowed_bedrooms.py | 24 +++++++++--- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 14d4d0b3e7..4f91b49750 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -42,25 +42,50 @@ class LHA_allowed_bedrooms(Variable): label = "The number of bedrooms covered by LHA for the benefit unit" documentation = ( "Universal Credit size criteria: one bedroom for the renter or " - "couple, one for each non-dependant aged 16 or over, and the " - "children's bedrooms. Joint tenants and other sharers of the rent, " - "boarders and lodgers are not non-dependants, so they add no bedroom " - "to anyone's entitlement; only the household head's family has " - "non-dependants (see is_non_dependant_of_household_head)." + "couple, one for each member of the benefit unit aged 16 or over who " + "is not the claimant or partner, one for each non-dependant aged 16 or " + "over, and the children's bedrooms. A qualifying young person the " + "renter is responsible for has their own bedroom; any other member " + "aged 16 or over is a non-dependant and has one too, except a " + "qualifying young person no one is responsible for, such as one " + "looked after by a local authority. Joint tenants and other sharers " + "of the rent, boarders and lodgers are not non-dependants, so they " + "add no bedroom to anyone's entitlement; only the household head's " + "family has non-dependants from other families (see " + "is_non_dependant_of_household_head)." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/10", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", ) def formula(benunit, period, parameters): person = benunit.members + # A child is a person under 16 (WRA 2012 s.40). aged_16_or_over = person("age", period) >= 16 - # UC Regs 2013 Sch 4 para 10(1)(c): a non-dependant who is not a child. + family_member = aged_16_or_over & ~person("is_claimant_or_partner", period) + # UC Regs 2013 Sch 4 para 10(1)(b): a qualifying young person for + # whom the renter is responsible (regs 4 and 5). + qualifying_young_person = family_member & person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) + # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a + # non-dependant, unless they are a qualifying young person for whom + # no one is responsible (para 9(2)(g); reg 4(6)). + family_non_dependant = family_member & ~person( + "is_qualifying_young_person_for_universal_credit", period + ) + family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) + # Para 10(1)(c): a non-dependant from another family who is not a + # child. non_dependant = aged_16_or_over & person( "is_non_dependant_of_household_head", period ) head_family = benunit.any(person("is_household_head", period)) non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) - return 1 + non_dependants + bedrooms_for_children(benunit, period) + return ( + 1 + family_rooms + non_dependants + bedrooms_for_children(benunit, period) + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py index 005d00c653..12ff521ea5 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -13,25 +13,39 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): "couple, one for each other occupier aged 16 or over, and the " "children's bedrooms. Occupiers are everyone who lives in the " "dwelling as their home except a joint tenant outside the claimant's " - "household, so a householder's boarder or lodger adds a bedroom, as " - "does a non-dependant, but a sharer of the rent does not. A sharer's, " - "boarder's or lodger's own claim counts only their own family." + "household. So every member of the benefit unit aged 16 or over who " + "is not the claimant or partner adds a bedroom, such as a young " + "person in full-time education; so do a householder's boarder or " + "lodger and a non-dependant, but a sharer of the rent does not. A " + "sharer's, boarder's or lodger's own claim counts only their own " + "family." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", "https://www.legislation.gov.uk/uksi/2006/214/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/2", ) def formula(benunit, period, parameters): person = benunit.members + # A child is a person under 16 (HB Regs 2006 reg 2(1)). aged_16_or_over = person("age", period) >= 16 + # HB Regs 2006 reg 13D(3)(b): a person who is not a child, here a + # member of the benefit unit other than the claimant or partner. + family_rooms = benunit.sum( + aged_16_or_over & ~person("is_claimant_or_partner", period) + ) head_family = person.benunit.any(person("is_household_head", period)) sharer = person.benunit("liable_for_share_of_household_rent", period) - # HB Regs 2006 reg 13D(3), with "occupiers" as defined in 13D(12). + # Reg 13D(3)(b) for occupiers outside the family, as defined in + # 13D(12). other_occupier = aged_16_or_over & ~head_family & ~sharer is_head_family = benunit.any(person("is_household_head", period)) other_occupiers = is_head_family * benunit.max( person.household.sum(other_occupier) ) - return 1 + other_occupiers + bedrooms_for_children(benunit, period) + return ( + 1 + family_rooms + other_occupiers + bedrooms_for_children(benunit, period) + ) From 7c6f5f30965f8bbcd2c5d1fcbe21ec8d196cca3c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:33:22 -0400 Subject: [PATCH 2/3] Test the bedroom of each benefit-unit member aged 16 or over YAML cases for a qualifying young person aged 17 and 19, an 18-year-old non-dependant, a looked-after qualifying young person (no UC bedroom), a couple with a young person and two younger children (category E), a partner aged 19 (no extra bedroom) and a pensioner couple's grandchild under HB (SPC). Two LHA_claimant_composition cases asserted the old one-bedroom count for an apprentice non-dependant and an 18-year-old qualifying young person; both are now two bedrooms (Sch 4 para 10(1)(c) and (b)). Hypothesis properties: bedrooms never fall when a person aged 16-19 joins any family; the exact change when they do; and agreement with an independent count of the size criteria, children's rooms found by brute force. Fixes #2008 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2008.fixed.md | 1 + .../family/LHA_claimant_composition.yaml | 16 +- .../family/LHA_young_person_bedroom.yaml | 251 ++++++++++++++++++ ...est_lha_bedroom_young_people_properties.py | 247 +++++++++++++++++ 4 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 changelog.d/2008.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml create mode 100644 policyengine_uk/tests/test_lha_bedroom_young_people_properties.py diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md new file mode 100644 index 0000000000..b2adf85554 --- /dev/null +++ b/changelog.d/2008.fixed.md @@ -0,0 +1 @@ +- Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit and Housing Benefit LHA size criteria: a qualifying young person (UC Regs 2013 Sch 4 para 10(1)(b)) or other non-dependant (para 10(1)(c)), and for Housing Benefit any person who is not a child (HB Regs 2006 reg 13D(3)(b)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index 2038e23465..915e00ae84 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -1,7 +1,7 @@ # UC Regulations 2013, Schedule 4 paragraphs 9 and 27-28: # https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 -# The existing bedroom calculation is unchanged: B = 1 bedroom, C = 2; -# A is the shared accommodation rate, overriding the bedroom calculation. +# B = 1 bedroom, C = 2; A is the shared accommodation rate, overriding the +# bedroom calculation. - name: Single 17-year-old renter is not their own dependent child period: 2025 input: @@ -63,8 +63,11 @@ is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false lha_renter_has_non_dependant: true is_lha_shared_accommodation_rate_specified_renter: false - # No UC QYP, but a non-dependant blocks A. Existing bedroom formula = 1: B. - LHA_category: B + # No UC QYP (UC Regs 2013 reg 5(3): training under a contract of + # employment does not count), but a non-dependant blocks A. The + # non-dependant is not a child, so has a bedroom (Sch 4 para 10(1)(c)): + # 1 + 1 = 2: C. + LHA_category: C - name: Lone parent with an 18-year-old qualifying young person period: 2025 @@ -87,8 +90,9 @@ is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true lha_renter_has_non_dependant: false is_lha_shared_accommodation_rate_specified_renter: false - # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. - LHA_category: B + # Responsibility for a UC QYP blocks A. The QYP has a bedroom (Sch 4 + # para 10(1)(b)): 1 + 1 = 2: C. + LHA_category: C - name: Couple aged 30 and 17 is not a single specified renter period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml new file mode 100644 index 0000000000..d1a3e83f8e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -0,0 +1,251 @@ +# A member of the benefit unit aged 16 or over who is not the claimant or +# partner has their own bedroom in the size criteria. +# +# Universal Credit: UC Regs 2013 Sch 4 para 10(1)(b), "a qualifying young +# person for whom the renter or either joint renter is responsible"; anyone +# else aged 16 or over living with the renter is a non-dependant (para 9(2)) +# with a bedroom under para 10(1)(c), "a non-dependant who is not a child", +# except a qualifying young person no one is responsible for (para 9(2)(g)). +# A child is a person under 16 (WRA 2012 s.40). +# +# Housing Benefit: HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b), "a +# person who is not a child", where a child is a person under 16 (reg 2(1)). +# +# Rents of £200 a week (£10,400 a year) sit between the published Maidstone +# one-bedroom (B) and two-bedroom (C) rates (VOA, LHA April 2024, held for +# 2025-26: B £172.60, C £208.27 a week), so the bedroom decides whether the +# rent is met in full. + +- name: A qualifying young person has their own bedroom + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_household_head: true + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + benunits: + family: + members: [parent, young_person] + households: + home: + members: [parent, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + # UC: the renter (para 10(1)(a)) and the qualifying young person (para + # 10(1)(b)): 2 bedrooms, category C. + LHA_allowed_bedrooms: 2 + LHA_category: C + # The rent is below the two-bedroom rate, so it is met in full. + uc_housing_costs_element: 10_400 + # HB: one bedroom each for the claimant and the young person, neither of + # whom is a child (reg 13D(3)(b)): 2 bedrooms, category C. + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A qualifying young person aged 19 has their own bedroom + period: 2026 + input: + people: + parent: + age: 50 + is_household_head: true + is_parent: true + young_person: + age: 19 + current_education: UPPER_SECONDARY + # UC Regs 2013 reg 5(1)(b) and 5(2): enrolled before 19, and before + # the 1 September after their 19th birthday. + age_started_or_accepted_current_education_or_training: 18 + is_before_universal_credit_qualifying_young_person_terminal_date: true + benunits: + family: + members: [parent, young_person] + households: + home: + members: [parent, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + LHA_allowed_bedrooms: 2 + LHA_category: C + uc_housing_costs_element: 10_400 + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A member aged 18 who is not a qualifying young person is a non-dependant with a bedroom + period: 2026 + input: + people: + parent: + age: 45 + is_household_head: true + is_parent: true + son: + age: 18 + current_education: NOT_IN_EDUCATION + benunits: + family: + members: [parent, son] + households: + home: + members: [parent, son] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_universal_credit: [false, false] + # UC: not a qualifying young person, so a non-dependant (para 9(2)) who + # is not a child (para 10(1)(c)). + LHA_allowed_bedrooms: 2 + LHA_category: C + # HB: a person who is not a child (reg 13D(3)(b)). + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A qualifying young person no one is responsible for has no Universal Credit bedroom + period: 2026 + input: + people: + carer: + age: 40 + is_household_head: true + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + # UC Regs 2013 reg 4(6)(a): no one is responsible for a qualifying + # young person looked after by a local authority. + is_looked_after_by_local_authority: true + benunits: + family: + members: [carer, young_person] + households: + home: + members: [carer, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + # Not under para 10(1)(b), and not a non-dependant (para 9(2)(g)): the + # renter's bedroom only. + LHA_allowed_bedrooms: 1 + LHA_category: B + +- name: A qualifying young person cannot share with a younger sibling of the same sex + period: 2026 + input: + people: + parent_1: + age: 42 + is_household_head: true + is_parent: true + is_male: true + parent_2: + age: 40 + is_parent: true + is_male: false + young_person: + age: 17 + is_male: true + current_education: UPPER_SECONDARY + boy: + age: 12 + is_male: true + girl: + age: 8 + is_male: false + benunits: + family: + members: [parent_1, parent_2, young_person, boy, girl] + households: + home: + members: [parent_1, parent_2, young_person, boy, girl] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, false, false, false] + # UC: the couple (para 10(1)(a)), the qualifying young person (b), and + # one bedroom each for the boy and the girl, who are of opposite sexes + # and one of whom is 10 or over ((d)-(f)): 4 bedrooms, category E. + LHA_allowed_bedrooms: 4 + LHA_category: E + # HB: the couple (reg 13D(3)(a)), the young person ((b)), and the two + # children ((e)): 4 bedrooms, the maximum category. + housing_benefit_LHA_allowed_bedrooms: 4 + housing_benefit_LHA_category: E + +- name: A partner aged 19 adds no bedroom + period: 2026 + input: + people: + claimant: + age: 24 + is_household_head: true + partner: + age: 19 + current_education: NOT_IN_EDUCATION + benunits: + couple: + members: [claimant, partner] + households: + home: + members: [claimant, partner] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true] + # The couple share one bedroom (UC para 10(1)(a); HB reg 13D(3)(a)). + LHA_allowed_bedrooms: 1 + LHA_category: B + housing_benefit_LHA_allowed_bedrooms: 1 + housing_benefit_LHA_category: B + +- name: A pensioner couple's grandchild in full-time education has their own Housing Benefit bedroom + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandparent_1: + age: 70 + is_household_head: true + grandparent_2: + age: 68 + grandchild: + age: 17 + current_education: UPPER_SECONDARY + benunits: + family: + members: [grandparent_1, grandparent_2, grandchild] + households: + home: + members: [grandparent_1, grandparent_2, grandchild] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, false] + is_child_or_young_person_for_legacy_benefits: [false, false, true] + # HB (SPC) Regs 2006 reg 13D(3): the couple ((a)) and the young person + # ((b)): 2 bedrooms, category C. + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + # No income, so Housing Benefit meets the eligible rent: the rent, which + # is below the two-bedroom rate. + housing_benefit: 10_400 diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py new file mode 100644 index 0000000000..b23fa6a8cd --- /dev/null +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -0,0 +1,247 @@ +"""Property-based tests for the bedrooms of benefit-unit members aged 16 or +over who are not the claimant or partner (UC Regs 2013 Sch 4 paras 9-10; HB +Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)). + +Invariants, for any generated population of households: + +1. Monotonicity: adding a person aged 16 to 19 to any family never lowers + any family's Universal Credit or Housing Benefit bedrooms, whether the + claimant and partner are supplied or inferred. +2. Exact effect: when the added person is not the claimant or partner, their + family gains one Housing Benefit bedroom, and one Universal Credit bedroom + unless they are a qualifying young person no one is responsible for (para + 9(2)(g)). The household head's family also gains one bedroom under both + schemes if the person joins a non-dependant's family, and one Housing + Benefit bedroom if they join a boarder's or lodger's family. No other + family's bedrooms change. +3. Reference: every family's bedrooms equal an independent count of the size + criteria: one for the claimant or couple; one for each other member aged + 16 or over (for Universal Credit, except a qualifying young person no one + is responsible for); for the household head's family, one for each person + aged 16 or over in a non-dependant's family (and, for Housing Benefit, in + a boarder's or lodger's family); and the fewest rooms that hold the + family's children under 16 two to a room, where only children of the same + sex or two children under 10 may share, found by brute force. Children of + other families are not counted for the household head, matching the model + (a separate, existing gap). +""" + +from itertools import combinations + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 # Situation inputs without a period are set for 2025. +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +ROLES = ["sharer", "boarder", "lodger", "non_dependant"] +EDUCATION = ["NOT_IN_EDUCATION", "UPPER_SECONDARY", "TERTIARY"] + +child = st.tuples(st.integers(0, 15), st.booleans()) # (age, is_male) + + +@st.composite +def family(draw, role): + return dict( + role=role, + adults=draw(st.lists(st.integers(20, 85), min_size=1, max_size=2)), + children=draw(st.lists(child, min_size=0, max_size=4)), + ) + + +@st.composite +def household(draw): + others = draw( + st.lists(st.sampled_from(ROLES).flatmap(family), min_size=0, max_size=2) + ) + return [draw(family("head"))] + others + + +@st.composite +def young_person(draw): + return dict( + age=draw(st.integers(16, 19)), + is_male=draw(st.booleans()), + current_education=draw(st.sampled_from(EDUCATION)), + age_started_or_accepted_current_education_or_training=draw(st.integers(14, 19)), + is_before_universal_credit_qualifying_young_person_terminal_date=draw( + st.booleans() + ), + is_looked_after_by_local_authority=draw(st.booleans()), + receives_benefits_in_own_right=draw(st.booleans()), + ) + + +@st.composite +def scenario(draw): + population = draw(st.lists(household(), min_size=1, max_size=4)) + h = draw(st.integers(0, len(population) - 1)) + f = draw(st.integers(0, len(population[h]) - 1)) + return population, (h, f), draw(young_person()) + + +def build(population, added=None, supply_roles=True): + """One situation for the population, optionally with a person added to + family ``added[0]`` = (household, family) with attributes ``added[1]``. + + Returns the situation and, per benefit unit, its (household, family).""" + people, benunits, homes, keys = {}, {}, {}, [] + for h, families in enumerate(population): + members = [] + for f, fam in enumerate(families): + name = f"h{h}_f{f}" + ids = [] + for i, age in enumerate(fam["adults"]): + pid = f"{name}_adult_{i}" + people[pid] = {"age": age, "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = True + elif fam["children"]: + people[pid]["is_parent"] = True + ids.append(pid) + for i, (age, male) in enumerate(fam["children"]): + pid = f"{name}_child_{i}" + people[pid] = {"age": age, "is_male": male, "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = False + ids.append(pid) + if added is not None and added[0] == (h, f): + pid = f"{name}_added" + people[pid] = {**added[1], "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = False + ids.append(pid) + extra = {} + if fam["role"] == "sharer": + extra["liable_for_share_of_household_rent"] = True + if fam["role"] in ("boarder", "lodger"): + people[ids[0]][f"rent_paid_as_{fam['role']}"] = 5_000 + if fam["role"] == "head": + people[ids[0]]["is_household_head"] = True + benunits[name] = {"members": ids, **extra} + members.extend(ids) + keys.append((h, f)) + homes[f"h{h}"] = { + "members": members, + "rent": 12_000, + "tenure_type": "RENT_PRIVATELY", + "brma": "MAIDSTONE", + } + situation = {"people": people, "benunits": benunits, "households": homes} + return situation, keys + + +def bedrooms(situation): + sim = Simulation(situation=situation) + uc = np.asarray(sim.calculate("LHA_allowed_bedrooms", YEAR)) + hb = np.asarray(sim.calculate("housing_benefit_LHA_allowed_bedrooms", YEAR)) + return sim, uc, hb + + +def fewest_rooms_for_children(children): + """Brute force: the fewest rooms holding the children two to a room, + where a pair must be of the same sex or both under 10.""" + + def can_share(a, b): + return a[1] == b[1] or (a[0] < 10 and b[0] < 10) + + def best(remaining): + if not remaining: + return 0 + first, rest = remaining[0], remaining[1:] + rooms = 1 + best(rest) + for j, other in enumerate(rest): + if can_share(first, other): + rooms = min(rooms, 1 + best(rest[:j] + rest[j + 1 :])) + return rooms + + return best(list(children)) + + +@PROPERTY_SETTINGS +@given(scenario(), st.booleans()) +def test_adding_a_person_aged_16_to_19_never_lowers_bedrooms(case, supply_roles): + population, target, person = case + before, keys = build(population, supply_roles=supply_roles) + after, keys_after = build(population, (target, person), supply_roles) + assert keys == keys_after + _, uc_before, hb_before = bedrooms(before) + _, uc_after, hb_after = bedrooms(after) + # 1. Monotonicity. + assert np.all(uc_after >= uc_before) + assert np.all(hb_after >= hb_before) + + +@PROPERTY_SETTINGS +@given(scenario()) +def test_exact_effect_of_adding_a_person_aged_16_to_19(case): + population, target, person = case + before, keys = build(population) + after, _ = build(population, (target, person)) + _, uc_before, hb_before = bedrooms(before) + sim, uc_after, hb_after = bedrooms(after) + # Whether the added person is a qualifying young person no one is + # responsible for. (The qualifying young person tests themselves are + # covered elsewhere.) + pid = f"h{target[0]}_f{target[1]}_added" + index = list(after["people"]).index(pid) + qualifying = sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) + responsible = sim.calculate( + "is_child_or_qualifying_young_person_for_universal_credit", YEAR + ) + unclaimed = bool(qualifying[index]) and not bool(responsible[index]) + # 2. Exact effect. + expected_uc = np.zeros(len(keys)) + expected_hb = np.zeros(len(keys)) + t = keys.index(target) + expected_uc[t] += 0 if unclaimed else 1 + expected_hb[t] += 1 + role = population[target[0]][target[1]]["role"] + head = keys.index((target[0], 0)) + if role == "non_dependant": + expected_uc[head] += 1 + expected_hb[head] += 1 + elif role in ("boarder", "lodger"): + expected_hb[head] += 1 + assert np.array_equal(uc_after - uc_before, expected_uc) + assert np.array_equal(hb_after - hb_before, expected_hb) + + +@PROPERTY_SETTINGS +@given(scenario()) +def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): + population, target, person = case + situation, keys = build(population, (target, person)) + sim, uc, hb = bedrooms(situation) + qualifying = sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) + responsible = sim.calculate( + "is_child_or_qualifying_young_person_for_universal_credit", YEAR + ) + unclaimed = dict( + zip(situation["people"], np.asarray(qualifying) & ~np.asarray(responsible)) + ) + # 3. Reference count. + for b, (h, f) in enumerate(keys): + families = population[h] + fam = families[f] + added = (h, f) == target + own_children = fewest_rooms_for_children(fam["children"]) + own_uc = int(added and not unclaimed[f"h{h}_f{f}_added"]) + own_hb = int(added) + outside_uc = outside_hb = 0 + if fam["role"] == "head": + for g, other in enumerate(families[1:], start=1): + aged_16_or_over = len(other["adults"]) + int(target == (h, g)) + if other["role"] == "non_dependant": + outside_uc += aged_16_or_over + if other["role"] in ("non_dependant", "boarder", "lodger"): + outside_hb += aged_16_or_over + assert uc[b] == 1 + own_uc + outside_uc + own_children + assert hb[b] == 1 + own_hb + outside_hb + own_children From c4556bdf6bb6b26ad9e42741ba89f9c4da7146e7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 08:28:52 -0400 Subject: [PATCH 3/3] Exclude a qualifying young person no one is responsible for across families; one HB bedroom for an occupier couple Review findings on #2010: - UC Sch 4 para 9(2)(g) applies to the whole extended benefit unit, so a qualifying young person no one is responsible for (reg 4(6)) in a non-dependant's family adds no bedroom for the household head. - HB reg 13D(3)(a): an ordinary occupier couple outside the claimant's family shares one bedroom (DWP circular HB A3/2017); only non-couple occupiers aged 16 or over take one each under (b). The property oracles encoded both errors as law; they now count the law, and two YAML cases pin the cross-family results. Co-Authored-By: Claude Opus 5.5 --- .../family/LHA_young_person_bedroom.yaml | 71 +++++++++++++++++++ ...est_lha_bedroom_young_people_properties.py | 31 +++++--- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 32 +++++---- .../housing_benefit_LHA_allowed_bedrooms.py | 26 ++++--- 4 files changed, 127 insertions(+), 33 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index d1a3e83f8e..11bca1d504 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -249,3 +249,74 @@ # No income, so Housing Benefit meets the eligible rent: the rent, which # is below the two-bedroom rate. housing_benefit: 10_400 + +- name: A qualifying young person no one is responsible for in a non-dependant's family adds no Universal Credit bedroom + period: 2026 + input: + people: + renter: + age: 40 + is_household_head: true + carer: + age: 45 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + is_looked_after_by_local_authority: true + benunits: + renter_family: + members: [renter] + carer_family: + members: [carer, young_person] + households: + home: + members: [renter, carer, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_non_dependant_of_household_head: [false, true, true] + is_qualifying_young_person_for_universal_credit: [false, false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, false, false] + # The renter (para 10(1)(a)) and the carer, a non-dependant who is not a + # child ((c)). The young person is no one's responsibility (reg 4(6)), so + # is not a non-dependant (para 9(2)(g)): 2 bedrooms, category C. + LHA_allowed_bedrooms: [2, 1] + LHA_category: [C, B] + +- name: A non-dependant couple has two Universal Credit bedrooms but one Housing Benefit bedroom + period: 2026 + input: + people: + renter: + age: 40 + is_household_head: true + non_dependant_1: + age: 30 + non_dependant_2: + age: 28 + benunits: + renter_family: + members: [renter] + couple: + members: [non_dependant_1, non_dependant_2] + households: + home: + members: [renter, non_dependant_1, non_dependant_2] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, true] + # UC: the renter (para 10(1)(a)) and each non-dependant who is not a + # child ((c)): 3 bedrooms, category D. The couple's own category would be + # 1 bedroom, and a couple is not a specified renter (para 28): B. + LHA_allowed_bedrooms: [3, 1] + LHA_category: [D, B] + # HB: the claimant ((b)) and the occupier couple ((a)): 2 bedrooms, + # category C. An ordinary couple shares a bedroom; only a couple who + # cannot share has two ((za); DWP circular HB A3/2017). The couple pay no + # rent; their own category would be 1 bedroom: B. + housing_benefit_LHA_allowed_bedrooms: [2, 1] + housing_benefit_LHA_category: [C, B] diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index b23fa6a8cd..eda77f7a98 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -10,16 +10,22 @@ 2. Exact effect: when the added person is not the claimant or partner, their family gains one Housing Benefit bedroom, and one Universal Credit bedroom unless they are a qualifying young person no one is responsible for (para - 9(2)(g)). The household head's family also gains one bedroom under both - schemes if the person joins a non-dependant's family, and one Housing - Benefit bedroom if they join a boarder's or lodger's family. No other - family's bedrooms change. + 9(2)(g)). If the person joins a non-dependant's family, the household + head's family also gains one Housing Benefit bedroom, and one Universal + Credit bedroom unless they are a qualifying young person no one is + responsible for (para 9(2)(g) applies to the whole extended benefit + unit). If they join a boarder's or lodger's family, the household head's + family gains one Housing Benefit bedroom. No other family's bedrooms + change. 3. Reference: every family's bedrooms equal an independent count of the size criteria: one for the claimant or couple; one for each other member aged 16 or over (for Universal Credit, except a qualifying young person no one is responsible for); for the household head's family, one for each person - aged 16 or over in a non-dependant's family (and, for Housing Benefit, in - a boarder's or lodger's family); and the fewest rooms that hold the + aged 16 or over in a non-dependant's family under Universal Credit (except + a qualifying young person no one is responsible for), and under Housing + Benefit one for each occupier aged 16 or over in a non-dependant's, + boarder's or lodger's family, a couple sharing one (reg 13D(3)(a)); and + the fewest rooms that hold the family's children under 16 two to a room, where only children of the same sex or two children under 10 may share, found by brute force. Children of other families are not counted for the household head, matching the model @@ -206,7 +212,7 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): role = population[target[0]][target[1]]["role"] head = keys.index((target[0], 0)) if role == "non_dependant": - expected_uc[head] += 1 + expected_uc[head] += 0 if unclaimed else 1 expected_hb[head] += 1 elif role in ("boarder", "lodger"): expected_hb[head] += 1 @@ -238,10 +244,15 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): outside_uc = outside_hb = 0 if fam["role"] == "head": for g, other in enumerate(families[1:], start=1): - aged_16_or_over = len(other["adults"]) + int(target == (h, g)) + joins = target == (h, g) + adults = len(other["adults"]) # supplied as claimant/partner if other["role"] == "non_dependant": - outside_uc += aged_16_or_over + # UC: each non-dependant who is not a child. + outside_uc += adults + outside_uc += int(joins and not unclaimed[f"h{h}_f{g}_added"]) if other["role"] in ("non_dependant", "boarder", "lodger"): - outside_hb += aged_16_or_over + # HB: a couple ((a)) or a single adult ((b)), plus the + # added person ((b)). + outside_hb += 1 + int(joins) assert uc[b] == 1 + own_uc + outside_uc + own_children assert hb[b] == 1 + own_hb + outside_hb + own_children diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 4f91b49750..6550d988e0 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -46,9 +46,10 @@ class LHA_allowed_bedrooms(Variable): "is not the claimant or partner, one for each non-dependant aged 16 or " "over, and the children's bedrooms. A qualifying young person the " "renter is responsible for has their own bedroom; any other member " - "aged 16 or over is a non-dependant and has one too, except a " - "qualifying young person no one is responsible for, such as one " - "looked after by a local authority. Joint tenants and other sharers " + "aged 16 or over is a non-dependant and has one too. A qualifying " + "young person no one is responsible for, such as one looked after by " + "a local authority, is not a non-dependant and has no bedroom, in " + "this family or another. Joint tenants and other sharers " "of the rent, boarders and lodgers are not non-dependants, so they " "add no bedroom to anyone's entitlement; only the household head's " "family has non-dependants from other families (see " @@ -67,22 +68,27 @@ def formula(benunit, period, parameters): # A child is a person under 16 (WRA 2012 s.40). aged_16_or_over = person("age", period) >= 16 family_member = aged_16_or_over & ~person("is_claimant_or_partner", period) - # UC Regs 2013 Sch 4 para 10(1)(b): a qualifying young person for - # whom the renter is responsible (regs 4 and 5). - qualifying_young_person = family_member & person( + qualifying = person("is_qualifying_young_person_for_universal_credit", period) + responsible = person( "is_child_or_qualifying_young_person_for_universal_credit", period ) + # UC Regs 2013 Sch 4 para 9(2)(g): a qualifying young person for whom + # no one is responsible (reg 4(6), such as one looked after by a local + # authority) is not a non-dependant, in this family or another. + no_one_responsible = qualifying & ~responsible + # Para 10(1)(b): a qualifying young person for whom the renter is + # responsible (regs 4 and 5). + qualifying_young_person = family_member & responsible # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a - # non-dependant, unless they are a qualifying young person for whom - # no one is responsible (para 9(2)(g); reg 4(6)). - family_non_dependant = family_member & ~person( - "is_qualifying_young_person_for_universal_credit", period - ) + # non-dependant. + family_non_dependant = family_member & ~qualifying family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) # Para 10(1)(c): a non-dependant from another family who is not a # child. - non_dependant = aged_16_or_over & person( - "is_non_dependant_of_household_head", period + non_dependant = ( + aged_16_or_over + & ~no_one_responsible + & person("is_non_dependant_of_household_head", period) ) head_family = benunit.any(person("is_household_head", period)) non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py index 12ff521ea5..d82f494e09 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -10,10 +10,10 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): label = "Bedrooms in the Housing Benefit size criteria" documentation = ( "Housing Benefit size criteria: one bedroom for the claimant or " - "couple, one for each other occupier aged 16 or over, and the " - "children's bedrooms. Occupiers are everyone who lives in the " - "dwelling as their home except a joint tenant outside the claimant's " - "household. So every member of the benefit unit aged 16 or over who " + "couple, one for each other occupier aged 16 or over (an occupier " + "couple sharing one), and the children's bedrooms. Occupiers are " + "everyone who lives in the dwelling as their home except a joint " + "tenant outside the claimant's household. So every member of the benefit unit aged 16 or over who " "is not the claimant or partner adds a bedroom, such as a young " "person in full-time education; so do a householder's boarder or " "lodger and a non-dependant, but a sharer of the rent does not. A " @@ -34,14 +34,20 @@ def formula(benunit, period, parameters): aged_16_or_over = person("age", period) >= 16 # HB Regs 2006 reg 13D(3)(b): a person who is not a child, here a # member of the benefit unit other than the claimant or partner. - family_rooms = benunit.sum( - aged_16_or_over & ~person("is_claimant_or_partner", period) - ) + claimant_or_partner = person("is_claimant_or_partner", period) + family_rooms = benunit.sum(aged_16_or_over & ~claimant_or_partner) head_family = person.benunit.any(person("is_household_head", period)) sharer = person.benunit("liable_for_share_of_household_rent", period) - # Reg 13D(3)(b) for occupiers outside the family, as defined in - # 13D(12). - other_occupier = aged_16_or_over & ~head_family & ~sharer + # Reg 13D(3) for occupiers outside the family, as defined in 13D(12): + # a couple has one bedroom ((a)) and anyone else aged 16 or over one + # each ((b)), so each member of a couple counts as half. The extra + # bedroom for a couple who cannot share ((za)) is not modelled. + in_couple = claimant_or_partner & ( + person.benunit.sum(claimant_or_partner & aged_16_or_over) == 2 + ) + other_occupier = (aged_16_or_over & ~head_family & ~sharer) * where( + in_couple, 0.5, 1 + ) is_head_family = benunit.any(person("is_household_head", period)) other_occupiers = is_head_family * benunit.max( person.household.sum(other_occupier)