Repository navigation
Settle property allowance elections without the charge where it cannot change them - #2695
Conversation
…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>
|
@MaxGhenis heads-up: this changes how One further option is left out because it is your #2019 decision. |
|
Round 1 at Verdict: the bound is sound and the reordered loop gives the same elections as 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 ( Findings
What I ran
Not run: the full Python and YAML suites, 🤖 Generated with Claude Code |
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
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
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; 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)
Counted-once checkEach 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.
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
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
|
…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>
|
Thanks @vahid-ahmadi. All four points are addressed in fe62bb2.
Edge cases. I also ran 20 synthetic scenarios through
This head matches Local runs at fe62bb2, all passing:
|
|
Round 2 at 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 Round 1 items
Testing the check. I hooked
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 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
CI. At What I ran
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>
|
Thanks @vahid-ahmadi. All three are addressed in ead3444.
New test cases in
The three new cases fail at Edge cases. I added your round 2 probes to the synthetic scenarios: neutralised
Local runs at ead3444, all passing:
The six new cases that do not involve the ceiling also pass on |
|
How this PR cuts the runtime On This PR builds far fewer of those full-benefit branches, in two ways, without changing any election.
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 Measured on
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:
|
|
Round 3 at 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
Round 2 probe, re-run. The same household (50, North West, £40,000 pay, £3,300 receipts, £100 finance costs) with Your claim that the other neutralisations are safe. It holds.
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 New findings
CI at What I ran
Not run: the other test files, the YAML suite, any Microcosm or microsimulation run, and your 26 synthetic scenarios. 🤖 Generated with Claude Code |
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
left a comment
There was a problem hiding this comment.
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
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>
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.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 onmain: 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
mainat 159a5ec (2.162.10) onmicrocosm_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_creditanduniversal_creditare identical in every year.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
mainat 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
mainon 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: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.pychecks 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.yamlpins 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_comparedcovers 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_defaultcovers 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:
ruff format --check .andruff check .make testwas not run.Not in this PR:
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