Repository navigation
Count Carer's Allowance earnings-limit pence in float32 in the property test - #2534
Conversation
…test test_bounds_and_differential recomputed the reg. 5 penny rounding on earnings converted to float64, while is_gainfully_employed_for_carers_allowance floors the weekly pence on the stored float32 value. At pay of 10,608.52 in 2026 (stored as 10,608.51953125) the formula counts 20,401 weekly pence and the float64 recomputation 20,400, so a draw there failed at random. Keep each variable's stored dtype in _person() and count the pence on it through _over_limit(); tolerance checks convert with float(). Add explicit examples at 10,608.00 (exactly at the limit) and 10,608.52 (a penny over), and derandomize the three property tests. Fixes #2532 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Household net income by decileNo 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; components the model counts twice; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles; standard reform costings (change in gov_balance); standard reforms: change in each reform's target. Checks failing on both base and head (1)
Counted-once checkEach of 84 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.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Components the model counts twice: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
From the GPT-6.1 Sol review of #2534: - Add test_a_penny_a_week_over_the_2026_limit_ends_entitlement, which asserts the outcome for 10,608.00 (not gainfully employed, full rate) and 10,608.52 (gainfully employed, nil) directly, not only agreement with _over_limit. - Add an @example with a trading loss and earnings below the limit (pay 8,000, profit -5,000, pension share 0.1); the derandomized draws have neither. - Correct the changelog: the value the float64 recompute got wrong is stored just below the exact boundary, and float32 arithmetic rounds it across. The float32 formula's own boundary behaviour is #2541. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The re-review of #2534 found that the derandomized draws do include below-limit earners, at £0 to £0.12 a year. What they lack is a real trading loss and earnings between a few pence and the limit. Comment only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hand-off to the UK hub
Land through the merge queue once |
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>
|
Merged by the UK hub (session local_8e19f9d5, Opus 5.5) on its merge gates, from the owner session's hand-off (local_68ce44ef). One merge-queue enqueue, pinned to head
🤖 Generated with Claude Code |
Fixes #2532
What and why
test_bounds_and_differentialchecks thatis_gainfully_employed_for_carers_allowanceis true exactly when weekly earnings, with a fraction of a penny disregarded (Computation of Earnings Regulations 1996 reg. 5), exceed the £204 limit. The test counted the pence on earnings converted to float64. The formula counts them on the stored float32 value. At pay of £10,608.52 in 2026, stored as 10,608.51953125, the formula counts 20,401 weekly pence (gainfully employed) and the test counted 20,400 (not gainfully employed). So a draw that lands there failed. Such a draw is rare, but the test wasn't derandomized, so CI could go red at random with no code change. The test now mirrors the formula's float32 arithmetic. The old float64 recompute applied reg. 5 exactly to the stored value, which is £204.00 a week (20,400.9991 pence floored). The formula's float32 rounding gives £204.01, which matches the £10,608.52 entered. Whether the formula should count pence in float32 at all is #2541: for the 2020-21 to 2024-25 limits the same rounding goes the other way, and a carer a penny a week over the limit keeps full Carer's Allowance. This PR leaves the model alone.This is the same pattern #2518 fixed in
test_carers_allowance_pension_contribution_deduction_rate_properties.py, and the same fix:_person()keeps each variable's stored dtype instead of converting tofloat._over_limit()counts the pence on that stored value, as the formula does. The bounds and tolerance checks convert withfloat(), so they compute exactly as before.@examples fortest_bounds_and_differential: pay £10,608.00 (exactly £204.00 a week), pay £10,608.52 (£204.01 a week), each with no profit or pension, and pay £8,000 with a £5,000 trading loss and a 10% pension share (earnings £7,600, below the limit; the derandomized draws have no real loss and no earnings between a few pence and the limit).test_a_penny_a_week_over_the_2026_limit_ends_entitlementasserts the outcome at £10,608.00 (not gainfully employed, full rate) and £10,608.52 (gainfully employed, nil) directly, not only agreement with_over_limit.derandomize=Trueon the file's three property tests, as in 116 of the 145 Hypothesis test files in the repo.Test only: no model source changes, and no change to any output.
Invariant under test
For a carer in England in 2026 with any pay, trading profit and pension share drawn:
is_gainfully_employed_for_carers_allowanceequalsfloor(stored_earnings / 52 * 100 + 1e-6) / 100 > 204, computed in the stored dtype. The allowance is nil when that holds and the full rate otherwise. The other invariants in the file (bounds, monotonicity, unearned income) are unchanged.Test plan
uvx ruff formatanduvx ruff checkon the changed file: clean.uvx ruff format --check .passes over the repo (1,847 files).changelog.d/2532.fixed.md.pytest policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py -n 4: 13 passed at 3b466a0 (11 at 370aade). This used the locked environment (policyengine-core 3.33.2, numpy 2.5.4, hypothesis 6.169.0, uv.lock identical to main's), importing this branch's source._person()(float(np.asarray(...)[0])) fails at the pay=10608.52 example, at both 370aade and 3b466a0:_over_limitdisagrees with the model on 0 of the 717.Review
subfleet run --task review --tier hard): REQUEST_CHANGES. It reproduced every number then in this body. It also ran the formula against_over_limiton all 1,024,001 float32 values in [10,000, 11,000] and 1,000,000 random values in [0, 60,000], and ran 22,000 non-derandomized draws, with 0 disagreements and 0 failures. It found one false sentence in this body, which I corrected by editing the body, and three nits, which 3b466a0 fixes: the direct outcome test, the trading-loss example and the changelog wording. Its follow-up on the formula became Carer's Allowance: a penny a week over the 2020-21 to 2024-25 earnings limits counts as within the limit #2541.Reproducer: policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py::test_bounds_and_differential
axiom: n/a: test-only change, no model source changes
🤖 Generated with Claude Code