Skip to content

Count Carer's Allowance earnings-limit pence exactly - #2543

Draft
MaxGhenis wants to merge 17 commits into
mainfrom
fix/2541-ca-earnings-pence
Draft

MaxGhenis wants to merge 17 commits into
mainfrom
fix/2541-ca-earnings-pence

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2541

What and why

is_gainfully_employed_for_carers_allowance decides whether a carer's earnings exceed the Carer's Allowance earnings limit: SSCBA 1992 s.70(1)(b), and ICA Regulations 1976 reg 8(1), "have exceeded £204" from 6 April 2026. In the Computation of Earnings Regulations 1996:

  • Reg 5 says that where "any calculation under these Regulations results in a fraction of a penny", the fraction "shall, if it would be to the claimant's advantage, be treated as a penny, otherwise it shall be disregarded".
  • Reg 8(1)(b)(iii) turns a yearly payment into a weekly one "by dividing the amount of the payment by 52".
  • Reg 10(4) nets PAYE, primary Class 1 and "one half of any sum paid … by way of a contribution towards an occupational or personal pension scheme" off pay. Regs 13 and 14 net a notional basic-rate income tax, Class 4 (and, before 2024-25, Class 2) and half of pension premiums off trading profit.

The formula floored the weekly pence in float32, the dtype of carers_allowance_earnings: np.floor(weekly * 100 + 1e-6) / 100 > limit. At these magnitudes the + 1e-6 does nothing, because float32 spacing near 20,400 is 0.00195. So float32 rounding, not the law, decided cases within about 0.002 pence a week of a boundary. Earnings of 52 × L + £0.52 a year (L + £0.01 a week, a penny over) counted as within the limit for L = £128, £132, £139 and £151. A carer a penny a week over the 2020-21 to 2024-25 limits got the full allowance.

The rule

Reg 5 is applied where the calculation happens, so net earnings are whole pence, which float32 can hold recoverably:

  1. carers_allowance_earnings applies reg 5 to each step. It works in float64 pence from the whole pence each input was entered as (policyengine_uk/utils/pence.py:stored_pence).
    • Pay and profit are taken down to a whole penny (a higher figure never favours the claimant).
    • The calculated deductions are taken up to a whole penny, as reg 5 requires: notional tax, Class 4, Class 2, and half of pension contributions. That turns the half penny of an odd-pence contribution into a penny off earnings.
    • PAYE and Class 1 are in law the amounts actually deducted (reg 10(4)(a)), which are whole pence. The model's figures for them are rounded up the same way, in the claimant's favour.
    • Class 1 is ni_class_1_employee's formula worked exactly on the year's NI income in whole pence. The model works out each month's Class 1 in float32 and sums the twelve. That can hold a whole-penny total further out than stored_pence recovers: £175.56 (8% of £14,764.46 less £12,569.96) is stored as 175.5600128, and rounding that up deducted a penny too much. The exact figure is used where it agrees with the stored total to within float32 error (one float32 step of the NI income plus 64 of the contribution). Otherwise, as under a reform that replaces the formula, the stored total is used.
    • Pension contributions are pension_contributions, whether entered as a total or summed from its programmes. A float32 sum is redone in float64 pence (stored_sum_pence) where the stored total is within float32 rounding of its programmes' sum; otherwise it is read as entered. The model holds each programme as one annual amount, not the payments reg 10(4)(b) halves one by one, so the half is of the year's total, rounded up once.
  2. is_gainfully_employed_for_carers_allowance reads the whole pence back exactly, divides by 52 with the fraction disregarded (reg 8(1)(b)(iii) for pay; reg 11(1)(a), average weekly earnings over a year, for profit; with reg 5), and tests whether that exceeds the limit (reg 8(1), strict). An amount entered directly with a fraction of a penny has the fraction disregarded. The limit is compared in pence, rounded to a millionth of a penny, so a reformed limit of £204.10 isn't read as 20,409.999… pence.
annual_pence = np.floor(stored_pence(earnings))
weekly_pence = np.floor(annual_pence / WEEKS_IN_YEAR)
return weekly_pence > np.round(limit * 100, 6)

stored_pence(x) returns the whole pence a float32 lies within half a float32 step of, and otherwise its exact value in pence. An amount entered in whole pence is stored within half a step of them; below £131,072 a step is under a penny, so no other whole number of pence is that close. Every step is exact in float64 (a float32 has 24 significant bits, so × 100 is exact).

Why not round the stored total. This PR's first version took the nearest penny of the total and its second the nearest half penny. GPT-6.1 Sol's review found that the first counted 52L + £0.515 (pay less half an odd-pence contribution) up. juaristi22's review found that the second counted genuine fractions up: £14,000.01 of profit and a £6,039.38 premium leave £10,608.5174 unrounded, which it took to £10,608.52 (£204.01 a week), removing the full £4,495.40. A tolerance below the total can't fix this. Float32 holds a £14k–£20k profit only to within about a tenth of a penny before tax is taken off, so whole-pence and fractional totals overlap. A two-float32-step tolerance still misjudged 39 of 1,779 such traders within 5p of the 2026-27 boundary. Applying reg 5 to each step, before storage, decides them all exactly (below).

Known limits:

  • Inputs float32 can't tell apart. An input within half a float32 step of a whole penny is read as that penny: £10,608.5195 is stored as the same float32 as £10,608.52.
  • Class 1 falls back to the stored total when the exact formula disagrees with it beyond float32 error, as under a reform that replaces the Class 1 formula. A whole-penny amount the float32 sum holds more than half a step out then counts as a penny more. No baseline row disagrees in r5/class1_tolerance_study.py. It covers about 54,000 pays a year to £250,000, for every year from 2015 to 2031, plus 2022 full-year category A directors and statutory sick pay. The largest error is 0.42p, 0.18 of the tolerance.
  • An entered pension total within float32 rounding of its programmes' sum (under 0.2p for a total of £10,000) is read as that sum.
  • Half of pension contributions is of the year's total, rounded up once. A reading of "one half of any sum paid … in respect of a pay period" for a model that holds no payments.
  • The model's own approximations are unchanged. PAYE is the tax on the pay alone on a standard code, as documented in the variable. The model's annual Class 1 primary threshold works out at £12,569.96 (pay of £12,601.00 gives Class 1 of £2.4832). This PR takes Class 1 as the model's formula gives it.
  • Above £131,072 a year, whole pence may not be recovered exactly. That affects the decision only for limits above £2,520 a week; the highest statutory limit is £204.

Effect (~/reviews/uk-2541-ca-pence/probe_years.py; carer in England, aged 45, caring 40 hours, earnings = pay)

Year Limit Pay Weekly Before After
2015–2019 £110, £116, £120, £123 52L + 0.52 L + £0.01 gainfully employed, CA nil unchanged
2020 £128 £6,656.52 £128.01 not gainfully employed, CA £3,497.00 gainfully employed, CA nil
2021 £128 £6,656.52 £128.01 not, CA £3,515.20 gainfully employed, CA nil
2022 £132 £6,864.52 £132.01 not, CA £3,624.40 gainfully employed, CA nil
2023 £139 £7,228.52 £139.01 not, CA £3,991.00 gainfully employed, CA nil
2024 £151 £7,852.52 £151.01 not, CA £4,258.80 gainfully employed, CA nil
2025–2028 £196, £204 52L + 0.52 L + £0.01 gainfully employed, CA nil unchanged

52 × L and 52 × L + £0.51 stay within the limit in every year, before and after. carers_allowance_earnings itself moves by at most a few pence a year (each deduction's fraction of a penny).

Employees whose Class 1 is a whole number of pence that the float32 sum holds out are now judged as the law says. An example is £14,764.46 of pay with a £7,960.76 contribution in 2026-27 (YAML Cases 26–28, probe_rows.py in the review's salvage). They are exactly a penny a week over:

Pension contributions entered as a total (pension_contributions: 1000) come off earnings as on main (YAML Case 29). 32f1639 had dropped them.

Two variables read carers_allowance_earnings:

  • is_gainfully_employed_for_carers_allowance;
  • for the Scottish Carer Support Payment (2025-26 on), carer_support_payment_pre_couple_limit. Its own test, earnings < (limit + £0.01) × 52 in float32, is exact on whole pence.

CSP's SSI 2023/302 Schedule 2 has reg 5's rounding (para 4) and the same net earnings and profit (paras 9 and 12), so its boundary is now exact too. In Scotland in 2026-27, £12,576.76 of profit with a £3,932.96 premium has earnings of £10,608.51 (£204.00 a week) and CSP of £4,495.40; main had £10,608.5225 and CSP nil (r5/probe_csp.py). A penny more profit is over the limit on both.

Exhaustive and through-the-model checks (~/reviews/uk-2541-ca-pence/, scripts with logs)

sweep_exhaustive.py runs the limit test's real formula on numpy arrays for every distinct limit, against exact integer references. Main's and the second version's (a67d545) formulas run on the same inputs:

Set Reference This head Main a67d545
A. Every float32 in [4,096, 16,384), the two binades that hold every boundary (16.8 million values) this rule, in integer arithmetic on the float32 mantissa 0 1 each for £128–£151 (52L + £0.52, #2541) 2–5 per limit (fractions over three quarters of a penny counted up)
B. Every whole-pence amount below £65,536, entered as float32 A // 52 > 100 × L 0 1 each for £128, £132, £139, £151 0
C. Every half-penny amount below £65,536, entered directly A // 52 > 100 × L 0 0 0

sweep_offgrid.py runs net earnings with fractions of a penny through the full model, for 2024-25 to 2026-27, against the law in exact arithmetic, deduction by deduction: P less each deduction rounded up, over exactly when that reaches 52 × (100L + 1) pence. Each tree runs its own model:

Route (rows a year; cells are wrong rows over the three years) This head 32f1639 Main a67d545
Trading: profit P above the allowance, notional tax and Class 4, a premium C (≈1,770) 0 0 455 609
Employee: pay P above the primary threshold, Class 1 at 8% of P less £12,569.96 worked exactly, a contribution C (1,123) 0 0 124 214

About 140 trading and 31 employee rows a year fall on different sides of the boundary when only the total is rounded. Main and a67d545 never rounded each deduction, so most of their misses are these. This head's stored earnings are within half a float32 step of the legal whole pence in every row. The employee reference used to be the model's stored Class 1, which hid the Class 1 noise; with the exact Class 1, main's count is 124 rather than 115, and a67d545's 214 rather than 219.

sweep_class1_exact.py (written by the review of 32f1639) takes the untaxed employee route above, widens it to 5,248 rows a year (every 997p of contribution) and judges it against the law with Class 1 worked exactly. This head is wrong on 0 rows; 32f1639 on 6 (2 a year, the £175.56 and £235.38 rows); main on 580.

sweep_pension_pairs.py runs 59,532 whole-pence pairs of pay and pension contribution through the full model (every contribution from 0 to 40p, net earnings within 60p a year of the boundary, 2015 to 2026), against (P − ceil(C/2)) // 52 > 100 × L: 0 wrong.

Invariants

Stated in the test module docstrings, and executed:

  1. Whole pence are judged exactly: annual earnings of A pence are over the limit L exactly when A // 52 > 100 × L, in every year from 2015 to the parameter horizon. Tested by test_whole_pence_are_judged_exactly (Hypothesis, with boundary examples for every Carer's Allowance: a penny a week over the 2020-21 to 2024-25 earnings limits counts as within the limit #2541 limit), the per-limit tests and sweep B.

  2. Pay less half a pension contribution is judged as the law says: pay of P pence with a contribution of C pence is over the limit exactly when (P − ceil(C/2)) // 52 > 100 × L. Tested by test_pay_less_half_a_pension_contribution_is_judged_as_the_law_says (Hypothesis), test_half_an_odd_pence_pension_contribution_counts_as_a_penny_off (which also checks earnings are exactly P − ceil(C/2)), YAML Cases 23 and 24, and sweep_pension_pairs.py.

  3. Each deduction counts a fraction of a penny as a penny: with tax and Class 1 or Class 4 to pay, net earnings are P less each deduction rounded up, exactly, and the limit test follows them. Class 1 is 8% of P less 52 × £241.73, worked exactly in the test, not read from the model. Tested by test_each_deduction_counts_a_fraction_of_a_penny_as_a_penny (trading and employee, 2024-25 and 2026-27, with the review cases), YAML Cases 25–28, sweep_offgrid.py and sweep_class1_exact.py.

  4. Exact arithmetic (differential): for any stored amount, the float64 formula agrees with _over_limit, which restates it in fractions.Fraction. Tested by test_the_formula_matches_exact_arithmetic_and_rises_with_earnings, with the float32 values either side of 52L + £0.52 and the second version's review-case value as explicit examples, and through test_bounds_and_differential and the pension-share property test.

  5. Monotonic in earnings: more earnings never take a person from over the limit to within it.

  6. Net earnings are monotonic in steps of 2p: raising pay or profit by 2p or more never lowers them.

    • Why: the deductions that rise with pay (PAYE, Class 1) or with profit (notional tax, Class 4) have marginal rates adding to at most a half in 2026-27, and each is rounded up. So a rise of R pence adds less than R/2 + 2 pence of deductions, which is at most R when R ≥ 2.
    • Intended violation: a 1p rise can lower net earnings by 1p, when it takes both deductions to their next penny at once. £12,570.50 → £12,570.51 of profit with a £3,923.70 premium takes earnings from £10,608.52 (over) to £10,608.51 (within). That is reg 5 applied to each deduction, which the law requires.
    • Tested by test_net_earnings_never_fall_for_a_rise_of_two_pence_or_more, which runs every penny across £12,570, £50,270 and £125,140 (pay) and £12,570 and £50,270 (profit) and finds no fall at 2p or more. It also checks that each 1p fall is exactly 1p. Pinned by test_a_penny_more_profit_can_lower_net_earnings_by_a_penny and YAML Cases 30–31.
  7. Entered pension totals: pension_contributions entered as a total comes off earnings exactly as the same amount entered as a programme. Tested by test_a_pension_total_entered_directly_counts_like_a_programme and YAML Case 29.

  8. stored_pence has four properties, tested in test_stored_pence.py:

    • every whole-pence amount below £131,072 round-trips exactly (exhaustive);
    • the result is never more than half a float32 step from the stored amount;
    • a fraction more than half a step from a whole penny is kept exactly;
    • it is monotonic.

    stored_sum_pence recovers every float32 sum of up to five whole-pence amounts below £131,072, added one at a time as adds does (2 million sums, some of which stored_pence alone misses). It reads a total that is not such a sum as entered. Also YAML Case 32.

  9. Strict at the limit: earnings of exactly 52 × L a year are within it, in every year.

  10. The existing invariants (bounds, unearned income is not earnings, the pension-share exactness, within a penny of reg 5 rounding) still hold.

Tests

  • Every limit, in every year (2015 to 2041): test_a_penny_a_week_over_each_limit_is_over_it_in_every_year enters carers_allowance_earnings at 52L, 52L + £0.51 and 52L + £0.52. After the last legislated limit (2026-27) it takes the limit from the model, so a projection of the limit (as Project the Carer's Allowance earnings limit as 16 hours at the national living wage #2699 proposes) is covered rather than breaking the test, as MaxGhenis's heads-up on this PR suggested. test_pay_a_penny_a_week_over_each_limit_ends_entitlement enters pay, from 2015 to 2026; it stops at 2026 because from 2029 employment_income subtracts salary_sacrifice_broad_base_haircut.
  • Odd-pence pension contributions at every limit, 2015 to 2026, plus the Hypothesis property above.
  • Fractions of a penny in each deduction: test_each_deduction_counts_a_fraction_of_a_penny_as_a_penny, as above: 1,210 trading and employee pairs in 2024-25 and 2026-27 whose net earnings, deductions unrounded, lie from 4p below to 2p above the threshold. It adds C1 and the seven employee rows the review of 32f1639 found, each asserted to be exactly a penny a week over.
  • Monotonic net earnings at every penny across each threshold, the 1p exception, and entered pension totals, as above.
  • test_a_limit_in_pounds_and_pence_is_read_exactly: a reformed limit of £204.10.
  • YAML Cases 21–32, with expected values from the law: reg 8(1)'s £151 from 8 April 2024, £196 from 7 April 2025 and £204 from 6 April 2026; regs 5, 10(4) and 14; Class 1 at 8% above 52 × £241.73; and the CA rates of £81.90 and £86.45.
    • 21–24: 2024-25 pay of £7,852.51 (within) and £7,852.52 (over), and £7,852.52 and £7,852.53 each with a 1p contribution (earnings £7,852.51, within; £7,852.52, over).
    • 25: 2026-27 self-employed, £14,000.01 profit and a £6,039.38 premium (earnings £10,608.50, within, CA £4,495.40).
    • 26–28: employees with a whole-penny Class 1 (£175.56 in 2026-27 and 2025-26, £43.80 in 2024-25), each a penny a week over.
    • 29: a £1,000 pension total entered directly (within).
    • 30–31: the 1p pair.
    • 32: two pension programmes whose float32 sum is out by 0.0137p, more than half a step (over).
  • CSP YAML: two cases at the per-step boundary in Scotland, 2026-27 (£12,576.76 within, CSP £4,495.40; £12,576.77 over, CSP nil).

Each wrong rule fails at least one test. For each, the formula files were swapped with everything else at this head (r5/swap_checks.sh, r5/yaml_swaps.sh). The tests are the two CA property files and test_stored_pence.py, 37 tests in all.

Wrong rule Property tests failed (of 37) CA YAML cases failed CSP cases failed
Main's formulas 13: every-limit and pay-over tests, whole pence, odd pence, the differential, all four fraction cases, 2p monotonic (profit), the 1p pair, two entered-total cases 22, 25, 28, 31 both
a67d545's formulas (nearest half penny of the total) 10: odd pence, the differential, all four fraction cases, 2p monotonic, the 1p pair, two entered-total cases 25, 28, 31 both
32f1639's formulas (Class 1 read as stored, programmes only) 6: both employee fraction cases, all four entered-total cases 26, 27, 28, 29 none
Reg 5 on the total only (deductions unrounded, net floored) 6: all four fraction cases, 2p monotonic, the 1p pair 25, 31 within
No whole-pence recovery (stored_pence returns the exact value) 20 14, 24–28, 30–32 over, and two others
Deductions rounded down (reg 5 against the claimant) 10: the pairs property, odd pence, all four fraction cases, the 1p pair, three entered-total cases 23, 25, 28, 31 both
Earnings at the limit counted as over (>=) 14, including test_bounds_and_differential and the every-limit tests 3, 13, 21, 23, 25, 31 none
Class 1 read as stored (the exact formula never used) 2: both employee fraction cases 26, 27, 28 none
Float32 sums read as stored (stored_sum_pence never sums the parts) 2: both stored_sum_pence tests 32 none
Entered totals ignored (stored_sum_pence always sums the parts) 5: four entered-total cases, the entered stored_sum_pence test 29 none

Logs: r5/swaps.log (property tests and YAML at d4b8cb2) and r5/yaml_swaps.log (YAML case names at 8f1df68, which adds Case 32).

Review

  1. GPT-6.1 Sol reviewed ab644ac (REQUEST_CHANGES: odd-pence contributions judged over) and then approved a67d545 (~/reviews/uk-2541-ca-pence/review-sol.md, rereview-sol.md). Its nit, that the aggregate-impact sentence was stale, is answered below. Its one disputed claim, whole-pence recovery below £131,072 under the half-penny rule, was settled: that rule failed at £65,536.05. Under this head's rule whole pence are recovered below £131,072, checked exhaustively in test_stored_pence.py.
  2. juaristi22 reviewed a67d545 (REQUEST_CHANGES, C1: nearest-half-penny rounding put a self-employed carer within the limit over it). Fixed by applying reg 5 to each deduction, as above. The review's case is YAML Case 25 and an explicit row of the through-the-model test, and rulespec-uk#482's bridge is updated.
  3. MaxGhenis's heads-up (Project the Carer's Allowance earnings limit as 16 hours at the national living wage #2699 and Project the minimum wage rates by the OBR's national living wage forecast #2700 would project the limit and break the per-year test): adopted. Years after the last legislated limit read the model's limit, and the set-equality check covers the legislated years only.
  4. Independent review of 32f1639. It ran as subfleet run --task review --tier hard; no Codex lane admitted it, so Opus 5.5 served it. It was stopped at its six-hour limit before it finished. Its draft (REQUEST_CHANGES) and scripts are in ~/reviews/uk-2541-ca-pence/rereview2b-salvage/. Each finding was checked here, and all are addressed at this head:
    • Blocker, Class 1 float32 noise: a penny of Class 1 counted as two for some employees exactly a penny a week over. Class 1 is now worked exactly, the test expectation is independent, and the review's rows are tests and YAML Cases 26–28. This head is wrong on 0 rows of sweep_class1_exact, against 6 for 32f1639. The known-limits text is corrected.
    • Entered pension_contributions was ignored: the total is now read (stored_sum_pence), YAML Case 29.
    • CSP also reads these earnings: stated above, with two CSP YAML cases and the changelog.
    • Two test_stored_pence properties lacked deadline=None: added.
    • Invariant 3 was false at the penny: restated as monotonic in steps of 2p, checked at every penny across each threshold, and the 1p exception pinned as intended.
    • Nit, one rounding for two pension programmes: the half of the year's total, rounded once, is kept and logged as a reading (known limits). The model holds no individual payments, so rounding per programme would be no more literal.
    • Nit, PAYE and Class 1 are amounts deducted, not reg 5 calculations: the docs say so; rounding them up is the claimant-favourable convention. Nit, reg 11(1)(a) is cited for the self-employed weekly amount, here and in rulespec-uk#482.
    • PR body nits: the off-grid table header ("over the three years") and the effect text are corrected.
  5. A fresh independent review of this head (subfleet run --task review --tier hard) is pending.

Local runs. They use this worktree's own venv (uv sync --frozen --extra dev from the merged uv.lock; Python 3.13.9, policyengine-core 3.33.12, numpy 2.5.4, Hypothesis 6.170.11), with -n 4 and -o xfail_strict=true.

  • pytest at d4b8cb2, on every carer test module and test_stored_pence.py (11 files): 105 passed (6 min). 8f1df68 adds only YAML Case 32.
  • YAML at 8f1df68, policyengine-core test on every YAML file under policyengine_uk/tests/policy that mentions carers or care hours (66 files): 1,205 passed.
  • Sweeps (r5/, on trees extracted at each commit): sweep_offgrid.py and sweep_class1_exact.py as above. sweep_pension_pairs.py: 0 wrong of 59,532. sweep_exhaustive.py: identical to the earlier head (this head 0 in sets A–C). probe_years.py: this head misjudges no year from 2015 to 2028; main misjudges 2020 to 2024.
  • Class 1 tolerance study: 0 rows outside the tolerance (above).
  • Ruff: uvx ruff@0.17.1 and uvx ruff@latest (0.17.3), format --check . and check .: clean.

Aggregate impact

At 8f1df68: pending (CI is running; this section is updated when it reports).

CI's Aggregate impact report at 32f1639 (run 40589857208): base 6030cbc0ba, which is main and the first parent of the merge commit c95ea031b0 (its second parent is this head). Both sides ran on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1), fiscal years 2025-26 to 2031-32, with a full microsimulation each year on each side. No household's net income changed in any year. No receipts, spending, caseload, poverty, inequality or decile figure moved. Checks: 0 broken, 0 fixed, 1 failing on both sides (VAT sign, unrelated). No record in the survey data lies close enough to a boundary to change; the fix matters for individual carers a penny a week over the limit.

Reproducer: policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py::test_a_penny_a_week_over_each_limit_is_over_it_in_every_year, policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py::test_pay_a_penny_a_week_over_each_limit_ends_entitlement, policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py::test_each_deduction_counts_a_fraction_of_a_penny_as_a_penny, policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py::test_a_pension_total_entered_directly_counts_like_a_programme, policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml, policyengine_uk/tests/policy/baseline/gov/social_security_scotland/carer_support_payment_earnings_limit.yaml

axiom: TheAxiomFoundation/rulespec-uk#482 queued

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 20, 2026 17:01
is_gainfully_employed_for_carers_allowance floored the weekly pence in
float32, where the 1e-6 tolerance does nothing near 20,000 pence, so float32
rounding decided cases within about 0.002 pence of a boundary. Earnings of
52 x L + 0.52 a year (a penny a week over) counted as within the 2020-21 to
2024-25 limits of 128, 132, 139 and 151.

The formula now takes the stored annual earnings to the nearest penny in
float64 (an exact half penny disregarded, reg. 5), spreads them over 52
weeks in whole pence with the fraction disregarded (reg. 5), and compares
with the limit in pence (reg. 8(1)).

Tests: every limit in earnings_limit.yaml in every year it applies, entered
as earnings and as pay; a pounds-and-pence reformed limit; property tests
for exact whole-pence judgement, agreement with exact rational arithmetic
and monotonicity; YAML cases for 2024-25. The property-test helpers now
restate the new rule in Fraction arithmetic.

Fixes #2541

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base df239bcc21 against head 1995bd77f1, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

None.

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise additional rate by 3pp 0 0 0 0 0 suppressed suppressed

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise additional rate by 3pp: change in income_tax 0 0 0 0 0 suppressed suppressed

Household net income by decile

No household's net income changes in any year.

No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-53,889,156 in total; 2026-27: 76 units below zero, a weighted £-55,420,387 in total; 2027-28: 76 units below zero, a weighted £-56,618,038 in total
Counted-once check

Each of 91 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main df239bcc21 uk 2.163.0 (files 6a573e7e11a1), core 3.33.12, py 3.13.18 aarch64 79e32045b647 1eac2ca06b91 1836.09s
head #2543 merged into main 1995bd77f1 uk 2.163.0 (files 10b1f717ec16), core 3.33.12, py 3.13.18 aarch64 79e32045b647 1eac2ca06b91 1904.74s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Final charge on cash interest in non-cash ISAs £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Lifetime ISA withdrawal charge £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Income tax £298.70bn £318.31bn £335.60bn £350.52bn £365.79bn £383.53bn £401.03bn
National Insurance (employee and self-employed) £52.37bn £54.71bn £56.48bn £58.15bn £60.22bn £62.11bn £64.44bn
National Insurance (employer) £150.59bn £156.99bn £161.74bn £166.13bn £173.24bn £178.59bn £185.29bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.61bn £13.05bn £13.53bn £14.02bn £14.52bn £15.05bn £15.58bn
Student loan repayments £7.91bn £8.21bn £8.43bn £8.63bn £8.85bn £9.10bn £9.42bn
VAT excluding private school fees £249.37bn £255.28bn £261.81bn £267.87bn £274.12bn £280.56bn £286.81bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.00bn £2.05bn £2.10bn £2.21bn £2.21bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.53bn £24.64bn £26.64bn £26.69bn £26.42bn £25.76bn £26.35bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.41bn £4.42bn £4.44bn £4.45bn £4.47bn £4.48bn
Council tax £49.21bn £51.88bn £54.97bn £58.29bn £61.55bn £64.92bn £68.49bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £38.04bn £38.88bn £39.76bn £43.07bn £43.13bn £44.57bn
Stamp duty land tax on the household's own purchases £6.90bn £7.39bn £7.81bn £8.21bn £8.62bn £9.02bn £9.42bn
Stamp duty land tax paid by companies £4.81bn £5.98bn £6.73bn £7.46bn £8.21bn £8.87bn £9.16bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.09bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.34bn £0.35bn £0.37bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.64bn £126.07bn £127.08bn £128.87bn £132.52bn £136.31bn £140.08bn
Pension Credit £6.66bn £7.04bn £7.32bn £7.46bn £7.60bn £7.77bn £7.94bn
Winter Fuel Payment £1.86bn £1.86bn £1.80bn £1.78bn £1.79bn £1.79bn £1.80bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.21bn £80.38bn £82.93bn £84.23bn £85.58bn £86.87bn £87.97bn
Housing Benefit £8.55bn £8.89bn £9.17bn £9.31bn £9.58bn £9.84bn £10.10bn
Council Tax Reduction £2.07bn £2.18bn £2.32bn £2.46bn £2.60bn £2.74bn £2.89bn
Employment and Support Allowance (income-related) £0.79bn £0.81bn £0.83bn £0.85bn £0.86bn £0.89bn £0.90bn
Employment and Support Allowance (contribution-based) £5.20bn £5.34bn £5.46bn £5.59bn £5.72bn £5.85bn £5.99bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.30bn £0.31bn £0.32bn £0.32bn £0.33bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.94bn £28.06bn £29.13bn £29.83bn £30.54bn £31.26bn £31.96bn
Disability Living Allowance £7.49bn £7.80bn £8.10bn £8.29bn £8.49bn £8.69bn £8.88bn
Attendance Allowance £8.68bn £9.04bn £9.38bn £9.61bn £9.83bn £10.07bn £10.29bn
Carer's Allowance £4.48bn £4.67bn £4.84bn £4.96bn £5.08bn £5.19bn £5.31bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.53bn £0.54bn £0.55bn £0.57bn £0.58bn
Armed Forces Compensation Scheme £0.61bn £0.62bn £0.64bn £0.65bn £0.67bn £0.68bn £0.70bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.16bn £16.83bn £17.43bn £17.85bn £18.26bn £18.69bn £19.10bn
Scottish Child Payment £0.49bn £0.52bn £0.54bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.39bn £3.47bn £3.55bn £3.63bn £3.72bn £3.80bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.15bn £1.18bn £1.21bn £1.24bn £1.27bn £1.29bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.70bn £0.72bn £0.73bn £0.75bn £0.77bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.55bn £0.56bn £0.57bn £0.58bn £0.59bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.60bn £3.67bn £3.77bn £3.85bn £3.94bn
Universal childcare entitlement £2.05bn £2.23bn £2.28bn £2.34bn £2.40bn £2.47bn £2.52bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.53bn £0.54bn £0.56bn £0.52bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.30bn £0.31bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.83bn £204.54bn £205.13bn £205.79bn £206.49bn £207.23bn £207.73bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.13bn £25.21bn £25.29bn £25.37bn £25.46bn £25.53bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,261k 11,158k 10,849k 10,746k 10,783k 10,822k 10,848k
Pension Credit 1,485k 1,483k 1,480k 1,461k 1,462k 1,464k 1,460k
Winter Fuel Payment 7,842k 7,793k 7,494k 7,407k 7,432k 7,459k 7,477k
Pension Age Winter Heating Payment (Scotland) 803k 803k 803k 800k 803k 806k 808k
Universal Credit 6,334k 6,341k 6,366k 6,347k 6,345k 6,333k 6,298k
Housing Benefit 1,249k 1,247k 1,250k 1,234k 1,238k 1,240k 1,239k
Council Tax Reduction 3,175k 3,198k 3,209k 3,229k 3,244k 3,257k 3,264k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 72k 72k
Employment and Support Allowance (contribution-based) 719k 721k 723k 726k 728k 731k 732k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 68k 68k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,566k 3,579k 3,589k 3,601k 3,613k 3,626k 3,635k
Disability Living Allowance 1,291k 1,295k 1,299k 1,303k 1,308k 1,312k 1,316k
Attendance Allowance 1,694k 1,700k 1,705k 1,711k 1,716k 1,723k 1,727k
Carer's Allowance 1,034k 1,038k 1,041k 1,044k 1,048k 1,051k 1,053k
Carer Support Payment (Scotland) 71k 71k 72k 72k 72k 72k 73k
Carer's Allowance Supplement (Scotland) 0k 71k 72k 72k 72k 72k 73k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 138k 138k 139k 139k 140k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 95k 95k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,102k 8,128k 8,142k 8,165k 8,191k 8,216k 8,232k
Scottish Child Payment 350k 352k 353k 357k 354k 355k 342k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 199k 200k 201k 201k 202k 203k 203k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 332k 334k 335k 336k 337k 338k 339k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 183k 183k 184k 185k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 27k 27k
Bereavement Support Payment 98k 98k 98k 99k 99k 99k 100k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,014k 1,024k 1,024k 1,027k 1,027k 1,031k 1,013k
Extended childcare entitlement 796k 801k 806k 809k 815k 819k 820k
Universal childcare entitlement 1,071k 1,074k 1,077k 1,080k 1,086k 1,096k 1,097k
Targeted childcare entitlement 165k 156k 154k 155k 155k 156k 144k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 99k 99k 99k 100k 100k
Adult Dependants' Grant 18k 18k 12k 12k 12k 13k 13k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £898.82bn £941.86bn £980.11bn £1,013.84bn £1,052.89bn £1,089.01bn £1,129.10bn
Government spending £528.95bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.38bn †
Government balance, each receipt once £369.87bn † £398.03bn † £428.43bn † £455.77bn † £486.32bn † £513.70bn † £545.72bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £898.82bn £941.86bn £980.11bn £1,013.84bn £1,052.89bn £1,089.01bn £1,129.10bn
household_tax £496.91bn £527.60bn £554.51bn £577.74bn £603.32bn £627.65bn £654.69bn
pre_budget_change_household_tax £489.00bn £519.39bn £546.08bn £568.63bn £593.98bn £618.05bn £644.75bn
gov_spending £528.95bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.38bn †
household_benefits £528.95bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.38bn †
gov_balance £369.87bn † £398.03bn † £428.43bn † £455.77bn † £486.32bn † £513.70bn † £545.72bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,651.49bn £1,713.03bn £1,764.05bn £1,812.97bn £1,862.19bn £1,918.39bn £1,985.28bn
Household net income £1,644.42bn † £1,688.69bn † £1,719.55bn † £1,750.62bn † £1,781.68bn † £1,821.07bn † £1,867.39bn †
Household net income (HBAI definition) £1,457.29bn £1,504.48bn £1,537.58bn £1,569.36bn £1,602.67bn £1,640.49bn £1,687.04bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,224k 69,466k 69,668k 69,891k 70,128k 70,381k 70,550k
Benefit units 35,100k 35,223k 35,325k 35,438k 35,558k 35,686k 35,772k
Households 31,037k 31,145k 31,236k 31,336k 31,442k 31,555k 31,631k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.0% 15.1% 15.4% 15.5% 15.6% 15.7% 15.7%
Absolute poverty, before housing costs: children 24.0% 21.1% 21.3% 21.3% 21.5% 21.8% 21.8%
Absolute poverty, before housing costs: working-age adults 13.8% 13.5% 13.9% 14.1% 14.1% 14.2% 14.2%
Absolute poverty, before housing costs: pensioners 13.7% 13.2% 13.1% 13.4% 13.5% 13.5% 13.5%
Absolute poverty, after housing costs: everyone 21.4% 20.8% 21.3% 21.5% 21.7% 21.9% 22.2%
Absolute poverty, after housing costs: children 33.0% 30.7% 31.1% 31.1% 31.4% 31.7% 32.3%
Absolute poverty, after housing costs: working-age adults 19.8% 19.6% 20.2% 20.5% 20.7% 20.8% 21.1%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 12.3% 12.5% 12.8% 12.9% 13.0%
Relative poverty, before housing costs: everyone 17.2% 16.4% 16.5% 16.4% 16.2% 16.3% 16.5%
Relative poverty, before housing costs: children 26.2% 23.5% 23.4% 23.1% 22.6% 22.8% 23.1%
Relative poverty, before housing costs: working-age adults 14.6% 14.4% 14.7% 14.7% 14.6% 14.7% 14.8%
Relative poverty, before housing costs: pensioners 15.0% 14.8% 14.1% 14.1% 14.0% 14.0% 14.2%
Relative poverty, after housing costs: everyone 21.6% 21.0% 21.1% 21.2% 21.3% 21.4% 21.8%
Relative poverty, after housing costs: children 33.3% 31.2% 30.8% 30.7% 31.0% 31.0% 31.5%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.1% 20.3% 20.4% 20.5% 20.7%
Relative poverty, after housing costs: pensioners 12.9% 12.5% 12.1% 11.9% 11.8% 11.9% 12.8%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,057k 10,466k 10,734k 10,860k 10,960k 11,054k 11,081k
Absolute poverty, before housing costs: children 3,580k 3,171k 3,210k 3,220k 3,261k 3,305k 3,317k
Absolute poverty, before housing costs: working-age adults 5,853k 5,746k 6,021k 6,123k 6,169k 6,209k 6,219k
Absolute poverty, before housing costs: pensioners 1,625k 1,550k 1,504k 1,517k 1,530k 1,539k 1,545k
Absolute poverty, after housing costs: everyone 14,847k 14,422k 14,808k 14,994k 15,242k 15,402k 15,638k
Absolute poverty, after housing costs: children 4,937k 4,609k 4,671k 4,685k 4,757k 4,814k 4,912k
Absolute poverty, after housing costs: working-age adults 8,406k 8,359k 8,732k 8,898k 9,024k 9,114k 9,240k
Absolute poverty, after housing costs: pensioners 1,504k 1,454k 1,404k 1,411k 1,460k 1,475k 1,486k
Relative poverty, before housing costs: everyone 11,896k 11,403k 11,468k 11,487k 11,373k 11,468k 11,652k
Relative poverty, before housing costs: children 3,918k 3,523k 3,519k 3,491k 3,419k 3,458k 3,516k
Relative poverty, before housing costs: working-age adults 6,202k 6,136k 6,338k 6,400k 6,366k 6,415k 6,515k
Relative poverty, before housing costs: pensioners 1,776k 1,744k 1,611k 1,597k 1,588k 1,595k 1,621k
Relative poverty, after housing costs: everyone 14,957k 14,597k 14,698k 14,825k 14,951k 15,042k 15,351k
Relative poverty, after housing costs: children 4,976k 4,677k 4,630k 4,634k 4,690k 4,712k 4,794k
Relative poverty, after housing costs: working-age adults 8,443k 8,455k 8,680k 8,847k 8,916k 8,977k 9,092k
Relative poverty, after housing costs: pensioners 1,537k 1,465k 1,388k 1,344k 1,346k 1,354k 1,464k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3085 0.3059 0.3065 0.3071 0.3075 0.3081 0.3094
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,781 † £17,906 † £17,832 † £17,905 † £18,142 † £18,339 † £18,483 †
Decile 2: mean household_net_income per household £27,962 † £28,917 † £28,654 † £28,906 † £29,096 † £29,646 † £30,235 †
Decile 3: mean household_net_income per household £34,380 † £35,463 † £36,065 † £36,509 † £36,850 † £37,338 † £38,043 †
Decile 4: mean household_net_income per household £38,785 † £40,713 † £41,873 † £42,413 † £43,145 † £43,906 † £44,167 †
Decile 5: mean household_net_income per household £43,788 † £44,626 † £45,606 † £46,289 † £47,328 † £48,286 † £49,711 †
Decile 6: mean household_net_income per household £48,210 † £49,525 † £50,736 † £51,914 † £52,203 † £53,174 † £54,203 †
Decile 7: mean household_net_income per household £56,218 † £57,224 † £57,764 † £58,010 † £59,040 † £60,061 † £61,765 †
Decile 8: mean household_net_income per household £69,150 † £70,533 † £71,800 † £73,075 † £74,092 † £75,540 † £77,427 †
Decile 9: mean household_net_income per household £79,490 † £81,421 † £82,828 † £84,135 † £85,539 † £87,053 † £89,944 †
Decile 10: mean household_net_income per household £117,581 † £120,296 † £121,933 † £124,163 † £126,045 † £128,342 † £131,110 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.47bn £6.76bn £6.67bn £6.86bn £7.06bn £7.35bn £7.55bn
Raise higher rate by 1pp £3.69bn £3.98bn £4.03bn £4.29bn £4.46bn £4.78bn £4.91bn
Raise personal allowance by ~800GBP/year £-3.82bn £-3.94bn £-4.01bn £-4.09bn £-4.18bn £-4.18bn £-2.00bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.56bn £-1.46bn £-1.35bn £-1.23bn £-1.12bn
Reduce Universal Credit taper rate to 20% £-20.39bn £-20.96bn £-21.38bn £-21.61bn £-21.87bn £-22.17bn £-22.60bn
Raise Class 1 main employee NICs rate to 10% £11.02bn £11.42bn £11.77bn £12.07bn £12.37bn £12.75bn £13.07bn
Raise VAT standard rate by 2pp £24.89bn £25.55bn £26.14bn £26.75bn £27.39bn £28.03bn £28.66bn
Raise additional rate by 3pp £2.96bn £3.21bn £3.26bn £3.46bn £3.65bn suppressed suppressed
Raise the Universal Credit standard allowance by 10% £-3.05bn £-3.18bn £-3.30bn £-3.34bn £-3.38bn £-3.42bn £-3.48bn
Raise the State Pension by 10% £-7.79bn £-8.02bn £-8.01bn £-8.12bn £-8.43bn £-8.54bn £-8.87bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.61bn £6.91bn £6.83bn £7.03bn £7.24bn £7.47bn £7.73bn
Raise higher rate by 1pp: change in income_tax £3.69bn £3.99bn £4.04bn £4.25bn £4.47bn £4.74bn £4.93bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.95bn £-4.08bn £-4.17bn £-4.24bn £-4.34bn £-4.41bn £-1.76bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.95bn £1.84bn £1.73bn £1.62bn £1.50bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.39bn £20.95bn £21.37bn £21.62bn £21.87bn £22.22bn £22.61bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.28bn £11.74bn £12.06bn £12.37bn £12.69bn £13.03bn £13.49bn
Raise VAT standard rate by 2pp: change in vat £24.69bn £25.35bn £25.94bn £26.54bn £27.17bn £27.81bn £28.43bn
Raise additional rate by 3pp: change in income_tax £2.96bn £3.21bn £3.26bn £3.46bn £3.66bn suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.05bn £3.18bn £3.29bn £3.36bn £3.43bn £3.48bn £3.52bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.18bn £11.31bn £11.55bn £12.00bn £12.44bn £12.94bn

MaxGhenis and others added 4 commits October 21, 2026 05:10
The #2543 review found that taking stored earnings to the nearest penny
misjudges whole-pence pay less half of an odd-pence pension contribution.
Net earnings of 52 x L + 0.515 are 52 x L + 0.51 in law, because reg. 5
counts the half penny of the reg. 10(4)(b) deduction as a penny. Float32
holds that value just above the half penny for most limits, so nearest
penny counted it up and over the limit, at all ten limits.

The formula now takes the nearest half penny (an exact quarter penny down),
disregards a half penny, then divides by 52 in whole pence (1996 reg.
8(1)(b)(iii) with reg. 5). np.floor(x / n) replaces // so that infinite
earnings stay over the limit. Every whole-pence and half-penny amount below
65,536 pounds is recovered exactly.

Tests: odd-pence contributions at every limit in every set year, a
Hypothesis property checking pay less half a contribution against the law
in integer arithmetic, YAML Cases 23 and 24, float32 values either side of
52L + 0.515 and 52L + 0.5175 as explicit examples, and boundary examples
for every #2541 limit. The Fraction references follow the new rule.
changelog.d/2532.fixed.md (from #2534) no longer describes this head, so it
is removed; the wording fixes the review asked for are included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22 juaristi22 self-assigned this Oct 22, 2026

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review status: COMPLETE
Review severity: REQUEST_CHANGES
Reviewed head: a67d545

C1 — Nearest-half-penny rounding can remove the entire allowance for valid self-employed earnings. In is_gainfully_employed_for_carers_allowance.py:61–63, the new step can round computed net earnings upward even when the supplied income and pension inputs are whole-pence amounts.

Executed case: an otherwise eligible 45-year-old carer in England in 2026, providing 40 qualifying care hours weekly, with £14,000.01 self-employment profit, £6,039.38 personal pension premiums, no employed income and no overlapping benefit. No eligibility or intermediate amount is overridden.

Both the current calculation and a prior-formula-only control produce stored net earnings £10,608.517578125. The new formula promotes this to £10,608.52 and pays £0 Carer’s Allowance; reverting only that formula pays £4,495.40.

Computation of Earnings regulation 5 requires fractional pennies to favour the claimant. With regulations 13 and 14, claimant-favourable deductions are £286.01 tax, £85.81 Class 4 and £3,019.69 pension. Annual earnings are £10,608.50, giving £204.00 weekly after disregarding the fraction. This is within the strict 2026 earnings limit, so the £4,495.40 award remains due. Rounding a combined deduction instead leaves £10,608.51 and the same eligibility; even the existing unrounded approximation is below £204.01 a week.

Preserve the whole-pence and odd-pension fixes while ensuring computed off-grid earnings receive claimant-favourable rounding. Add this actual-input regression and update the corresponding bridge/case in queued Axiom issue #482. The disclosed generic approximation does not cover this demonstrated full-award regression, and the prior-formula control isolates this PR's change.

Validation: 66 Python tests and 24 changed YAML cases pass, with no failures or skips. The original interrupted run was recovered with native exit 0; it was not repeated. The additional current/prior diagnostic separately exited 0. Twenty source originals and extracts were checked. Review scope is GitHub's five actual PR files, with the comparison base qualified independently of the shallow local history. No local population run, full suite or proposed repair is claimed.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Heads-up on a semantic interaction with #2699 and #2700. There's no file overlap.

test_a_penny_a_week_over_each_limit_is_over_it_in_every_year builds LIMITS_BY_YEAR from the YAML values in force on 30 April up to HORIZON. _penny_steps_misjudged then asserts that system.parameters(f"{year}-01-01").gov.dwp.carers_allowance.earnings_limit == limit, and the test asserts the set of limits equals the YAML's. Once #2700 lands, both fail from 2027.

Suggested change: take years after the last YAML date from the model, which keeps the "every limit in every year" coverage:

def _limits_by_year(last_year):
    limits = []
    for year in range(LIMIT_VALUES[0][0].year, last_year + 1):
        if year <= LIMIT_VALUES[-1][0].year:
            limit = [v for date, v in LIMIT_VALUES if date <= datetime.date(year, 4, 30)][-1]
        else:
            limit = system.parameters(f"{year}-01-01").gov.dwp.carers_allowance.earnings_limit
        limits.append((year, limit))
    return limits

Restrict the set-equality assertion to SET_YEARS. The penny steps (52L, 52L + £0.51, 52L + £0.52) are just as useful at the projected limits.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 27, 2026 14:40
juaristi22's review of #2543 found a trader within the limit whom the
nearest-half-penny rule put over it: £14,000.01 of profit and a £6,039.38
premium leave £10,608.5174 unrounded, which the half-penny step took to
£10,608.52 (£204.01 a week). Reg. 5 counts a fraction of a penny in each
deduction as a penny, giving £10,608.50.

carers_allowance_earnings now works in float64 pence from the whole pence
each amount was entered as (utils.pence.stored_pence), taking pay and
profit down to a whole penny and each deduction (PAYE, Class 1, notional
tax, Class 4, Class 2, half of pension contributions) up to one. Net
earnings are whole pence, which is_gainfully_employed_for_carers_allowance
reads back exactly before dividing by 52.

Tests: stored_pence round-trips every whole-pence amount below £131,072
and moves an amount at most half a float32 step; a through-the-model test
checks trading and employee net earnings against the law, deduction by
deduction, in exact arithmetic, with the review case; YAML Case 25 pins
it. The limits test now reads projected limits from the model after the
last legislated one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pence

# Conflicts:
#	policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

@juaristi22, thanks. C1 was right, and it went deeper than the half-penny step. Rounding the stored total can't separate whole-pence earnings from genuine fractions of a penny: float32 holds a £14k–£20k profit only to within about a tenth of a penny before tax comes off, so the two overlap. Even a two-step tolerance misjudged 39 of 1,779 traders within 5p of the 2026-27 boundary.

So 32f1639 applies reg 5 where the calculation happens, at each step:

  • carers_allowance_earnings works in float64 pence from the whole pence each input was entered as (policyengine_uk/utils/pence.py:stored_pence). Pay and profit are taken down to a whole penny. Each deduction is taken up to one: PAYE, Class 1, notional tax, Class 4, Class 2 and half of pension contributions. Net earnings are therefore whole pence.
  • is_gainfully_employed_for_carers_allowance reads those pence back exactly, divides by 52 with the fraction disregarded, and tests strictly against the limit.

Your case is £14,000.01 of profit and a £6,039.38 premium in 2026-27. The deductions are notional tax £286.002, rounded up to £286.01; Class 4 £85.8006, rounded up to £85.81; and half the premium, £3,019.69. That leaves £10,608.50, or £204.0096 a week, which is not over £204, so CA is £4,495.40.

It is now covered in three places:

  • YAML Case 25.
  • An explicit row of the new test_each_deduction_counts_a_fraction_of_a_penny_as_a_penny, which also checks 1,210 trading and employee pairs (2024-25 and 2026-27) with fractions in their deductions against exact arithmetic, deduction by deduction.
  • sweep_offgrid.py: 0 wrong at this head, against 455 and 115 on main and 609 and 219 at a67d545.

The PR body has the full rule, the sweeps and the wrong-rule checks. rulespec-uk#482's bridge and companion row are updated to match.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Checked: at 32f1639 this merges cleanly onto #2699 (3c19621) and onto #2700 (21f0fa85f). Its CA tests pass on both:

That includes #2700, where the limit rises to £212 from 2027-28. The _limits_by_year change covers it, so the earlier heads-up no longer applies.

MaxGhenis and others added 4 commits October 28, 2026 06:23
Review of #2543 at 32f1639 (GPT-6.1 Sol lanes closed; Opus reviewer):

- Class 1: the model sums twelve float32 months, which can hold a
  whole-penny total (£175.56) further out than stored_pence recovers, so
  rounding up deducted a penny too much and put employees exactly a penny
  a week over the limit within it. carers_allowance_earnings now works the
  model's Class 1 formula exactly on the year's NI income in whole pence,
  where it agrees with the stored total to within float32 error, and
  otherwise reads the stored total.
- Pension: read pension_contributions, summing its programmes again in
  float64 only where the stored total is their float32 sum
  (stored_sum_pence), so a total entered directly still comes off.
- Docs: PAYE and Class 1 rounding is the claimant-favourable convention,
  reg. 5 governs the calculated deductions; the self-employed weekly
  amount cites reg. 11(1)(a); CSP reads these earnings too.
- Tests: employee-route Class 1 expectation worked exactly, with the
  review's rows; invariant 3 restated (2p rises never lower net earnings,
  a 1p rise can, as reg. 5 intends) and checked over every penny across
  each threshold; entered pension totals; stored_sum_pence; deadline=None
  on the stored_pence property tests; YAML Cases 26-31 and two CSP cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A float32 sum of the two programmes can lie more than half a step from
its pence; read alone, half of it would round up a penny too far. This
case fails if stored_sum_pence reads a sum as stored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the three findings of the review of 8f1df68, and a fourth of the
same kind found while fixing them:

- Class 1 entered as an input (nil, or any amount near the formula's) was
  replaced by the formula worked exactly whenever the two lay within
  float32 error. Class 1 is now worked out again only where the model
  calculated it with its own formula: no reform changed how it or its
  parts are calculated, and no month was supplied or stored by a scenario.
  Otherwise the stored amount is the one deducted (reg. 10(4)(a)).
- Sums nested in sums (employee_pension_contributions inside
  pension_contributions, and employment_income itself) kept float32 noise.
  amount_pence now recurses through adds/subtracts of amounts the
  simulation derived, reading each as stored_pence does, and takes their
  exact sum where the float32 total lies within the rounding of the
  additions that formed it. Pay is read this way too: 10,276.42 + 1,415.69
  is stored as 11,692.109375 and was floored to 11,692.10.
- A formula in place of a sum (adds=None) crashed the reader; a variable
  with a formula, defined_for, another period or another entity's part is
  now read as stored.

YAML Cases 33-36 pin the four executed cases, each a penny a week over the
2026-27 limit; model-level tests pin entered and reformed Class 1, a
parameter reform, a formula reform and 1,203 sums the model forms (600
with Class 1, 603 without NI), and the
every-year test checks Class 1 against an independent Fraction restatement
from 2015 to the horizon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three of the 22 wrong rules swapped in at 6e8a53f failed no test:

- Employee pension contributions read as stored for PAYE.
  test_sums_the_model_forms_are_read_in_whole_pence gains a PAYE regime:
  600 sets of inputs with pay less member contributions over the personal
  allowance, mostly by a multiple of 5p, so that 20% of it is whole pence
  and contributions misread by a fraction of a penny change the tax.
- The reader's input check removed. A pension total entered as
  £3,242.6401 is the very float32 that programmes of £1,807.49 and
  £1,435.15 sum to; only the input check tells them apart, and the entered
  total is read as entered (£10,608.51, within) where the programmes alone
  give £10,608.52 (over, CA Case 32).
- The reader's formula check removed. policyengine-core refuses a variable
  with both a formula and adds, so the check could never be true; it is
  dropped, and a test pins the core behaviour it relied on.

The fourth survivor, the Class 1 tolerance backstop, is intended: the
exact path restates the model's own formula, so the two can only disagree
if one is edited without the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Heads-up from #2719 (Budget 2026 measures, draft until Budget week ends): its Carer's Allowance taper commit (b5d2d77) moves the weekly-pence step out of is_gainfully_employed_for_carers_allowance into a new variable, carers_allowance_weekly_earnings_above_limit. That variable feeds both the gainful-employment test and the 100% taper. If #2719 lands first, this PR's pence change belongs in that new variable, and the taper picks it up too.

🤖 Generated with Claude Code

MaxGhenis and others added 4 commits October 28, 2026 13:04
Fixes the r7 review's findings on PR #2543 (refs #2541):

- Class 1 entered for the year (which core divides among the months in
  float32) or month by month was read from the float32 annual sum, so
  £1.20 (or 12 x 10p) was deducted as £1.21. amount_pence now sums a
  monthly flow's months exactly (months_summed), recovering a year's
  whole pence within the rounding of the division; the Class 1 fallback
  and backstop read it that way.
- The exact Class 1 path accepted a reform that ended the primary
  formula or dated it from 2027, since it compared formula code alone.
  It now compares, month by month, the formula core runs (code, the
  globals it reads, defaults, closure), the variables' other calculation
  metadata against the model's own classes, abolition by gov.abolitions,
  and how the formulas read NI income, liability and directorship.
- A sum, or Class 1, that the cache policy declines to keep counted as
  supplied (is_derived is false for an unstored value). Inputs are now
  told by the holder's input periods.

Tests: the reviewer's reproducers, 400-carer sampled property tests of
Class 1 entered in whole pence (year and months), reforms that end,
re-date, abolish or hold in whole pounds the primary part, run its code
with another global value, or change how it reads liability or NI
income, cache-policy invariance, and months_summed invariants
(exhaustive recovery of a year's whole pence below £32,768).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The r7 review's reproducer: £1.20 entered with £10,609.72 of pay leaves
£10,608.52, a penny a week over, not £10,608.51.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tment

The r7 review noted that _natural_rows called its reference legal net
earnings, though the model counts statutory sick pay in NI income only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

Carer's Allowance: a penny a week over the 2020-21 to 2024-25 earnings limits counts as within the limit

2 participants