Skip to content

Round the High Income Child Benefit Charge as s. 681C(3) requires - #2323

Merged
MaxGhenis merged 6 commits into
mainfrom
hicbc-rounding
Oct 12, 2026
Merged

MaxGhenis merged 6 commits into
mainfrom
hicbc-rounding

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2321

axiom: ukpga/2003/1/681C TheAxiomFoundation/rulespec-uk#451 queued (rulespec-uk already rounds the percentage and the charge; #451 adds the rounding of the Child Benefit total; the signed encoder run needs its own approval)

What changes

ITEPA 2003 s. 681C(3) rounds the Child Benefit total and the charge down to whole pounds, and the appropriate percentage down to a whole number. CB_HITC rounded none of them.

  • policyengine_uk/utils/child_benefit.py gains two helpers:

    • child_benefit_charge_percentage: the s. 681C(2) percentage as whole points, capped at 100. It works in float64 and multiplies before dividing, so an income on an exact £200 step gives exactly that many points even when incomes are stored as float32. At the first head, 50 of the 100 exact steps lost a point in float32.
    • round_down_to_pounds: takes the amount to the nearest penny in float64 first, so four children's £4,199, stored as 4,198.9995, stays £4,199.

    Neither adds slack.

    • The percentage needs none: with whole-pound band edges, an exact step is a quotient of integers with an integer value, which IEEE division returns exactly. Slack would round a penny income just below a step up to it. Under a £60,000-£2,000,000 band (as a reform or test can set), £79,399.99 would be charged £14 instead of £0.
    • Pounds are taken by integer division of whole pence. Every legitimate amount here is whole pence: weekly rates in pence × 52, and whole points × whole pounds.
  • CB_HITC = round_down(percentage × round_down(Child Benefit)).

  • The household-based charge reform (gov.contrib.conservatives.cb_hitc_household) overrides CB_HITC and applies the same rounding to the household's combined income. A single parent therefore pays the same under the reform as under the baseline: £1,391 at £79,800.

  • Unchanged: the Child Benefit opt-out (child_benefit) still reads the continuous, unrounded charge share against its behavioural threshold, opt_out_charge_share. That rule is a modelling assumption, not s. 681C. The README, the child_benefit_opts_out description and opt-out test Case 10 now call it the continuous charge share, to separate it from the rounded statutory charge.

Worked example: adjusted net income £71,336, Child Benefit £1,406.60. The percentage is 56.68%, rounded down to 56%, and 56% × £1,406 = £787.36, so the charge is £787. main gives £797.26.

Impact (real run)

Measured on enhanced_frs_2023_24.h5 (local policyengine-uk-data storage, 6 October build) for 2026, at head a0516c0. The result is identical at 14f3f88, which had the slack. In one simulation of this branch, main's formula (the unrounded share × Child Benefit, for the higher earner) is computed from the same simulation's variables and compared with CB_HITC (impact_hicbc_direct.py).

main formula This PR Change
High Income Child Benefit Charge £2,062.7m £2,055.2m −£7.6m
  • Who changes: 2,584 records (1.34m people weighted). The largest reduction is £51.62 a person. No charge rises: the largest positive difference is £0.0005, which is float32 storage noise.
  • Income Tax adds the charge, so it falls by up to the charge reduction. It can fall by less: the capped married couple's allowance reduction also reads CB_HITC (capped_mcad), and tax can't go below nil.

Tests

  • New YAML cases in gov/hmrc/income_tax/charges/child_benefit_hitc.yaml:

    • the worked example;
    • an exact 50%;
    • one pound short of the next £200 step;
    • full Child Benefit at £90,000, in whole pounds;
    • 2023-24's 1% per £100 above £50,000;
    • £60,200, an exact step: 1% of £1,406 is £14;
    • four children at £90,000: £4,199, not £4,198.
  • Changed tests, which encoded unrounded charges:

    • child_benefit_hitc.yaml Case 5: £2,337.40 → £2,337, with Income Tax down by the same 40p.
    • uc_earnings_deductions.yaml: £703.30 → £703.
    • child_benefit/opt_out.yaml Case 9: 49.995% → 49%, so £490.
    • child_benefit/opt_out.yaml Case 8: under its softer taper, the old income gave 0.4%, which rounds to nil. The case exists to show a positive charge, so its income moves to £200,000 (1.41%, so 1%: £23).
    • test_child_benefit_opt_out.py: the cliff test expects the whole Child Benefit rounded down to pounds. The softer-taper case moves its taper end from £10,000,000 to £2,000,000, so the charge stays positive after rounding (2.06%, so 2%: £46).
  • Property and regression tests in test_child_benefit_charge_rounding_properties.py (Hypothesis). They hold for whole-pound incomes and whole-pence Child Benefit totals, including their float32 storage:

    • the charge is whole pounds;
    • 0 ≤ charge ≤ Child Benefit rounded down;
    • a differential test against the statute in integer arithmetic (500 examples): points = min(100, max(0, floor((ANI − L) / X)));
    • with Child Benefit fixed, the charge never falls as income rises;
    • every exact step from 0 to 100 points, held as float32, gives exactly that many points;
    • under band ends of £80,000, £2,000,000 and £10,000,000, each tested step and the largest float32 income below it give that many points and one fewer. That includes £79,399.99 under the £2,000,000 end.
    • float32 annual totals are taken to the penny before rounding;
    • a full simulation's CB_HITC matches the statute, for one to four children;
    • the household-based reform equals the baseline for a single parent (£1,391 at £79,800) and charges a couple on £50,000 and £30,000 the whole £1,406 on their combined £80,000. Without the reform fix, this test gives £1,392.53 and £1,406.60.
  • Run locally, at a0516c0:

    • the full policy YAML suite, 3,003 passed;
    • the touched YAML files, 44 passed;
    • test_child_benefit_charge_rounding_properties.py and test_child_benefit_opt_out.py, 34 passed.
  • Reviews: two GPT-6.1 Sol reviews asked for changes:

    • first: float32 steps, £4,199, the household-based reform and wording;
    • second: the slack under wide tapers.

    Both are addressed here.

🤖 Generated with Claude Code

MaxGhenis and others added 2 commits October 11, 2026 12:47
…equires

Fixes #2321. The charge used the unrounded share of the phase-out band. The
percentage is rounded down to a whole number, and the Child Benefit total and
the charge down to whole pounds. Three tests that encoded unrounded charges now
expect the statutory ones; the softer-taper opt-out case moves its income so
the rounded charge stays positive.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits October 11, 2026 13:15
The cliff case now expects the Child Benefit rounded down to pounds, and the
dataset softer-taper case moves its taper end so the rounded charge stays
positive (2%, £46).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes for #2323:
- Work out the appropriate percentage in float64, multiplying before
  dividing, so every exact £200 step gives exactly that many points when
  incomes are held as float32 (50 of 100 steps lost a point before).
- Take an amount to the nearest penny before rounding down to pounds, so
  four children's £4,199 stored as 4,198.9995 stays £4,199.
- Apply the s. 681C(3) rounding in the household-based charge reform too.
- Describe the opt-out test as the continuous charge share, as distinct
  from the rounded statutory charge.
- Regression tests: float32 steps, four children, single-parent reform
  equivalence and the couple's combined charge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Second-round review fix for #2323. With whole-pound band edges, an income on
an exact step gives an integer-valued quotient when multiplied before dividing
in float64, which IEEE division returns exactly, so the 1e-6 slack was not
needed. Under a wide taper it rounded a penny income just below a step up to
it: £79,399.99 under a £60,000-£2,000,000 band (as a reform or test can set)
charged £14 instead of £0. round_down_to_pounds now floors whole pence by
integer division.

Regressions: every tested step and the float32 income just below it under the
statutory, £2,000,000 and £10,000,000 band ends, and the £79,399.99 case. The
property docstring states the whole-pound and whole-pence domain.

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

Copy link
Copy Markdown
Collaborator Author

CI run cancelled to free runners (UK hub, 2026-10-11 23:30Z). The PolicyEngine org is at GitHub's limit of 60 concurrent jobs, with about 360 queued. Fleet ops asked the hub to let the Budget-critical #2305 take runners first. This PR's queued run had not started, so I cancelled it. Nothing is wrong with the PR.

Please don't re-run it or push just to retrigger CI. The hub re-runs this run itself once #2305 has runners. The record is ~/reviews/uk-hub/ci-yield-2026-10-11.json; see notice 17 in ~/reviews/uk-hub/notices.md.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Hand-off to the UK hub (session local_66e4b1fd, PolicyBench UK round 2).

  • Head: eb99a944ab7a2f5fc0fe8e53425f75c35ebdef1e (includes main at ed7b42f95).
  • Review: GPT-6.1 Sol, three rounds, APPROVE at this head: ~/reviews/policybench-uk-round2/reviews/pe-uk-2323-review-r3b.md. Earlier findings (float32 steps, the household-based reform, slack under wide tapers) are fixed.
  • CI: not yet run at this head. The run was queued, and the hub cancelled it to free runners (notice 17, ~/reviews/uk-hub/ci-yield-2026-10-11.json); the hub re-runs it. The last completed run, at 14f3f88d, failed one test only, main's test_dataframe_dataset round-trip (notice 14), with 5,047 others passing. This head includes main's fix for it (Export the identifiers for every year an input frame names, so it round-trips #2349).
  • Local tests (~/reviews/policybench-uk-round2/fixes/evidence/), at the approved head before the main merge, which changed none of this PR's files (pr2323_a0516c0d_net.diff and pr2323_eb99a944_net.diff are identical): 3,003 YAML cases and 34 Python tests (pr2323_r4_full_yaml.log, pr2323_r4_py.log).
  • PolicyBench check at this head: all 2,800 round 2 references recomputed on this head (audit/attribution/refs_hicbc_final.csv); 62 move, the same cells and values as at the reviewed head. With all five fix heads merged onto main ed7b42f95, 96 move, also unchanged.
  • Impact (enhanced FRS, 2026, in the PR body): HICBC −£7.56m.
  • Depends on: nothing. No order constraint with Apply the Carer's Allowance earnings limit #2312, Relieve a miscellaneous loss only against miscellaneous income #2327, Set the Personal Savings Allowance on rest-of-UK bands for Scottish taxpayers #2348 or Band savings and dividends above property income #2352.
  • Decision: none. A bug fix to ITEPA 2003 s. 681C(3).
  • Follow-ups: none owed. The 52-week annualisation (2026-27 has 53 benefit weeks, s. 681H(3)) is a model-wide convention, noted in the round 2 record and not filed. Axiom parity: rulespec-uk#451.

@MaxGhenis
MaxGhenis merged commit 0820203 into main Oct 12, 2026
7 of 12 checks passed
@MaxGhenis
MaxGhenis deleted the hicbc-rounding branch October 12, 2026 03:07
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.

The High Income Child Benefit Charge ignores the rounding in ITEPA 2003 s. 681C(3)

1 participant