Skip to content

Settle property allowance elections without the charge where it cannot change them - #2695

Merged
juaristi22 merged 4 commits into
mainfrom
property-allowance-charge-bound
Oct 27, 2026
Merged

juaristi22 merged 4 commits into
mainfrom
property-allowance-charge-bound

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2694

The property allowance election (uses_property_allowance) compares each landlord's income tax, including the winter fuel payment charge, with and without the allowance, with everyone else's route held fixed. Since #2066 the charge depends on everyone's benefits, so each comparison branch works out every benefit for the whole population. This keeps the comparison and its result, and works the charge out only where it can change a choice.

  • winter_fuel_payment_charge_ceiling (new) is the most the charge could be, whatever benefits anyone receives. It is the largest amount of each payment whose age and residence conditions the person meets, when the charge has effect and their total income exceeds £35,000, and nothing otherwise. It needs no benefits. The charge as the model works it out is never more, because entitlement beyond those conditions, which amount applies, and the exemption all turn on benefit receipt.
  • On each route the charge is between nothing and its ceiling. A cheap pair of branches gives tax before the charge and the ceiling on each route. Where the allowance's saving before the charge, less the ceiling using it and plus the ceiling not using it, keeps its sign beyond a penny and float32 rounding, the choice is the one income tax including the charge gives.
  • Each household's landlords decide in the same order, rounds and stopping rule as before, but each household at its own pace. A decision the ceilings leave open waits, with its household, until no household can decide without the charge. All the waiting decisions are then compared on income tax including the charge, in one pair of branches. This relies only on what the current code already relies on when one round settles a household with one landlord: one household's routes do not move another's tax.
  • The ceiling is used only while the model works out income tax, the ceiling, the charge, the payments it recovers and entitlement to them itself (ceiling_bounds_the_charge). Income tax must still add exactly tax before the charge and the charge, and neither income tax nor the ceiling may be neutralised. None of these may hold an input for the period, and their formulas must be the model's own. Otherwise every election is compared on income tax including the charge, as on main: for instance with a payment given to someone under pensionable age in a household calculation, a reform that tapers the charge, or a reform that neutralises income tax. Neutralising the charge, a payment or entitlement keeps the shortcut, because the charge is then nil, within the ceiling.

Compared with main at 159a5ec (2.162.10) on microcosm_uk_2024_25.h5, building the microsimulation and calculating 2025, 2026 and 2027 (policyengine-core 3.33.12, one Mac, nothing else running, at b15d707):

  • uses_property_allowance, income_tax, winter_fuel_payment_charge, household_net_income, pension_credit and universal_credit are identical in every year.
  • The run takes 122.5 s against 267.6 s on main: 38.9 s against 52.5 s to build, and about 28 s against about 71 s for each year.

The check (added at fe62bb2 and extended at ead3444) runs once per period and passes on Microcosm, which holds none of the variables it looks at and neutralises nothing. The full run was not timed again because the machine is in use. For 2028 to 2032, the same six outputs are identical to main at fe62bb2 on a landlord-heavy household sample of Microcosm, one year per run: every household with finance costs, plus 5% of the others. A full-Microcosm comparison for those years has not been run.

Routes, income tax, the charge and household net income are also identical to main on two synthetic populations built to stress the comparison, for 2024 to 2027. Those populations have pensioner landlords either side of £35,000, with and without Pension Credit and Universal Credit claims, in all four countries. The same four outputs are identical for 2025 to 2027 on a 20% household sample of Microcosm.

Edge cases: 26 synthetic scenarios, comparing routes, income tax, the charge, the Winter Fuel Payment, household net income, Pension Credit and Universal Credit with main, all identical at ead3444:

  • payment and entitlement inputs, including a payment above the largest amount and Scotland's entitlement;
  • inputs of income tax and of the charge;
  • a tapered-charge reform;
  • reforms neutralising income tax, the charge, the Winter Fuel Payment or entitlement to it;
  • parameter reforms of the threshold, the higher amount and the State Pension age requirement;
  • pensioners either side of £35,000 in England, Scotland and Northern Ireland, in 2024, 2025 and 2026;
  • the property test's shared-payment household;
  • two pensioner couples who both straddle £35,000;
  • a two-landlord married couple;
  • a mixed-age couple on Universal Credit;
  • several of these in one simulation.

In 12 of them, settling on tax before the charge alone would choose a different route from main.

New tests:

  • policyengine_uk/tests/test_winter_fuel_payment_charge_ceiling_properties.py checks that the charge is never negative, never above the ceiling, and nothing where the ceiling is nothing. It runs on the model's own routes, and with everyone or nobody using the allowance, in 2025 and 2026.
  • policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge_ceiling.yaml pins the ceiling's values: the higher amounts in England and Scotland, the threshold, Pension Credit, working age, and 2024.
  • test_property_income_properties.py::test_a_charge_beyond_the_ceiling_is_still_compared covers a payment or entitlement given under pensionable age, a payment above the largest amount, a tapered-charge reform, and a ceiling given as nothing or neutralised. In each, the chosen route has the lower income tax including the charge, while tax before the charge alone would choose the other route.
  • test_property_income_properties.py::test_income_tax_the_same_on_both_routes_leaves_the_default covers income tax given as an input or neutralised by a reform. Income tax is then the same on both routes, so the default stands.

Each new election case fails at the head before the review round that added it, and all pass at ead3444. The six that do not involve the ceiling also pass on main, where the ceiling does not exist. The existing pensioner property tests, including the shared-payment example, still check that each chosen route is a best response including the charge.

At ead3444 the following all pass locally:

  • 146 targeted Python tests: property income, the charge and its ceiling, winter heating payments, measurement branches, reform representations, variable units and code sharing.
  • 312 YAML tests: charges, allowances, reliefs and winter payments.
  • 2,984 code-health tests.
  • ruff format --check . and ruff check .

make test was not run.

Not in this PR:

  • Pension Credit's tax-on-earnings counterfactual could keep each person's election as made. That would take the same run to 75.0 s with identical results on Microcosm, but it amends the Apply the Pension Credit earnings disregards (SPC Regs 2002 Sch VI) #2019 top-slice method, so it is raised separately for Max.
  • Building a microsimulation still runs one comparison including the charge for each year from 2026 to 2032, through uc_LCWRA_element, at about 3 s each.

axiom: n/a: changes how the property allowance election is worked out, not what it is; no provision's encoding changes.

🤖 Generated with Claude Code

…t change them

The winter fuel payment charge depends on everyone's benefits, so comparing a
landlord's income tax on both routes worked out every benefit for the whole
population, at each rank and round. Add winter_fuel_payment_charge_ceiling,
the most the charge could be whatever benefits are received, and settle each
election on income tax before the charge wherever the ceilings on the two
routes cannot reverse it. Each household's landlords decide in their own
order, and the elections that turn on the charge are compared on income tax
including it together, once no household can decide without it.

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

Copy link
Copy Markdown
Collaborator Author

@MaxGhenis heads-up: this changes how uses_property_allowance from #2066 works out its comparison, without changing the result. You may want to review it as well as Vahid.

One further option is left out because it is your #2019 decision. income_tax_with_inputs_zeroed drops computed arrays, so at the top level Pension Credit's tax on earnings is worked out with everyone's property allowance election chosen again for a year without earnings. Inside the comparison's branches, the election stays as made. Keeping it as made at the top level too (earnings as the top slice of income as it is) would make the two agree. It would also cut the same Microcosm run, build plus 2025 to 2027, from 122.5 s to 75.0 s, with all six outputs identical. Would you want that, or is choosing the election again the reading you intended for option (a)?

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 1 at b15d7077a.

Verdict: the bound is sound and the reordered loop gives the same elections as main whenever the payments are worked out by the model. One should-fix: the ceiling is not an upper bound when a winter fuel payment is set as an input. Draft, so no approval.

What "the charge" is. Here it means the winter fuel payment charge (ITEPA 2003 s.681I, Finance Act 2026 Sch. 10), not the High Income Child Benefit Charge. Under s.681I(1) and (3) the charge equals the whole payment when total income exceeds £35,000. Under s.681I(4) a relevant benefit in the qualifying week removes it. Total income is after the property allowance, so the election can move it across £35,000. Through Pension Credit it can also change the exemption. So comparing routes on tax including the charge is right, and that comparison is unchanged here.

Law checked. On legislation.gov.uk: ITTOIA 2005 s.783BE (full relief when relevant property income is at most the allowance), s.783BJ (election out of full relief), s.783BK (election for partial relief) and s.783BL (no relief in a year the s.274A finance-cost reduction is given). The variable already cites these. s.783AF to 783AM are the trading allowance equivalents and are not in play.

Bound and loop. On each route the charge lies in [0, ceiling], so the full saving lies in [saving − ceiling_using, saving + ceiling_not_using]. Where both ends share a sign by more than the rounding margin (uses_property_allowance.py:171-177), the before-charge saving has that sign too. So prefers_allowance matches what the full comparison gives, including the tie rule for full relief, because ties can only arise where both ceilings are nil. Only the cross-household independence the old per-rank loop already relied on is used. Per-household rounds and the MAX_ROUNDS cap give the same fixed point as the old global loop. Every pass either moves at least one household or settles every waiting decision, so the while loop (line 150) ends. There is no circularity. The ceiling reads total_income and parameters only, and taxable_social_security_income is already resolved before the branches.

Findings

  1. should: winter_fuel_payment_charge_ceiling.py:45-62. The ceiling rebuilds eligibility from age, country and the parameters. It ignores a winter_fuel_payment or pension_age_winter_heating_payment value set as an input. Checked locally: a 60-year-old in England, £50,000 employment income, winter_fuel_payment: 200 as input gives charge £200 and ceiling £0. A landlord in that position would have the election settled without the charge, so the choice could differ from main. The same happens for an input above the largest parameter amount. Microcosm results are identical, so this looks like a household-API and reform edge case rather than a data one. Even so, the variable's documentation says "the charge is never more than this". Suggest taking the maximum of the formula ceiling and the payments wherever either payment holds an input for the period, or falling back to the full comparison then. Also add a case to the YAML or the property test.
  2. nit: winter_fuel_payment_charge_ceiling.py:34-38. The references give SI 2025/969 reg 2 (entitlement) but not reg 3 (amounts), which the YAML header cites. The Northern Ireland regulations (NISR 2025/142) are also missing, although s.681I(6)(a) covers them and wfp.eligibility.countries includes Northern Ireland.
  3. nit: winter_fuel_payment_charge_ceiling.py:47. is_SP_age | has_attained_state_pension_credit_qualifying_age is wider than the post-2024 test in winter_fuel_payment_eligible (State Pension age only). That is safe for a ceiling. A one-line comment saying it is deliberately wider, to cover the pre-2024 test and reforms, would save a later reader from "fixing" it.
  4. nit: PR body. The comparison with main is reported for 2025 to 2027. A sentence on why 2028 onwards (where uc_LCWRA_element still triggers a full comparison at build time) is covered would help, or just say it is not.

What I ran

  • gh auth status showed vahid-ahmadi.
  • I read the PR body, issue uses_property_allowance works out every benefit on both routes since #2066, so microsimulations run several times slower #2694, the full diff, and on the branch winter_fuel_payment_charge.py, winter_fuel_payment_eligible.py, pawhp_eligible.py, utils/winter_heating.py::winter_heating_payment_amount and income_tax_before_winter_fuel_payment_charge.py.
  • pytest policyengine_uk/tests/test_winter_fuel_payment_charge_ceiling_properties.py: 1 passed (32 s).
  • policyengine-core test .../charges/winter_fuel_payment_charge_ceiling.yaml: 6 passed.
  • A one-household script for finding 1.

Not run: the full Python and YAML suites, test_property_income_properties.py, and any Microcosm or microsimulation run. A long local build is using the machine, so I relied on the PR's own timings and equality checks. At b15d7077a CI shows lint, changelog, docs, smoke imports and fix evidence passing. Test / YAML, Test / Python 1-5 and Aggregate impact were still running when this was posted.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base b9f55f84b5 against head b7beef1191, 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 b9f55f84b5 uk 2.162.12 (files 31952b344b07), core 3.33.12, py 3.13.18 aarch64 cad278205035 4f86ac4248d9 1991.39s
head #2695 merged into main b7beef1191 uk 2.162.12 (files d25750c1b916), core 3.33.12, py 3.13.18 aarch64 cad278205035 4f86ac4248d9 1989.7s

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.12bn £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.04bn £0.04bn £0.04bn £0.04bn £0.04bn
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.96bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.37bn †
Government balance, each receipt once £369.86bn † £398.03bn † £428.43bn † £455.78bn † £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.96bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.37bn †
household_benefits £528.96bn † £543.83bn † £551.68bn † £558.06bn † £566.57bn † £575.31bn † £583.37bn †
gov_balance £369.86bn † £398.03bn † £428.43bn † £455.78bn † £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.43bn † £1,688.69bn † £1,719.55bn † £1,750.61bn † £1,781.67bn † £1,821.07bn † £1,867.39bn †
Household net income (HBAI definition) £1,457.30bn £1,504.48bn £1,537.58bn £1,569.36bn £1,602.66bn £1,640.48bn £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.4% 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.7% 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,049k 10,466k 10,736k 10,860k 10,960k 11,054k 11,081k
Absolute poverty, before housing costs: children 3,580k 3,171k 3,211k 3,220k 3,261k 3,305k 3,317k
Absolute poverty, before housing costs: working-age adults 5,845k 5,746k 6,022k 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,391k 11,468k 11,652k
Relative poverty, before housing costs: children 3,918k 3,523k 3,519k 3,491k 3,435k 3,458k 3,516k
Relative poverty, before housing costs: working-age adults 6,202k 6,136k 6,338k 6,400k 6,368k 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,482 †
Decile 2: mean household_net_income per household £27,963 † £28,917 † £28,654 † £28,906 † £29,096 † £29,646 † £30,235 †
Decile 3: mean household_net_income per household £34,381 † £35,463 † £36,065 † £36,509 † £36,851 † £37,338 † £38,043 †
Decile 4: mean household_net_income per household £38,785 † £40,713 † £41,873 † £42,413 † £43,144 † £43,906 † £44,167 †
Decile 5: mean household_net_income per household £43,788 † £44,626 † £45,606 † £46,288 † £47,327 † £48,285 † £49,710 †
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,219 † £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

…nd it

Review r1 of #2695: the ceiling rebuilds entitlement from age, residence and
the amount parameters, so a payment or entitlement given as an input, an
input of income tax, or a structural reform of the charge or the payments can
take the charge above it, or income tax away from tax before the charge plus
the charge. uses_property_allowance now checks once per period that the model
works out income tax, the charge, the payments and entitlement itself, and
otherwise compares every election on income tax including the charge, as
before. Add tests for a payment and entitlement given under pensionable age,
a payment above the largest amount, a tapered-charge reform and an income tax
input; cite SI 2025/969 reg 3, NISR 2025/142 and SSI 2025/282; say why the
ceiling's age test is wider than the payment's.

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

Copy link
Copy Markdown
Collaborator Author

Thanks @vahid-ahmadi. All four points are addressed in fe62bb2.

  1. Inputs and reforms. Agreed, and the gap is a little wider than payment inputs. An input of winter_fuel_payment_eligible or pawhp_eligible breaks the ceiling in the same way. So does an input of income_tax itself, which makes every choice a tie on main. A structural reform of the charge (a taper, say) breaks it too. Rather than widen the ceiling case by case, uses_property_allowance now checks once per period (ceiling_bounds_the_charge) that the model works out income tax, the charge, both payments and entitlement to them itself:

    • income tax still adds exactly tax before the charge and the charge, with no formula of its own;
    • none of them holds an input for the period;
    • the charge, payment and eligibility formulas are the model's own, each in its own file under variables/.

    If any check fails, every election is compared on income tax including the charge, as on main. While the election runs, a value held for the period can only be an input, because all of these wait on the election. A top-level set_input made later drops calculated values before the next calculation, so the check sees it too. The ceiling's documentation now says it bounds the charge as the model works it out, and that the election does not rely on it otherwise.

    New tests in test_property_income_properties.py:

    • test_a_charge_beyond_the_ceiling_is_still_compared, with four cases: your payment given under pensionable age, entitlement given under pensionable age, a payment above the largest amount, and a tapered-charge reform;
    • test_income_tax_given_as_an_input_leaves_the_default.

    In the first four the charge decides the election. In the last, income tax is the same on both routes, so the default stands. All five fail at b15d7077a and pass on main and at this head.

  2. References. Added SI 2025/969 reg 3, NISR 2025/142 regs 2 and 3, and SSI 2025/282 reg 9, which substitutes SSI 2024/351 reg 10.

  3. Wider age test. Added the comment.

  4. Years covered. The PR body now says which years were compared. The full Microcosm comparison covers 2025 to 2027. For 2028 to 2032, I compared the same six outputs on a landlord-heavy household sample of Microcosm, one year per run: every household with finance costs, plus 5% of the others. They are identical to main in every year. A full-Microcosm run for those years needs the machine to itself, so it has not been done.

Edge cases. I also ran 20 synthetic scenarios through main, b15d7077a and this head, comparing routes, income tax, the charge, the Winter Fuel Payment, household net income, Pension Credit and Universal Credit:

  • the five input and reform cases above;
  • parameter reforms of the threshold, the higher amount and the State Pension age requirement;
  • pensioners either side of £35,000 in England, Scotland and Northern Ireland, in 2024, 2025 and 2026;
  • the SHARED_PAYMENT household from the property test, with its children;
  • two pensioner couples who both straddle £35,000;
  • a two-landlord married couple;
  • a mixed-age couple on Universal Credit;
  • several of these in one simulation, so households decide at their own pace.

This head matches main in all 20. b15d7077a differed in exactly the five input and reform cases. In 11 of the 20 the charge decides the election: settling on tax before the charge alone would choose differently, so these exercise the comparison including the charge.

Local runs at fe62bb2, all passing:

  • 115 targeted Python tests: property income, the charge and its ceiling, winter heating payments, measurement branches, variable units and code sharing;
  • 312 YAML tests: charges, allowances, reliefs and winter payments;
  • 2,984 code-health tests;
  • ruff format --check . and ruff check ..

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 2 at fe62bb240.

Verdict: round 1 is resolved. The new check catches every input I tried, parametric reforms keep the fast path, and the common case still uses it. One small should: a reform that neutralises income_tax gets past the check. Draft and CI still running, so no approval.

Round 1 items

  1. should, ceiling ignores inputs: fixed. uses_property_allowance.py:36-70 adds ceiling_bounds_the_charge, called once per period at line 196. When it fails, lines 226-231 send every choice to the comparison including the charge. The ceiling's documentation now says when it applies (winter_fuel_payment_charge_ceiling.py:26-33). The new tests are at test_property_income_properties.py:644-702.
  2. nit, references: fixed. winter_fuel_payment_charge_ceiling.py:37-45. All four new links resolve on legislation.gov.uk. SSI 2025/282 reg 9 does substitute reg 10 of SSI 2024/351.
  3. nit, wider age test: fixed. winter_fuel_payment_charge_ceiling.py:52-55.
  4. nit, years covered: fixed. The PR body now says which years were compared, and that a full-Microcosm run for 2028 to 2032 has not been done.

Testing the check. I hooked ceiling_bounds_the_charge and with_route and ran one-landlord households in 2026. Each time I compared the chosen route with income tax worked out on both routes.

Case Check Route matches the full comparison
No inputs, pensioner on £33,000 passes yes
winter_fuel_payment input, age 60 fails yes
winter_fuel_payment_eligible input, age 60 fails yes
pawhp_eligible input, age 60, Scotland fails yes
income_tax input fails yes (tie, default)
winter_fuel_payment_charge input fails yes
Threshold cut to £30,000 (parametric) passes yes
Higher amount raised to £2,000 (parametric) passes yes
State Pension age requirement off (parametric), age 60 passes yes
is_SP_age input, age 60 passes yes
Eligibility, or household_net_income, calculated first passes yes
set_input of the payment after the election passes, then fails on recalculation yes
winter_fuel_payment, its eligibility or the charge neutralised passes yes
income_tax neutralised passes no

Parametric reforms keep the fast path because the ceiling reads the same parameters as the charge, as you said. Inputs to shared upstream variables such as is_SP_age feed both the ceiling and the payment, so they cannot break it either. A later top-level set_input does clear the cached election, so your claim there holds.

Cost and the common case. The check took 50 to 65 µs per call, once per period. In the common case it passes, so settled choices are compared on tax before the charge only. Where only the not-using route has a ceiling, one income tax branch is skipped. Microcosm 2024-25 and the enhanced FRS 2024-25 hold none of the six variables it looks at, so it will pass in microsimulation too. I did not time a full run. A 300-household synthetic timing, with the check forced to fail and not, showed no difference; it is too small to show the saving.

New findings

  1. should: uses_property_allowance.py:57-59. neutralize_variable clones the variable and sets is_neutralized, so it keeps its adds and has no formula of its own. A reform that neutralises income_tax passes the check, yet income tax is then nil on both routes. main treats every choice as a tie and keeps the default. Here the choice is settled on tax before the charge. Checked on the base (159a5ec99) and at this head: a 50-year-old in the North West on £40,000 pay, with £3,300 receipts and £100 finance costs, gets uses_property_allowance False on main and True here. Household net income was the same in that case and in two Universal Credit families I tried, so the effect may stop at the route and taxable property income. The repo already builds such a reform in test_reform_representations.py:80. Suggest or income_tax.is_neutralized on line 58. Neutralising the charge, a payment or eligibility is safe, because the charge then falls to nil, below the ceiling.
  2. nit: uses_property_allowance.py:63-69. The source-file test accepts any replacement defined in a file of the same name under a directory called variables. A contrib reform kept under policyengine_uk/variables/ would pass. None exists today, so a comment on this limit would do.
  3. nit: uses_property_allowance.py:60. winter_fuel_payment_charge_ceiling is not among the variables checked for an input, so an input or a reform of the ceiling itself would pass. Unlikely in practice, but adding it to the get_array loop costs nothing.

CI. At fe62bb240, lint, changelog, docs, smoke imports, release build and fix evidence pass. Test / YAML, Test / Python 1-5 and Aggregate impact are still running. The bot's 0-move aggregate impact (12:29) was for fc00f3411e, the merge of b15d7077a into 159a5ec99. It covers round 1's code, not the check. One Fix evidence run was cancelled and its rerun passed.

What I ran

  • gh auth status showed vahid-ahmadi.
  • I read your reply, the diff b15d7077a..fe62bb240, and on the branch winter_fuel_payment_charge.py, winter_fuel_payment.py, winter_fuel_payment_eligible.py, pension_age_winter_heating_payment.py, pawhp_eligible.py, utils/winter_heating.py::winter_heating_payment_amount, and core's neutralize_variable, get_branch and set_input.
  • pytest policyengine_uk/tests/test_property_income_properties.py policyengine_uk/tests/test_winter_fuel_payment_charge_ceiling_properties.py: 14 passed (128 s).
  • policyengine-core test .../charges/winter_fuel_payment_charge_ceiling.yaml: 6 passed.
  • The household probes above. The neutralised income_tax case was also run on the base commit. For the late set_input row, the route after recalculation matches the payment-input row.

Not run: the full suites, any Microcosm or microsimulation run, and your 20 synthetic scenarios. A long local build is using the machine.

🤖 Generated with Claude Code

… is neutralised

Review r2 of #2695: neutralize_variable clones a variable and sets
is_neutralized, so a neutralised income tax keeps the parts it adds and passed
the check while being nil on both routes. ceiling_bounds_the_charge now also
fails when income tax or the ceiling is neutralised, and checks the ceiling
itself for an input or a replacement formula. Neutralising the charge, a
payment or entitlement keeps the shortcut: the charge is then nil, within the
ceiling. Note the limit of the source-file test, and add test cases for a
ceiling given as an input or neutralised and for a neutralised income tax.

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

Copy link
Copy Markdown
Collaborator Author

Thanks @vahid-ahmadi. All three are addressed in ead3444.

  1. Neutralised income_tax. Fixed: ceiling_bounds_the_charge now also fails when income_tax is neutralised. I extended the same rule to the ceiling, so a neutralised winter_fuel_payment_charge_ceiling also sends every choice to the comparison including the charge. Neutralising the charge, a payment or eligibility still passes. get_neutralized_variable clones the variable, so the model's formula is kept, and the charge then falls to nil, within the ceiling. A comment says so.
  2. Source-file limit. Added a comment at the check: a replacement kept in a file named after its variable under policyengine_uk/variables would pass, and structural reforms live elsewhere.
  3. The ceiling itself. The check now looks for an input of the ceiling, and requires its formula to be the model's own, like the payments.

New test cases in test_property_income_properties.py:

  • test_a_charge_beyond_the_ceiling_is_still_compared now also has ceiling_given_as_nothing and ceiling_neutralised_by_a_reform;
  • test_income_tax_the_same_on_both_routes_leaves_the_default (renamed) covers both an income_tax input and your neutralising reform.

The three new cases fail at fe62bb240, and all eight pass at this head.

Edge cases. I added your round 2 probes to the synthetic scenarios: neutralised income_tax (your household), neutralised charge, payment and eligibility, a pawhp_eligible input in Scotland, and a winter_fuel_payment_charge input. Comparing routes, income tax, the charge, the Winter Fuel Payment, household net income, Pension Credit and Universal Credit with main:

  • this head matches main in all 26 scenarios;
  • fe62bb240 differed only in the neutralised income_tax case;
  • in 12 of the 26, settling on tax before the charge alone would choose a different route from main.

Local runs at ead3444, all passing:

  • 146 targeted Python tests, now including test_reform_representations.py;
  • 312 YAML tests;
  • 2,984 code-health tests;
  • ruff format --check . and ruff check ..

The six new cases that do not involve the ceiling also pass on main. The two ceiling cases need the variable, which main does not have.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

How this PR cuts the runtime

On main, uses_property_allowance decides each landlord's election by working out their income_tax with and without the allowance, in two branches of the whole simulation. Since #2066, income_tax includes the winter fuel payment charge. The charge needs Pension Credit, Universal Credit and the other relevant benefits, so each pair of branches works out every benefit for the whole population, then drops it. main builds such a pair for each rank and round of decisions within households. It does so in every period the election is calculated for, including the periods a microsimulation calculates while it is built.

This PR builds far fewer of those full-benefit branches, in two ways, without changing any election.

  1. Most elections never need the charge. winter_fuel_payment_charge_ceiling is the most the charge could be for a person on a route, and it needs no benefits. It is nothing for anyone who meets neither payment's age and residence conditions, in a year before the charge has effect, or with total income of £35,000 or less on that route. Otherwise it is the largest amount of the payments whose conditions they meet. A cheap pair of branches works out tax before the charge and the ceiling on each route, costing about what the whole comparison cost before Charge the winter fuel payment back through income tax, person by person #2066. The charge on each route lies between nothing and its ceiling. So where the before-charge saving keeps its sign within those limits, the election is the one income tax including the charge would give, and the full-benefit branches are not needed.
  2. The elections that can turn on the charge are batched. Each household goes through its landlords in the same order, rounds and stopping rule as before, but at its own pace instead of in one global step per rank and round. A household whose next decision could turn on the charge waits. Once no household can decide without the charge, all the waiting decisions are compared on income tax including it, in one pair of full-benefit branches. When only one route has a ceiling above nothing, for instance because the allowance takes total income to £35,000 or less, the branch for the other route is skipped, since tax before the charge is the tax there.

Where an input or a reform replaces income tax, the ceiling, the charge, the payments or entitlement to them, or a reform neutralises income tax or the ceiling, the ceiling no longer bounds the charge. The election then compares every choice on income tax including the charge, as on main.

Measured on microcosm_uk_2024_25.h5 at b15d707, on one Mac, building the microsimulation and calculating 2025 to 2027:

  • the run fell from 267.6 s on main to 122.5 s;
  • each year fell from about 71 s to about 28 s;
  • the build fell from 52.5 s to 38.9 s;
  • 2.151.0, before the charge existed, took about 24 s to build and calculate three years.

The checks added since run once per period. Microcosm holds none of the variables they look at, so it takes the same path. That full run has not been re-timed.

What is left:

  • Building a microsimulation calculates uc_LCWRA_element for 2026 to 2032, which reaches income tax through the ESA permitted-work test. So the build still runs one comparison including the charge for each of those years, at about 3 s each.
  • Pension Credit's tax-on-earnings counterfactual (income_tax_with_inputs_zeroed) drops calculated values, so it chooses everyone's election again and runs the comparison a second time. Keeping elections as made there, which is the question for Max above, would take the same run to 75.0 s.

@juaristi22
juaristi22 marked this pull request as ready for review October 27, 2026 13:58
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 3 at ead3444d7.

Verdict: round 2 is resolved and I have no new must or should findings. The PR is out of draft, but the test jobs and Aggregate impact are still running, so I am not approving yet. Once they are all green at this head, it is ready to approve.

Round 2 items

  1. should, neutralised income_tax: fixed. uses_property_allowance.py:61-68 now fails the check when income_tax or the ceiling is neutralised. Covered by test_income_tax_the_same_on_both_routes_leaves_the_default[income_tax_neutralised_by_a_reform] (test_property_income_properties.py:718) and ceiling_neutralised_by_a_reform (line 693).
  2. nit, source-file limit: fixed. The comment at uses_property_allowance.py:76-79 states the limit.
  3. nit, the ceiling itself: fixed. Line 69 checks for an input of the ceiling, and line 72 checks that its formula is the model's own, as for the payments. Covered by ceiling_given_as_nothing (line 685).

Round 2 probe, re-run. The same household (50, North West, £40,000 pay, £3,300 receipts, £100 finance costs) with income_tax neutralised: the check now fails, and the route is expenses, the default, as on main.

Your claim that the other neutralisations are safe. It holds. winter_fuel_payment_charge.py multiplies the payment by the threshold and benefit tests. Each payment goes through winter_heating_payment_amount, which returns eligible * .... So neutralising the charge, a payment or its eligibility leaves that part nil, and nil is within a ceiling that is never below nothing. I hooked the check and compared the chosen route with income tax on both routes, for a 70-year-old landlord on £32,000 pension (the test file's landlord):

Reform Check Route matches the full comparison
None passes yes
winter_fuel_payment_charge neutralised passes yes
winter_fuel_payment neutralised passes yes
winter_fuel_payment_eligible neutralised passes yes
pension_age_winter_heating_payment neutralised, Scotland passes yes
pawhp_eligible neutralised, Scotland passes yes
winter_fuel_payment_charge_ceiling neutralised fails yes
income_tax_before_winter_fuel_payment_charge neutralised passes yes
total_income neutralised passes yes
pension_credit neutralised passes yes

In the no-reform and ceiling cases the charge reverses the choice made on tax before the charge, and the route still matches.

New tests. With the test file at this head and uses_property_allowance.py as at fe62bb240, the three new cases fail (ceiling_given_as_nothing, ceiling_neutralised_by_a_reform, income_tax_neutralised_by_a_reform) and the existing income_tax_given_as_an_input case passes. At this head the whole file passes: 16 passed (98 s).

New findings

  1. nit: winter_fuel_payment_charge_ceiling.py:27-30. The documentation lists what makes the election stop relying on the ceiling, but not an input or a neutralisation of the ceiling, or a neutralised income_tax. uses_property_allowance's documentation now has the full list, so a pointer to it would do. Not blocking.

CI at ead3444d7. Lint, changelog, docs, fix evidence, release build and smoke imports (py3.11 to 3.14) pass. Test / Python 1-6, Test / YAML and Aggregate impact are still running. The PR merges cleanly.

What I ran

  • gh auth status showed vahid-ahmadi.
  • I read your reply, the diff fe62bb240..ead3444d7, and at the head winter_fuel_payment_charge.py, winter_fuel_payment_charge_ceiling.py, winter_fuel_payment.py, winter_fuel_payment_eligible.py, pension_age_winter_heating_payment.py, pawhp_eligible.py and winter_heating_payment_amount.
  • pytest policyengine_uk/tests/test_property_income_properties.py at the head, and its new cases against the fe62bb240 election, as above.
  • The household probes above, in 2026.

Not run: the other test files, the YAML suite, any Microcosm or microsimulation run, and your 26 synthetic scenarios.

🤖 Generated with Claude Code

juaristi22 added a commit to PolicyEngine/microcosm that referenced this pull request Oct 27, 2026
PolicyEngine/policyengine-uk#2695's head now merges the 2.162.12 release
(b71ce793), so the workspace installs that commit, which the engine freeze
accepts as policyengine-uk 2.162.12. 2.162.11 imputes non-workers' wages
once per participation step with unchanged outputs, and 2.162.12 changes
only CI. The uprating and bonus-sacrifice pins keep every value, concept
coverage finds no input added or removed, and the coverage manifest, the H2
spine fixture, the version mentions and the US worker identity's lock
digest move with it.

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

@vahid-ahmadi vahid-ahmadi 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.

Approving at b71ce79; see round 3 above. That head only merges main (2.162.12) onto ead3444, and all 19 checks pass (YAML re-run after the known 30-minute timeout). 🤖 Generated with Claude Code

juaristi22 added a commit to PolicyEngine/microcosm that referenced this pull request Oct 27, 2026
2.162.14 is the first release on PyPI that carries
PolicyEngine/policyengine-uk#2695: 2.162.13's release run stopped at its
YAML test job's 30-minute limit, so it never published, and
PolicyEngine/policyengine-uk#2697 raised the limit. Its package code outside
the tests is the head commit the previous pin installed (b71ce793), so the
interim git source goes and the lock takes the registry release.

This pair's local-area cut stays the build at db50ae8, which records
policyengine-uk 2.162.12; its national is built at this commit on 2.162.14.
The uprating and bonus-sacrifice pins keep every value, concept coverage
finds no input added or removed, and the coverage manifest, the H2 spine
fixture, the version mentions and the US worker identity's lock digest move
with the lock.

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.

uses_property_allowance works out every benefit on both routes since #2066, so microsimulations run several times slower

2 participants