Fix Class 4 NI dropping the additional-rate band at non-round thresholds - #1884
Conversation
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>
|
Axiom differential check for this fix. TheAxiomFoundation/axiom-oracles#565 adds Against pe-uk 2.102.0, 20 rows reproduce this bug to the penny:
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>
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.
Finding 2: monotonicity property false across the Class 2 thresholdFixed.
Finding 3: Class 2 terms and the reg 100(1) gate are pre-April-2024 lawFixed. New dated parameter
Claim 5 counterexamplesThe claims were overstated; the PR body is corrected.
Maximum no longer reads
|
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>
Fixes #1878
What was wrong
With CPI-uprated (non-round) Class 4 thresholds,
ni_class_4dropped 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 whenstep_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, andmin(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 tomain_rate × clamp(UPL − class_4_profits, 0, UPL − LPL) + 53 × weekly Class 2 > 2 × (primary Class 1 + Class 2), evaluated in float64.0 > 0(from April 2024 there are no Class 2 terms).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 settinggov.hmrc.national_insurance.class_4.rates.mainto 0 madeni_class_4_maximum,ni_class_4andnational_insuranceNaN for every person. The headroom is now unlimited when the main rate is zero.hypothesisadded 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.
ni_class_4 = main × clamp(p − LPL, 0, UPL − LPL) + additional × max(p − UPL, 0).ni_class_4equals 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.0 ≤ ni_class_4 ≤the pre-maximum amount; the maximum only reduces liability.ni_class_4is 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_4is still non-decreasing when main rate ≥ additional rate.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.pyexample tests: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.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.mainwith only the zero-main-rate guard back-ported, both still fail on float32 case-selection counterexamples. Examples:Verification
Integer sweep of sole-trader profits £0–£500,000, old (main) vs new, against the closed-form statute:
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.yamlanduk/regulations/uksi/2001/1004/100.yaml, at the statutory £12,570 and £50,270) are an independent differential check on this fix:uk-national-insurance-pe) compares them with policyengine-uk for 2026-27 to 2030-31. 20 rows reproduce Class 4 NI drops the additional-rate band above the UPL when thresholds are non-round (float32 case selection in the reg 100 maximum) #1878 at 2.102.0.Independent review
The first three are fixed in 8144373; the fourth is answered in this comment and pinned by a test.
Commands run
ruff format .andruff 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)make documentation. Both pass in CI (Test, Test documentation builds).🤖 Generated with Claude Code