Skip to content

Fix Class 4 NI dropping the additional-rate band at non-round thresholds - #1884

Merged
MaxGhenis merged 3 commits into
mainfrom
fix-class-4-float32-cap
Sep 27, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
fix-class-4-float32-cap

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1878

What was wrong

With CPI-uprated (non-round) Class 4 thresholds, ni_class_4 dropped the whole 2% additional-rate band for many sole traders above the Upper Profits Limit. Example: £65,432 of profits in 2028 gave £2,353.38; s.15(3) SSCBA 1992 gives £2,616.00 at the model's 2028 limits (LPL £13,077.80, UPL £52,300.78).

ni_class_4_maximum (reg 100 of the Social Security (Contributions) Regulations 2001) chose Case 1 when step_4_raw > class_1 + class_2 + ni_class_4_main. For someone with no Class 1 or Class 2 and profits at or above the UPL, both sides equal 6% × (UPL − LPL) exactly. They were computed in float32 by different paths, though, so rounding picked the case. When Step Four rounded one ulp high, Case 1 applied. The maximum became Step Four alone, the main-band amount, and min(pre_maximum, maximum) removed the additional-rate band.

#1879 (NICs thresholds uprated before the freeze ends in April 2031) will make the default 2027–2030 limits round again. The fix still matters then: any reform that uprates, unfreezes or otherwise sets non-round Class 4 limits (a plausible Budget option) puts every sole trader above the UPL back on the float32 knife edge.

Changes

  • ni_class_4_maximum: Case 1 test without the structural knife edge. Main-rate Class 4 is Step Two less the main rate on the unused part of the main band. Case 1 is therefore equivalent to main_rate × clamp(UPL − class_4_profits, 0, UPL − LPL) + 53 × weekly Class 2 > 2 × (primary Class 1 + Class 2), evaluated in float64.
    • At or above the UPL the unused band is exactly 0, so the equality that rounding used to decide is now the exact comparison 0 > 0 (from April 2024 there are no Class 2 terms).
    • A genuine coincidence in the data (53 × weekly Class 2 = 2 × (Class 1 + Class 2), only possible before April 2024) is still decided by the float64 comparison, as in any floating-point implementation. With exactly representable amounts it returns the statute's answer: Case 2 when Step Four does not exceed the aggregate (tested).
    • All other steps are unchanged.
  • Class 2 drops out of reg 100 from 6 April 2024. New dated parameter gov.hmrc.national_insurance.class_4.annual_maximum.includes_class_2: true from 2003, false from 2024-04-06, per SI 2024/377 reg 6(5). It removes Class 2 from Step Three, Step Four and the Case 1 aggregate. Baseline results don't change, since the Class 2 rate is 0 from then; explicit or reformed Class 2 no longer reduces or triggers the maximum.
  • ni_class_4: reg 100(1) gate. The maximum applies only where primary Class 1 contributions are also payable, or, before 6 April 2024, Class 2. This matches the in-force text and the 2023-04-06 text ("primary Class 1 contributions or Class 2 contributions").
  • ni_class_4_maximum: Step Five no longer divides by zero. A reform setting gov.hmrc.national_insurance.class_4.rates.main to 0 made ni_class_4_maximum, ni_class_4 and national_insurance NaN for every person. The headroom is now unlimited when the main rate is zero.
  • Docs: the NI notebook said Class 4 is "capped at the published Class 4 maximum". It now describes the reg 100 maximum and when it applies.
  • hypothesis added to the dev extras.

Invariants

These hold for any 0 ≤ LPL < UPL, rates ≥ 0 and profits ≥ 0. Comparisons allow float32 storage: 1p + 4×10⁻⁷ × the largest amount involved.

  1. Statute when the maximum can't bind. With no primary Class 1 or Class 2 payable, ni_class_4 = main × clamp(p − LPL, 0, UPL − LPL) + additional × max(p − UPL, 0).
  2. Differential. For everyone, ni_class_4 equals an exact-rational (fractions.Fraction) implementation of s.15(3) capped by the literal reg 100 steps, including the original Case 1 comparison, run on the model's own Class 1/2 inputs. The one exception is a Case 1 tie that holds in real numbers but not in binary. It's only possible before April 2024, with the UPL below the small profits threshold; there the model follows the real-valued tie.
  3. Bounds. 0 ≤ ni_class_4 ≤ the pre-maximum amount; the maximum only reduces liability.
  4. Monotonicity. With employment income fixed, ni_class_4 is non-decreasing in self-employment profits wherever Class 2 does not change. Before April 2024, reg 100 subtracts Class 2, so crossing the small profits threshold can lower Class 4, as the statute intends; ni_class_2 + ni_class_4 is still non-decreasing when main rate ≥ additional rate.
  5. No change where the bug did not bite. This rests on exact old-vs-new comparisons in the sweeps below; the property-test tolerances (e.g. £0.81 at £2m of profits) can't establish it.
    • In the dual-earner grid, no one paying Class 1 changed. Class 1 below float32 noise (e.g. £0.0001) is on the same knife edge, and the fix corrects it.
    • Sole traders who weren't affected move only by float32 rounding, because the gate now returns the pre-maximum amount where the old cap sat an ulp or two below it. The largest such move in the £0–£500k sweeps is £0.0042; an independent reviewer found £0.0078, two ulps of a £43,575 liability, at £2.1m of profits.
    • Baseline results in every year are unaffected by the April 2024 Class 2 change.

The reference, like the model, still defines Class 4 profits as self-employment income less employee Class 1 NI. That definition has no statutory basis (SSCBA 1992 Sch 2 paras 2–3) and is fixed in a follow-up PR, so this PR can be verified as changing nothing else.

Tests

  • test_ni_class_4_maximum.py example tests:
    • £65,432 in 2028 at default parameters, and pinned to the 2028 non-round thresholds (£2,616.00).
    • Pinned 2027 (£100,000 → £3,281.73) and 2030 (£300,000 → £7,360.18) cases.
    • The first affected profit level, £52,302.
    • The zero-main-rate reform.
    • Class 2 with no Class 1: capped in 2023 (£20), not capped in 2024 or 2026 (£24).
    • The Case 1 "exceeds" boundary with exactly representable amounts.
  • test_ni_class_4_properties.py: Hypothesis tests for invariants 1–4, with random LPL, UPL, rates, Class 2 flat-rate reforms, years 2022–2030 (including 2024), and profits including points on and next to the limits. CI runs 25 derandomised examples per property.
  • On main, 8 of the 12 example tests fail. The rest are 2 pre-existing tests, the 2023 Class 2 case (pre-2024 law is unchanged) and the boundary pin; the old code happens to get those right. Both property tests fail too.
  • The property tests catch the float32 defect without relying on the NaN. On main with only the zero-main-rate guard back-ported, both still fail on float32 case-selection counterexamples. Examples:
    • Monotonicity: a 2026 sole trader, LPL £0, UPL £102,700.77, main 16.47%, additional 2.96%. Profits of £645,319 give £16,918.25, below the £19,963.02 given for a lower profit level in the same sorted list.
    • Differential: a 2027 sole trader who owes £30,585.97 by statute is charged £2,287.56.

Verification

Integer sweep of sole-trader profits £0–£500,000, old (main) vs new, against the closed-form statute:

Year Old: wrong (> 1p) Old: first wrong Old: max shortfall Old: worst drop in liability for £1 more profit New: wrong New: max error
2022, 2023, 2025, 2026 0 – – – 0 £0.0016
2027 383,189 (85.4% above UPL) £78,358 £8,974.49 −£2,621.43 0 £0.0008
2028 65,535 (all of £52,302–£65,535, 79.8% of £65,536–£131,071) £52,302 £1,310.70 0 0 £0.0014
2029 40,007 (9.0%) £144,412 £2,621.43 −£1,821.29 0 £0.0015
2030 224,250 (50.3%) £275,751 £8,911.73 −£4,426.73 0 £0.0016

The new code has no monotonicity violations in any year.

Dual-earner grid: 1,732 profit levels × 152 employment incomes, 263,264 cells per year, 2023 and 2026–2030. No cell with Class 1 changed in any year. Every changed cell, and a random 4,000 unchanged cells per year, is within £0.0015 of the exact-rational reg 100 reference.

Axiom parity

axiom: uk:regulations/uksi/2001/1004/100 encoded-correct (rulespec-uk uk/regulations/uksi/2001/1004/100.test.yaml::class_4_annual_maximum_does_not_bind_without_class_1: profits above the UPL with no Class 1 give Case 2 and £2,456.60 including the additional band)

Axiom's rulespec-uk self-employed NICs modules (uk/statutes/ukpga/1992/4/15.yaml and uk/regulations/uksi/2001/1004/100.yaml, at the statutory £12,570 and £50,270) are an independent differential check on this fix:

Independent review

  • Round 1 (GPT-6 Astra via Subfleet, at 66b7e20): REQUEST_CHANGES. It found:
    • Class 2 still in reg 100 after April 2024;
    • a false monotonicity property across the Class 2 threshold;
    • overstated no-change claims;
    • a Case 1 near-tie with a non-representable Class 2 rate.
      The first three are fixed in 8144373; the fourth is answered in this comment and pinned by a test.
  • Round 2 (Claude Opus 5.5 via Subfleet, read-only, at 8144373): APPROVE, nothing blocking. Its low-severity notes are addressed in 9f36b70: 2024 coverage, the notebook rates chart, gate robustness and the stated exception to the differential.

Commands run

  • ruff format . and ruff check .
  • pytest policyengine_uk/tests/test_ni_class_4_maximum.py policyengine_uk/tests/test_ni_class_4_properties.py (14 passed)
  • policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance -c policyengine_uk (38 passed)
  • policyengine-core test policyengine_uk/tests/policy -c policyengine_uk (1,254 passed)
  • A 300-random-example (non-derandomised) Hypothesis campaign per property: both passed.
  • Not run locally: the microsimulation tests and make documentation. Both pass in CI (Test, Test documentation builds).

🤖 Generated with Claude Code

The regulation 100 annual maximum chose Case 1 by comparing Step Four with
the aggregate of Class 1, Class 2 and main-rate Class 4 contributions in
float32. For a sole trader above the upper profits limit the two sides are
mathematically equal, so rounding picked the case; a wrong Case 1 capped
Class 4 at the main-band amount and dropped the 2% band. Test the equivalent
condition main rate x unused band + 53 Class 2 weekly rates > 2 x (Class 1 +
Class 2), which is exact at or above the limit.

Also apply the maximum only when primary Class 1 or Class 2 contributions are
payable (reg 100(1)), and stop Step Five dividing by a zero main rate, which
made national_insurance NaN under a reform abolishing the main rate.

Adds example tests and Hypothesis property tests (statute when the maximum
cannot bind, differential against an exact-rational regulation 100
implementation, bounds, and monotonicity in profits).

Fixes #1878

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

Copy link
Copy Markdown
Collaborator Author

Axiom differential check for this fix. TheAxiomFoundation/axiom-oracles#565 adds uk-national-insurance-pe, which compares the Axiom encodings of SSCBA 1992 s.15 and SI 2001/1004 reg 100 against policyengine-uk on a grid for 2026-27 to 2030-31. Every Axiom threshold comes from the Axiom modules; no PolicyEngine threshold is fed in.

Against pe-uk 2.102.0, 20 rows reproduce this bug to the penny: ni_class_4_maximum and ni_class_4 equal regulation 100 step four (the main band alone) where case 2 applies.

Year LPL / UPL Profits Axiom (statute) pe-uk 2.102.0
2027 12,821.38 / 51,275.32 100,000 3,281.73 2,307.24
2028 13,077.80 / 52,300.78 65,432 2,616.00 2,353.38
2029 13,339.32 / 53,346.68 87,654.32 3,086.59 2,400.44
2030 13,606.11 / 54,413.63 300,000 7,360.18 2,448.45

These rows are expected failures in Axiom, attributed to #1878. Once this PR is in a release, pinning that release should turn them into matches. I'll post the result here.

Two notes:

From 6 April 2024 SI 2024/377 removes Class 2 contributions from
regulation 100: from the applicability condition, Step Three, Step Four
and the Case 1 aggregate. Add a dated includes_class_2 parameter and use
it in the maximum and the reg 100(1) gate, so explicit or reformed Class 2
no longer triggers or reduces the maximum after that date.

Property tests: exercise Class 2 flat-rate reforms, gate Class 2 by year
in the exact reference, assert Class 4 monotonicity only where Class 2 is
unchanged (crossing the small profits threshold lowers Class 4 before
April 2024), and add total Class 2 + Class 4 monotonicity. Add
deterministic tests for the April 2024 change and for the Case 1
"exceeds" boundary with exactly representable amounts.

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

Copy link
Copy Markdown
Collaborator Author

Response to review round 1 (head 66b7e20 → 8144373)

Finding 1: Case 1 near-tie with a reformed Class 2 rate of 10/53 (£0.20)

Not changed in code. I've added a deterministic test and corrected the claim.

  • The example sits on a genuine data coincidence in the statute: 53 × weekly Class 2 = 2 × (Class 1 + Class 2) with no unused band. Reg 100's result is discontinuous there by design: Case 1 needs Step Four to exceed the aggregate, and equality gives Case 2.
  • The intended real-valued inputs (weekly rate 10/53, Class 1 £5) satisfy exact equality, so the statutory answer is Case 2. That's what the new code returns (£5.30). The £5.10 "reference" answer reflects the binary expansion of 10/53, not the policy. Neither old nor new code decides every such coincidence "exactly" for inputs that aren't representable; no floating-point implementation can.
  • What the change does make exact is the structural equality: profits at or above the UPL with no Class 1 or Class 2 terms. There the two sides are identical by construction, which covers every sole trader from April 2024.
  • New test test_annual_maximum_case_1_requires_step_four_to_exceed_the_aggregate pins the boundary with exactly representable amounts: weekly Class 2 £0.50 (53 × = £26.50) and Class 1 £13.25. Expected: Case 2, where Case 1 would cap Class 4 at £13.35 instead of ~£1,197.
  • Also, after finding 3's fix, Class 2 terms only exist before 6 April 2024, so this coincidence can't arise for any current year.

Finding 2: monotonicity property false across the Class 2 threshold

Fixed.

  • Class 4 monotonicity is now asserted only between points with equal ni_class_2.
  • New property: ni_class_2 + ni_class_4 is non-decreasing in profits when main rate ≥ additional rate. In Case 2/3 a Class 2 increase ΔC2 lowers the maximum by at most ΔC2: Step Four falls by ΔC2, and when main ≥ additional the extra Step Eight amount can only offset part of that.
  • The strategy now also draws Class 2 flat-rate reforms, so the SPT crossing is exercised.

Finding 3: Class 2 terms and the reg 100(1) gate are pre-April-2024 law

Fixed. New dated parameter gov.hmrc.national_insurance.class_4.annual_maximum.includes_class_2: true from 2003-04-06, false from 2024-04-06, citing SI 2024/377 reg 6(5). The legislation.gov.uk annotations on reg 100 show it omitting Class 2 from reg 100(1)(a)/(b) and (3) with effect from 6.4.2024.

  • It multiplies the Step Three addition and the Class 2 contributions used in Step Four and Case 1.
  • The gate in ni_class_4 counts Class 2 only when the parameter is true.
  • Your executed example (2026, £1,000 profits, explicit Class 2 £182, LPL 0 / UPL 100) now returns £24. It still returns £20 in 2023, where pre-2024 law applies. Both are pinned in test_class_2_counts_towards_annual_maximum_only_before_april_2024.
  • Baseline results are unchanged: the flag is true exactly while the baseline Class 2 rate is positive.

Claim 5 counterexamples

The claims were overstated; the PR body is corrected.

  • (a) Class 1 of £0.0001 (employment £12,821.348): that person is on the same float32 knife edge (C1 below float32 noise), and the old code dropped the band for them too. It's a correction, not a preservation failure. The body now says the sweeps found no change for anyone paying at least a penny of Class 1.
  • (b) £2,110,758.25 in 2030, a £0.0078 move: two float32 ulps of a £43,575 liability. The body now states the bound in ulps, with the £0–£500k sweep maximum (£0.0042) and your £2.1m example.
  • Tolerances: agreed, they can't substantiate the no-change claim. That claim rests on exact old-vs-new comparisons in the sweeps, and the body now says so.

Maximum no longer reads ni_class_4_main

Intentional.

  • ni_class_4 has never used ni_class_4_main for its own pre-maximum amount; it recomputes the main band from profits. The case test now uses that same computation, so both sides of every comparison share one definition.
  • For non-liable people (e.g. over State Pension age) ni_class_4 is 0 via defined_for, so the changed ni_class_4_maximum value there has no effect on liability.

Profits definition

Agreed, it's wrong. It's filed as #1885 and fixed in a stacked follow-up PR, kept out of this one so the float32 fix can be shown to change nothing else.

Test 2024-25, the first year Class 2 leaves regulation 100; keep the new
boolean parameter out of the NI notebook's rates chart; use
np.logical_and in the reg 100(1) gate so a numeric flag cannot raise; and
state the one exception to the exact-rational differential (a Case 1 tie
that holds in real numbers but not in binary).

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.

Class 4 NI drops the additional-rate band above the UPL when thresholds are non-round (float32 case selection in the reg 100 maximum)

1 participant