Repository navigation
Reconcile UC deduction and benefit cap components under the protected floor - #1819
vahid-ahmadi wants to merge 4 commits into
Conversation
|
Verified the five fixes independently against the branch — all confirmed:
One residual worth documenting (not blocking)
The award is correct in every row and the components reconcile — this is strictly an attribution issue between the two components, not a UC error, so it is much smaller than the bug this PR fixes. But under the absorption ordering the deduction should survive whole at 720.25 throughout, and in the last two rows it does not. It bites only when Suggest either fixing it or noting it in the docs alongside the existing last-resort caveat. |
|
@MaxGhenis review request — this fixes bugs in the deductions work merged in #1815. The judgement call is the absorption ordering. The floor limits combined reductions; something has to give first. This PR makes the benefit cap reduction absorb the floor while the deduction survives, on the reasoning that under the FRR the deductions cap (15%) equals a typical floor allowance, so cappable deductions alone essentially never breach it. JRF's worked example supports this — their floor allowance is GBP 14/week and their deduction is GBP 14, so it survives whole and the GBP 59 cap reduction goes. The model reproduces that exactly (SA GBP 92.34/wk vs their GBP 92, allowance GBP 13.85 vs their GBP 14). If you read their design differently — proportional abatement, say — that changes the split though not the award. Three things worth your eye:
Current law is bit-identical (verified numerically), and the gated |
|
Heads-up from #2089 (each scheme its own benefit cap), stacked on #2084. #2089 adds This PR adds a variable of the same name (the cap reduction left after the protected floor), so whichever lands second should rename one. Suggestion: keep #2089's as the reg 81 amount, and call this PR's |
Review of #1819 — Reconcile UC deduction and benefit cap components under the protected floorBase repository: PolicyEngine/policyengine-uk Source Documents
CriticalNone. Should AddressA1 — Give the changelog fragment a configured type (OPEN)Severity: SHOULD ADDRESS. Trigger: building the release notes with the repository's configured towncrier. Expected: this PR's five changes appear under its configured Validation: one batched run of changed Actual-input diagnostics use a couple aged35/33, four children aged13/11/9/7, London HA rent£30,000, zero earnings/assets, and an observed latent deduction demand0.30/ADVANCE_ONLY; UC eligibility and cap are computed. At floor0/.85/1.2, final UC is £19,769.96/£47,502.06/£48,632.64. Deduction plus applied UC cap component equals the actual lost entitlement in all three cases, and household net income changes consistently. Gross benefit cap reduction remains£27,355.24; the >1 floor adds no award above pre-cap entitlement. Diagnostic log ( An actual legacy HB claimant variant supplies reported HB receipt and a no-UC-claim take-up choice; its HB eligibility is computed. The .85 UC floor leaves HB£21,277.40 and net income£25,148.45 exactly unchanged, with both UC components0. HB diagnostic ( Material gap: the separately requested end-to-end case with a reported UC award below the protected floor was not completed before the shared17-minute substantive deadline. The low-award/minimum-payable unit/YAML tests pass, but they do not establish that additional reported-award scenario under an active floor. No incorrect result is inferred from this missing case. Source dates/historical JRF quotation are owned by the policy role. Snapshot clean; only RUN_ROOT scripts/logs/artifacts written. No posting. SuggestionsNone. Evidence Gaps
Notes
Validation Summary179 passed, 14.03s pytest /17.46s CLI; actual-family floor0/.85/1.2 component reconciliation and final UC/net income pass; legacy HB final award/net income unaffected; draft towncrier omits untyped fragment Original statutory/government/JRF sources read with SHA-bound extracts. current-pr-1819-review-sources.json; current-pr-1819-review-source-cache-check.json. No duplicate calculation diagnostics; original PDF extraction via bundled pypdf. Canonical source-manifest validator exited 0. CI at d287b18: Install + smoke-import (py3.9)=SUCCESS, Test documentation builds=SUCCESS, Install + smoke-import (py3.10)=SUCCESS, Install + smoke-import (py3.11)=SUCCESS, Install + smoke-import (py3.12)=SUCCESS, Install + smoke-import (py3.13)=SUCCESS, Install + smoke-import (py3.14)=SUCCESS, Lint=SUCCESS, Test=SUCCESS. Detailed code, policy, source and reproduction artifacts were retained locally. Timingelapsed seconds: 1151.70s; setup seconds: 272.06s Review SeverityCOMMENT. Open findings: 0 critical, 1 should address, 0 suggestions. |
d287b18 to
046e7d5
Compare
|
This PR's fragment 🤖 Generated with Claude Code |
046e7d5 to
ed1154d
Compare
|
Suggested The check isn't required yet (decision d1368); it fails fix PRs whose description names no reproducer. 🤖 Generated with Claude Code |
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed ed1154de76f4253653bdc9fb6be7662447da65c8. The 17 focused UC-deductions tests pass. I am requesting changes because the PR body fails current main’s Fix evidence checker: it has no named Reproducer: line. Adding Reproducer: policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_floor_components_reconcile_with_the_reduction_applied makes the checker pass. The green checks on this older branch do not include that workflow.
Please also settle the protected-floor component ordering. uc_deductions.py:29-35 limits debt recovery using the gross cap before uc_benefit_cap_reduction.py reduces the applied cap. With £6,000 pre-cap UC, £5,900 gross cap, and an 85% floor, an exact-head fixture gives £99.88 debt deduction and £620.37 applied cap; the PR’s stated debt-first ordering would give £720.25 debt and £0 cap at the same final UC. Add a near-exhaustion test and implement the stated ordering, or document and justify the gross-cap debt limit as the deliberate choice. JRF specifies a combined floor but does not prescribe component priority.
Finally, deductions/cap.yaml records the 2021 25% cap from 30 April, while DWP’s ministerial answer says 12 April 2021. This does not change the annual result because the model samples 30 April, but the historical date should be corrected. Please add the required axiom: status to the description.
|
Independent review (GPT-6.1 Sol, UK hub, 2026-10-12): REQUEST_CHANGES at @vahid-ahmadi: the review confirms the original bugs on main and that the reconciliation identity, non-negative components, the floor bound and HB's gross reading all hold. It found:
Suggested landing order after the fixes: #1819, then #1820, then #2076. Would you like to fix these yourself, or have the hub push the fixes as new commits on your branch? Say which here. VERDICT: REQUEST_CHANGES
Reading current main confirms the original component mismatch, above-one floor bug, and missing claim guard. The claim-guard reproducer additionally supplies a positive pre-cap award; latent-rate imputation alone does not trigger it when pre-cap UC is calculated normally. The 25%+5% comment correction is accurate. The 15% Fair Repayment Rate from 30 April 2025, statutory 40% ceiling, penny reference, and baseline floor zero are correctly distinguished. 2025 explanatory memorandum, Schedule 6. For claiming units with calculated nonnegative inputs, the requested floored award identity, nonnegative components, combined floor bound, and deduction penny constraint hold. Housing Benefit still reads the gross reduction; deductions and incidence retain their gross dependencies, so no new cycle appears. Recommended order after fixes: #1819 → #1820 → #2076; the first two can land independently. #1820 still needs its 12 October earnings-floor and pension-relievability fixes, and must preserve #1819’s floor components and gross HB cap. #2076 must merge main after both, retaining corrected individual earnings calculations, dated £846/£881 thresholds, scoped exemptions, and the UC reconciliation. Its current earnings rewrite would otherwise restore older behavior. Coordination comment. Validation: 6 YAML cases and 17 Python tests passed serially, using the existing cloned-system fixture. Compressor occupancy was below 30 GB. The diff weakens no assertions and introduces no skips or xfails; the new tests are fixed examples, not generated property tests. No files were edited and nothing was posted. |
|
@MaxGhenis thanks, I'll fix these myself on this branch today. |
… floor The protected minimum floor was applied only when computing universal_credit, so uc_deductions and benefit_cap_reduction reported their unfloored values: a floor of 0.85 with a 1,000 GBP benefit cap reduction and a 0.30 latent rate reported 960.34 + 1,000 GBP against a 720.25 GBP reduction actually applied. The floor now lives in the components. uc_maximum_reduction holds the allowance the floor leaves; uc_deductions is limited only where deductions alone exceed it, and the new uc_benefit_cap_reduction takes what remains. That ordering matches JRF's worked example, which preserves the deduction and eliminates the cap reduction, and it means the components sum to the entitlement forgone. benefit_cap_reduction stays gross because Housing Benefit uses it and the UC floor does not protect Housing Benefit, which also keeps it out of a dependency cycle. Also clamps the floor allowance at zero (a floor above 1 made it negative and added money to the award), gates uc_deductions on would_claim_uc for the dataset-imputed migration path, and corrects the 25% cap date to 30 April 2021 and the one-penny citation to Schedule 6 paragraph 3(1)(a) of SI 2013/380. Current-law results are unchanged: the floor is zero, so the allowance is infinite and both components pass through unaltered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… under the protected floor The applied UC cap reduction is bounded by the award actually lost (a £1,000 gross reduction on a £500 award applied £720.25 under an 85% floor, and £1,000 with none). Where the floor binds, deductions are met first from the protected award, up to the floor allowance, and the cap reduction absorbs the rest; previously the penny rule squeezed deductions under the gross cap, so heavy capping moved the split. Where the floor does not bind, and under current law, values are unchanged and universal_credit keeps main's float order. The 25% deductions cap dates from 12 April 2021 (DWP Annual Report 2020-21). The JRF reference moves to the September 2026 briefing, with a test of its example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ed1154d to
656a0f8
Compare
|
Thanks both. Everything is in 656a0f8, rebased onto current main (e01f2f5). The description is rewritten to match.
@MaxGhenis, hub review
Tests
The @juaristi22 @MaxGhenis could you re-review? |
There was a problem hiding this comment.
Re-reviewed 656a0f8203a084c9a418123e3671e846ffe38832. The earlier low-award bound, heavy-cap priority, Reproducer, historical date and changelog issues are resolved. The 299 focused head tests pass. One merge blocker remains against current main 1a31ef1fd2c1a1e53f616df8d27c464a726e13ed:
Both new reduction components use a narrower claim guard than current main’s UC award. uc_benefit_cap_reduction.py:24 and deductions/uc_deductions.py:26 use defined_for = "would_claim_uc". Main now pays UC for claims_universal_credit, including an actual claim at legacy-benefit closure when ordinary take-up is false. Composing these components with that route removes both the cap and debt deductions from an actual claimant.
Reproduction: model year 2025, couple aged 35 and 33, children aged 13, 11, 9 and 7, prior CTC reported £3,000, London council rent of £30,000, no earnings/disability/caring, would_claim_uc:false, would_claim_uc_at_legacy_closure:true; assume the migration claim is made and there is no prior-earnings grace. Eligibility, closure claim and cap are calculated. Current main gives pre-cap UC of £48,632.64, a gross cap reduction of £27,355.24, deductions of £1,130.58 and final UC of £20,146.82. An in-memory overlay of the exact PR classes, preserving main’s claim guard, zeros both components and gives £48,632.64: £28,485.82 too much annually. Changing only the two component guards to claims_universal_credit restores main exactly.
This is a current-main composition check, not a failure on the older isolated head or a claim that a merged Git tree was tested. Please update onto main, preserve its actual-claim route, use the same predicate for both components, and add a migration regression through final UC. Migration regulation 44 supports the made-claim premise; UC regulation 81 still requires the cap. The current head’s green CI does not cover this newer claim route.
|
#2089 merged on 2026-10-17 as When this PR next merges main, two things will fail until the floor-bounded amount here is renamed:
As suggested on 10/02, it could become |
Fixes bugs in the UC deductions and protected floor code merged in #1815. Current-law results are unchanged to the bit (checked on enhanced FRS 2026, below); the fixes change how a protected-floor reform is reported, and stop a floor above 1 adding money.
Reproducer:
policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_floor_components_reconcile_with_the_reduction_applied,policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_applied_cap_never_exceeds_the_award_lost,policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_heavy_capping_does_not_squeeze_the_deduction,policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_floor_changes_nothing_where_it_does_not_bind,policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_a_floor_above_the_standard_allowance_adds_nothing,policyengine_uk/tests/test_uc_deductions.py::TestReformLevers::test_no_deductions_without_a_universal_credit_claimaxiom: n/a: current-law UC awards are unchanged (identical to main on enhanced FRS 2026); the operational deductions caps and the protected floor have no rulespec-uk module; the reg 81 amount in uk/regulations/uksi/2013/376/81.yaml is untouched
What was wrong
universal_creditlimited the sum ofbenefit_cap_reductionanduc_deductionsto the floor allowance, but neither component was rewritten. With floor 0.85, a £1,000 gross cap reduction and latent rate 0.30, they reported £960.34 and £1,000 against £720.25 actually taken.(1 - floor) x standard allowancewent negative, soprotected_floor = 1.2turned a £6,000 entitlement into £6,960.34.uc_deductionshad nowould_claim_ucguard, so a directly imputed latent rate gave deductions with no claim.The fix
One helper,
deductions/_reduction_allocation.py::allocate_uc_reductions, returns both components.uc_deductionsand the newuc_benefit_cap_reductioneach call it, and neither reads the other, so the graph stays acyclic. Its inputs are pre-cap UC, the grossbenefit_cap_reduction, the raw deduction demand (rate x standard allowance), the floor allowanceuc_maximum_reductionand the annualised penny.universal_creditsubtracts(uc_benefit_cap_reduction + uc_deductions), which keeps main's float order.benefit_cap_reductionstays gross because Housing Benefit reads it and the UC floor does not protect HB.uc_has_deductionstill reads the gross figure, so deduction incidence is unchanged.Why deductions come first
JRF fixes the combined limit, not a priority. Their September 2026 briefing (which replaces the October 2025 one) proposes that "the sum of the 'capped amount' and any debt deductions is limited to the difference between a household's standard allowance and the level of the protected minimum floor". So the order is a modelling choice. Debt first is the one that:
test_jrf_2026_example_keeps_a_full_rate_deduction;The award is the same under either order; only the split differs.
Still a modelling choice: where deductions alone exceed the allowance, the floor binds on the above-cap excess too (last resort and child maintenance deductions), which current law exempts from the deductions cap. JRF does not say whether their floor exempts those. This is documented in
protected_floor.yaml,uc_deductions.pyand the validation page, and pinned bytest_protected_floor_binds_on_the_above_cap_excess.Parameters and references
deductions/cap.yaml: the 25% cap now starts on 12 April 2021, per DWP Annual Report and Accounts 2020-21, p.57 ("reduced from 30% to 25% from 12 April 2021"), now cited. There's no annual effect, because fiscal-year parameters are sampled on 30 April.deductions/protected_floor.yaml: the JRF reference now points to the September 2026 briefing.deductions/minimum_payable.yamlanduc_deductions.py: the one-penny rule is Schedule 6 paragraph 3(1)(a) of SI 2013/380.2026 impact (enhanced FRS 2024-25,
policyengine-uk-data-privatesnapshot47bbb58d, maine01f2f5ecvs this branch)uc_deductionsuc_benefit_cap_reduction£0.125bnThe floor raises UC by £1.061bn for 330,093 benefit units (weighted) on both main and this branch. No unit in the 2026 data has a gross cap above its pre-cap award or a deduction squeezed by heavy capping, so fixes 2 and 3 move no totals there; they are covered by unit tests.
Tests
test_uc_deductions.py: 23 passed (10 added by this PR). The tests for fixes 2 and 3, and the not-binding test, failed oned1154de7before this round.tests/policy/baseline/finance/benefit/family/universal_credit): 414 passed. Benefit cap YAML: 24 passed.policyengine-core test policyengine_uk/tests/policy): 3,058 passed.pytest policyengine_uk/tests -m "not microsimulation"(excludingtests/policy): 5,406 passed, 70 skipped, 2 xfailed.ruff format --checkandruff check: clean..github/check_fix_pr.py: passes.Coordination
#2089 (open) adds a variable also called
uc_benefit_cap_reduction, the reg 81 amount. Whichever lands second should rename. The suggestion on this thread is to keep #2089's name for the reg 81 amount and rename this oneuc_benefit_cap_reduction_after_floor. Suggested landing order: #1819, #1820, #2076.🤖 Generated with Claude Code