Skip to content

Stop deciding a household's UC health element rate by its position - #2251

Merged
MaxGhenis merged 3 commits into
mainfrom
fix-uc-health-claim-status
Oct 11, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
fix-uc-health-claim-status

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2250

Stacked on #2248, the protected LCWRA amount; the diff below is relative to it. Retarget to main once #2248 merges.

What was wrong

add_universal_credit_reform gave the new-claimant LCWRA rate by a seeded draw over every benefit unit in a simulation, household situations included:

  • Position decided the award. A household's award depended on its position among the other benefit units. On main, 40 identical single LCWRA claimants in one situation get £217.26 at positions 1, 2, 15, 25 and 32, and the protected amount elsewhere.
  • No way to state it. No situation could state that a claimant is new, so a lone household always got the protected amount.

The law

UC Regs 2013 reg. 36 gives the new-claimant rate to claimants other than a pre-2026, severe conditions criteria or terminally ill claimant. That is a fact about the claimant, which a household calculation should be able to state.

Changes

  • uc_receives_new_claimant_health_element (new, benefit unit, bool, default false).
  • uc_reform.py decides the status per year in _new_claimant_health_element_status, reading the simulation's own branch (sim.get_array):
    • A value the situation or dataset supplies is used as given.
    • Data that supplies none keeps the seeded draw, the same draw as before, now recorded as an input.
    • A situation that supplies none gets the protected amount (false).

Invariants (tested)

  • Position independence. In a situation, a household's health element does not depend on how many other benefit units the simulation holds or where it sits among them. 40 identical households all get what a lone household gets.
  • Supplied status wins. A supplied status is used as given, in situations and in data.
  • Microsimulation unchanged. On data that does not supply the status, the seeded draw is unchanged. Estimated on the local enhanced FRS 2023-24, 2026-27 LCWRA is £23.3207bn and UC £62.1568bn, identical to Pay pre-2026 UC health element claimants the legislated protected amount #2248; 2027-28 matches as well.

Tests

test_uc_rebalancing.py:

  • Position. test_household_award_does_not_depend_on_its_position (40 households). On main, the same check finds 5 of 40 at £217.26.
  • Stated status. test_new_claimants_use_fixed_health_element now states the status rather than forcing the draw.
  • Data. test_data_without_a_status_keeps_the_seeded_draw and test_data_that_supplies_the_status_overrides_the_draw use in-memory datasets.
  • Branches. test_a_branch_can_state_its_own_status: a branch's status, and a nested branch's inherited status, give the new-claimant rate while the parent is unchanged. It fails on the default-branch lookup.

Local runs:

  • test_uc_rebalancing.py, code_health/test_cached_arrays_not_written_in_place.py and test_income_support_esa_entered_directly.py: 71 passed.
  • UC YAML policy tests: 414 passed.
  • Ruff check and format: clean.

Impact

None in microsimulation. Household situations without the status now always get the protected amount; previously about 11% of benefit units got the new-claimant rate in 2026, depending on position.

axiom: n/a: input plumbing, and no rule or amount changes. rulespec-uk uk/regulations/uksi/2013/376/36.yaml already takes the claimant's pre-2026 status as an input (claimant_is_pre_commencement_lcwra_claimant).

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 10, 2026 08:11
The UC rebalancing modifier gave the new-claimant LCWRA rate by a seeded
draw over every benefit unit in a simulation, situations included, so a
household's award depended on its position among the others (5 of 40
identical households in one situation got £217.26 on main) and no
situation could state the claimant's status.

Add uc_receives_new_claimant_health_element (benefit unit, default
false). A supplied value is used as given; data without one keeps the
seeded draw, recorded as an input; a situation without one gets the
protected amount. Microsimulation results are unchanged.

Fixes #2250

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address the review of #2251:
- Look the status up with sim.get_array, which reads the simulation's
  branch and the branches it inherits from; the holder's get_array read
  only the default branch, so a branch-supplied status was ignored. A new
  test sets a branch's status, checks the branch and a nested branch get
  the new-claimant rate and the parent is unchanged; it fails on the old
  lookup.
- uc-rebalancing.md: the seeded draw applies only to data without a
  supplied status; situations default to the protected amount and can set
  the status per year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the fix-uc-health-claim-status branch from fa87c3c to af865c6 Compare October 10, 2026 12:13
Base automatically changed from fix-uc-protected-lcwra-2026 to main October 10, 2026 22:48
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merging on the gates.

🤖 Generated with Claude Code

@MaxGhenis
MaxGhenis merged commit cfa0c55 into main Oct 11, 2026
7 checks passed
@MaxGhenis
MaxGhenis deleted the fix-uc-health-claim-status branch October 11, 2026 13:17
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
Main push runs were cancelled when a newer commit landed, so 83001d5 and
cfa0c55 never finished a Test on 11 October, and a conflict between
#2251 and #2270, each green alone, showed only at 7343f94. Remove the
main push workflow's concurrency group so every merged commit finishes
its run.

pr_code_changes.yaml also runs on merge_group, so a merge queue on main
can require the same checks and test each pull request merged onto the
latest main before it lands; the Changes job compares a group with the
main it merges onto. Main then moves only to a commit the queue tested,
so the main push workflow's new Tested job skips the suite for a commit
that already has a passing "Test / Result" check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…able-simulated-benefits-scoped-awards

No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
Brings in #2258 (120-minute Test job), #2180 (NICs thresholds indexed by
September CPI after 2030-31), #2063 (salary sacrifice needs pay), #2038
(winter payments to the member a benefit is paid to), #2259, #2248,
#2251, #2262, #2266, #2270, #2285, #2288, #2294 and #2021.

Conflict: test_legacy_award_readers_properties.py imports. Kept both
hypothesis.event (this branch's screened-out events) and
hypothesis.example (#2294's found families). The rest of the file merged
cleanly: #2294's without/silent/with-other comparison, the severe
disability premium bound and the held benefit cap sit alongside this
branch's work generators, screened reference and outside-member tests.
Two comments now say the ESA and JSA readers also read the members' work.

_legacy_award_payee.py merged cleanly: #2038's can_claim (Pension Credit)
and this branch's own_report_is_paid are independent. The shared NI
parameters already match main (#2058); #2180 changes only 2031-32 onward.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
…efit-cap-75f-claimant-partner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A household's UC health element depends on its position among the other benefit units in a simulation

1 participant