Repository navigation
Conversation
The transferor's election cuts their own personal allowance by the fixed transferable amount (s.55B(4)-(6)); the gaining party gets a Step 6 tax reduction at the basic or Scottish basic rate, capped at their Step 5 tax (s.55A(2), s.55B(1), (3), s.29(2)). The couple elects only when the conditions in s.55B(2) and s.55C(1)(b)-(c) hold, no married couple's allowance is claimed, it lowers their combined income tax, and the elder spouse's take-up draw is true. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…roperties YAML cases from ITA 2007 ss.55A-55E, hand-computed, including the issue's worked example and each departure it lists. The State Pension and earnings-deduction UC properties now run with Marriage Allowance claimed, holding each couple's election at its first-run choice; the strict xfail on the partner's Marriage Allowance now passes, and a new case pins the point where the couple stops electing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ential The couple's tax never rises with the model's election, which matches the cheapest allowed option found by running each election fixed; the gaining partner's reduction does not depend on the elector's income; transfers balance and reductions stay within s.55B(1)/(3) and s.29(2); couple tax never falls with more income. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dataset draws would_claim_marriage_allowance independently per person at 50%. Reading the elder spouse's draw reshuffled take-up across about 0.65m couples on the Enhanced FRS for no legal reason. The gaining spouse's draw is the one that decided take-up before, so the same couples claim. Also add the changelog fragment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est comments s.55B(2)(ba) asks whether dividends would reach the dividend upper rate, which starts at the basic rate limit (s.13); the dividends scale's own threshold lags it before 2026-27. State the married premise in the UC property tests, and correct the comments on why the couples do not elect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tie case no longer asserts income tax, which depended on the model's fixed allowance order rather than s.25(2). The married couple's allowance case uses spouses old enough to claim it. The ordering case cites s.27(2) and s.29(2)-(3); the limit notes s.27(6) for double taxation relief; the single election per couple is described as a modelling choice, since s.55E does not bar elections both ways. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e rate test s.16 puts dividends above property income as well as earnings, so a landlord whose dividends cross the basic rate limit is liable at the dividend upper rate (s.55B(2)(ba)). Note that a spouse outside the benefit unit is not modelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the election-independent income tax inputs before branching, so a reform that branches inside adjusted_net_income gives the same result whichever variable is asked for first, and free the branch arrays afterwards. Only a spouse can give allowance up, and spouses must be old enough to marry (16, the Scottish minimum). On a tie the lower-income spouse elects. The CPS expanded reform lifts the income condition only when remove_income_condition is set. Refresh stale comments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…riage Allowance The locked policyengine-uk predates PolicyEngine/policyengine-uk#1963. A lock bump changes the dataset build, so it belongs in its own release. The test runs as soon as the installed model defines marriage_allowance_relinquished. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Heads-up from #1949, which this stacks on: its head moved from d0fc22f to 514132f with one added commit and no history rewrite. The commit changes two lines in |
|
Update from #1949: its head is now c98c53c, a merge of #1943's new head ce85220, which has main 7b9fc37 merged in. No history rewrite and no formula change. Rebasing or merging onto c98c53c brings in current main, so this PR's impact base moves too. The one conflict #1949 had was the UC docs means-test sentence. |
…ce-election Take #1949's uc_income_tax_on_earnings, which now sets every Step 6 reduction (income_tax_subtractions, which this branch extends with the Marriage Allowance reduction) against the tax on earnings first. Identify spouses as the married claimant and partner (#1896), dropping the two-eldest rule and the minimum-age parameter. Keep #1896's Marriage Allowance and CPS tests, adapted to the transfer model; carry the pinned elections into #1949's married couple's allowance and property-income properties. Drop the branch-array clearing now that core shares branch arrays until they are written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hold each couple's Marriage Allowance election at the base run's choice in the UC capital-income properties too, in place of switching Marriage Allowance off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - income_tax_subtractions.yaml: keep main's property_finance_cost_relief and this branch's marriage_allowance_tax_reduction. - test_uc_earnings_deductions_properties.py: keep this branch's election holding for invariants 1-3; take main's pre-#1942 formula (main removed uc_mif_capped_earned_income) and its start-up period for invariant 4. - test_uc_income_from_capital_properties.py, test_uc_state_pension_properties.py: keep this branch's election holding in place of main's marriage_allowance=False switches; take main's comments on the claimant-or-partner presumption. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion Main added property_finance_cost_relief as a Step 6 reduction limited to the tax left after every other reduction, so it came after the Marriage Allowance reduction. The election then valued the gaining spouse's reduction against tax the finance-cost reduction would have cleared anyway: a landlord whose finance costs wipe out their tax still "saved" 252, so the couple elected and paid the elector's extra tax for nothing. Each Step 6 reduction is limited to the tax left after those before it (ITA 2007 s. 29(2)-(3)); the order changes how the total is split but not the total (s. 27(2)). Take the finance-cost reduction first and the Marriage Allowance reduction last, so the reduction is exactly the tax the election saves the gaining spouse. YAML cases pin both the no-election and the partial cases; the property tests now generate finance costs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests switched Marriage Allowance off because the old model booked it as the transferor's unused allowance (#1947). With the statutory transfer, an election fixes the gaining partner's reduction, so hold each couple's election at the first run's choice instead, as the other UC property tests now do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Companion cases B and C of TheAxiomFoundation/rulespec-uk#437: on 50,070 of earnings, 200 of dividends reaches the basic rate limit exactly and meets the conditions; 201 would put 1 at the dividend upper rate if s. 13A were omitted, so it does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finance costs make the property allowance a choice that income tax weighs in its own branches, which nest with the election's, so giving them to every adult made the property tests about ten times slower. Draw them for about one population in four, and always try a landlord couple whose finance-cost reduction clears the landlord's tax. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cleanup The election and the property allowance route both weigh income tax in policyengine-core branches, and they nest. Check that results are bit-for-bit the same whichever output is asked for first, with and without a dated personal allowance reform; that a reform's baseline matches an unreformed simulation whichever runs first; and that no branch is left after computing six years. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…16 amount The independent review found a couple whose automatic choices raised their tax: the landlord spouse's property allowance route was chosen on their own tax, which forfeited the other spouse's Marriage Allowance reduction (2026: 3,492 against 3,372 with full relief and the election). The route now compares the benefit unit's income tax; the case is a YAML test and an explicit example in both couple-tax properties, which fail on the old rule. 2015-16's transferable amount is fixed at 1,060 (ITA 2007 s.55B(4)(a)), not 10% of whatever the allowance is, via a fixed_amount parameter; the CPS reform respects it outside its expansion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The last description update dropped this PR's 🤖 Generated with Claude Code |
|
Coordination note: #2545 (fixing #2544, where Step 6 reductions offset the Step 7 charges) changes the lists this PR reads. Whichever lands second rebases.
If #2545 lands first, this PR's rebase:
If this PR lands first, #2545 carries |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # policyengine_uk/reforms/cps/marriage_tax_reforms.py
Main's Gift Aid and adjusted net income changes (#2253 and the s. 58 rework) moved the pension relief into adjusted net income while allowances still deduct it, and made Gift Aid extend the rate limits instead of coming off income. The rate test took allowances off adjusted net income, so after the merge it deducted pension relief twice (a higher-rate pension saver passed s. 55B(2)(b)) and took the gross gift off income below dividends instead of extending the basic rate limit (a donor with dividends failed s. 55B(2)(ba)). It now reads non_savings_taxable_income, the base savings and dividend tax use, and extends the non-savings and dividend limits by the grossed-up gift (ITA 2007 s. 414(2)(b)), as earned_income_tax does. The election also holds net_income in its branches, so they do not rebuild it from the held adjusted net income through net_income_from_adjusted_net_income. Also drops the stale #1947 xfail allowlist entry (the xfail went with the fix) and the stale Marriage Allowance sentence in non_savings_taxable_income. 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: 2 (thresholds in Notes
Moves flagged for review
Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
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
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: population. 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
|
…rtner roles From the independent review of 10e6b33: the fixture left is_claimant_or_partner to inference from ages, which can read a married couple aged 18 and 100 as parent and child, so neither is a spouse and test_only_married_couples_transfer failed on a valid draw. Adults are now claimant or partner and children are not, as in the other fixtures, and that couple is pinned as an example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Preserve final income tax inputs on the real simulation while recomputing Marriage Allowance counterfactual tax. Add composition and couple-tax regressions, and update the two Scottish Pension Credit fixtures from independently sourced 2025-26 arithmetic.
|
At The latest validation ran 807 Python IDs (806 passed; one credential-dependent skip) and 11 affected YAML calls (all passed). Qualified retained outcomes bring the accounting ledger to 8,241 unique IDs (8,137 passed; 104 skipped); they do not represent one whole-head execution. The unchanged Darwin/glibc skip keeps the actual strict checker exit at 1. Actual resulting-head Linux CI is pending. Independent committed-snapshot code/source review is COMPLETE/APPROVE with no open findings or material gaps; the fix verifier is CLEAN. C1/C2/C3 are resolved. Source verification covered 30 distinct cached official originals; it supplies no private-population or encoded-parity certificate. Axiom parity and data #502/d833, CPS #2553/Steps #2545, consumer adoption and owner acceptance remain qualified; no merge or new lifecycle effect is requested by this reply. |
|
Published |
Main now adds the winter fuel payment charge to income_tax and settles the property allowance route on the benefit unit's tax including the charge. - The Marriage Allowance election compares income tax before the charge. The charge falls on a person whose own total income is over the threshold, which the election does not change, so it can differ between the two positions only through a relevant benefit, and the election does not weigh benefits. This keeps benefits out of the election's branches. - The property allowance route is weighed on the benefit unit's total tax, since the route can move the spouse's tax through the election. - Take main's six CI shards and drop the branch's eight-shard change and the declared capital gains tax read in fiscal_partition.yaml: the read made the counted-once guard report a false positive (capital gains tax never moves at any guard step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now presumes a member 20 or more years younger than the claimant is their child (613326b), so the pensioner transferor case read the couple as parent and child and no one elected. Set is_claimant_or_partner for both, as the variable's documentation advises. Also tighten the comment on why the election compares tax before the winter fuel payment charge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main into this branch ( The head was
Kept from the 22-23 Oct commits:
Dropped:
Changed for main's winter fuel payment charge: main's
To keep a single writer on this branch, please coordinate here before pushing: I'm continuing it through the impact re-run, review and hand-off to the UK hub. The PR body will carry the details. |
|
#2723 depends on this PR. Its certified aggregate-impact test (
The statutory transfer here (s.55B(4)-(6): a fixed £1,260, with the transferor's allowance cut by it) has no such step. I have not run the guard on the two branches together. |
Round-3 review of 8117a44 found tests that still asserted the old mechanics: - CPS switch suite (#2553): the expanded allowance is a Step 6 reduction at the basic rate, and a CPS rate alone keeps the income condition. The removed eligible_bands parameter is no longer read. Monotonicity is on the transferable amount; a couple that stops electing as the rate rises is pinned as intended. Bands are judged a penny lower, since tax_band calls taxable income at the basic rate limit higher. - Pension Credit earnings oracle: the transferor's notional tax uses the allowance cut by s. 55B(6). - Statutory paternity pay: the recipient's tax is monotone only while the election is unchanged. - Marriage Allowance properties: tax before the winter fuel payment charge, which the election weighs; a couple whose election starts the charge is pinned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_partial_setter_failure_restores_only_touched_input patched child.set_input with a plain closure. Simulation.clone copies instance attributes and rebinds only bound methods, so every branch the Marriage Allowance election makes from the child kept the closure: the branch's set_input wrote to the child, the branch never held its election, and the election's formula branched again until the recursion limit (CI shard 2/6 on 8117a44). Binding the setter to the child lets clone rebind it to each branch; only the child's own write fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The conditions property judged tax_band a penny lower to step round tax_band calling income exactly at the basic rate limit higher. That missed an earner liable at the higher rate on less than a penny (£50,270.005, stored as £50,270.00390625), so a £0.01 tolerance in the condition could pass. The property now compares each earner's taxable income with the 2025-26 limits, strictly, and checks both ways; the sub-penny earner is an @example and a pinned case. A £0.01 tolerance mutant now fails both tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-core records each new branch in every calculation under way (get_branch adds it to each open frame's created_branches), and those frames keep it until they finish. So the two branches behind each Marriage Allowance election, with their copies of the simulation's arrays and their own results, lasted until the calculation that asked for the election ended, though they were already out of simulation.branches. tax_position now clears their storage when it drops them. It clears it directly: delete_arrays and drop_computed_arrays would record a change in each of those frames, though nothing reads the branch again. On a 2,033-household sample of the certified Enhanced FRS, the traced peak while calculating one year's household_net_income is 48.4 MB on main 41fa5dd, 77.1 MB on 5cedfaf and 52.0 MB here. Seven outputs over three years are bit-for-bit the same as at 5cedfaf. CI's test shard 3, which reaches 15.2 GB of a 16 GB runner on main, lost its runner at every head of this PR since the 28 Oct merge. test_the_election_releases_its_branches fails at 5cedfaf and passes here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
meets_marriage_allowance_income_conditions stops reading tax_band. It ports liable_only_at_marriage_allowance_rates from #1963 and computes it from income before the Marriage Allowance, since this head still takes the allowance off earned_taxable_income: non-savings income up to the start of the Scottish higher rate or the basic rate limit, savings above the limit only where the savings nil rate and the starting rate for savings cover them, and dividends counted without the dividend allowance (s. 55B(2)(ba)). The CPS expanded reform reads the same helper, and the eligible_bands parameter, which nothing else read, goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1947.
Builds on #1949 (merged), which deducts each person's own tax on earnings in UC.
What the law says and what changes
ITA 2007 ss. 55A-55E (legislation.gov.uk, read 2026-09-30; verbatim text in the issue):
fixed_amountparameter)would_claim_marriage_allowancewould_claim_marriage_allowance: one draw per couple, and the same draw that decided take-up before (the dataset draws it per person at 50%)The rate test reads the model's own liabilities. Two points follow from the statute's wording:
Scottish savings and dividends are banded on UK rates, so an intermediate-rate Scottish taxpayer with savings stays eligible. The old band test made them ineligible.
Variables
New, all Person, YEAR:
marriage_allowance_transferable_amount: s. 55B(4)-(5).is_marriage_allowance_spouse: married and the claimant or partner (Replace generic child and adult flags with each programme's legal definitions #1896'sis_claimant_or_partner), only where the benefit unit has two. Children are never spouses.makes_marriage_allowance_election: the couple's choice; users can set it as an input.marriage_allowance_relinquished: s. 55B(6).marriage_allowance_appropriate_percentage: s. 55B(3).marriage_allowance_tax_reduction_limit: s. 29(2)-(3).marriage_allowance_tax_reduction: s. 55B(1). Added toincome_tax_subtractions.Changed:
marriage_allowanceis now the amount received from the spouse's election, and only the gaining spouse has it. It no longer comes off taxable income: it was removed fromearned_taxable_income_exclusions.personal_allowancesubtractsmarriage_allowance_relinquished.meets_marriage_allowance_income_conditionsis the rate test above.Removed:
partners_unused_personal_allowance: it is not part of the law and nothing else used it.eligible_bandsparameter.The CPS expanded-MA contrib reform now overrides
meets_marriage_allowance_income_conditionsandmarriage_allowance_transferable_amount. Its mechanics follow the statute, so a higher-rate recipient saves the basic rate × the amount, not their marginal rate. It also lifts the income condition only whenremove_income_conditionis set; before, raising onlyma_ratelifted it too.How the election is computed
The election depends on the couple's tax with and without it, and the tax depends on the election. So
makes_marriage_allowance_electionruns two policyengine-core branches, the pattern PE-US uses for itemisation:For each spouse, the saving from electing is the partner's reduction (capped at their Step 5 tax left) minus the elector's extra tax. It is rounded to pennies. The couple elects if the saving is positive and the conditions hold. The spouse with the larger saving elects.
The inputs to income tax that don't depend on the election (employment income, adjusted net income, the other allowances, savings, dividend and property income, the Child Benefit charge, other tax credits) are resolved first and set as inputs in both branches. So is the property allowance route (
uses_property_allowance, ITTOIA 2005 s. 783BL), which is settled before the election. Holding them fixed does three things:set_inputon a branch drops values derived from the input it replaces, the branches don't recompute them.Branches share the simulation's cached arrays until they write (core#556) and are dropped after use.
Tie-break: when both directions save the same, the spouse with the lower adjusted net income elects, then the elder, then the first listed.
Property allowance route: main now chooses each landlord's property allowance route (ITTOIA 2005 ss. 783BE-783BL) by comparing tax with and without the allowance. This PR makes that comparison weigh the benefit unit's income tax, not the landlord's alone. A route can change whether a spouse elects, and so the other spouse's tax. Example (2026): a spouse on £12,000 with £800 of lettings and £200 of finance costs, married to a basic-rate earner on £30,000. Full relief plus the election costs the couple £3,372. Electing out of the allowance for the finance-cost reduction costs the landlord £6 instead of £138, but stops the election, so the couple pays £3,492. The independent review found this; it is now a YAML case and an explicit example in both couple-tax properties, which fail with the old own-tax rule.
Pinned election: set as an input, an election is taken to be one the other spouse is entitled to gain from; the conditions are checked only when the model makes the choice. Only a spouse can give allowance up.
Universal Credit
uc_income_tax_on_earnings(#1949; on main it now callspolicyengine_uk/utils/uc_income_tax.py) sets the Step 6 reductions inincome_tax_subtractionsagainst the tax on earnings first, except the property finance-cost reduction, which goes against tax on property income. This PR adds the Marriage Allowance reduction to that list, so the recipient's reduction comes off the tax on their earnings. That matches how HMRC delivers it: gov.uk's tax code letters say code M means "you've received a transfer of 10% of your partner's Personal Allowance". So a pensioner's State Pension no longer changes the earning partner's UC earned income once the couple has elected. The strict xfailtest_state_pension_does_not_change_partners_marriage_allowancenow passes.Known interaction (intended): the election weighs the couple's income tax before the winter fuel payment charge, not their UC. At the point where electing stops saving tax, the couple stops electing. The earner's tax then rises by up to £252, and their UC rises by up to 55% of that.
test_couple_stops_electing_when_it_no_longer_saves_taxpins an example: 2026, State Pension £12,500 → £12,600, UC £5,509.26 → £5,547.86. A choice rule that also weighed UC would behave differently; that is a separate modelling decision. The same holds for the winter fuel payment charge, which turns on UC and Pension Credit:test_electing_can_start_the_winter_fuel_payment_chargepins a 2026 couple aged 70 and 45 whose election saves £114 before the charge, ends their £1 of UC and so starts a £200 charge (income tax including it: £7,002.26 → £7,088.26).Departures in the issue, each now a YAML test
Also covered:
meets_marriage_allowance_income_conditions.yamlis rewritten on incomes, not tax-band inputs. It includes s. 55C(1)(c) with the cut allowance (50,000 earnings: eligible with the full allowance, ineligible once it falls to 11,310).Invariants and property tests
test_marriage_allowance_properties.py(Hypothesis). It generates couples and singles with every taxable income type, in England, Wales and Scotland, with children and occasional married couple's allowance claims, for 2019, 2026 and 2027. "Income tax" in these is tax before the winter fuel payment charge, which is what the election weighs:test_income_conditions_match_the_tax_bands).UC properties now run with Marriage Allowance claimed:
test_uc_state_pension_properties.py: non-increasing, pound-for-pound, the private-pension equivalence, and property income excluded while State Pension counts;test_uc_earnings_deductions_properties.py: invariants 1-3, with Deduct only each person's own tax and NI on earnings from UC earned income #1949's married couple's allowance cases;test_uc_income_from_capital_properties.py: invariant 2.Before, these switched Marriage Allowance off "until this is fixed". Where a property compares two runs, each couple's election is held at its first-run choice, because the election legitimately responds to income.
Mutation check (YAML): ten mutants, each restoring one old behaviour (the unused-allowance cap, relief off taxable income instead of Step 6, no transferor cut, band-based eligibility, double counting, rounding of a partial amount, and four more), were all killed at
418a0b9b3(evidence kept with the review files). They were not re-run after the 20 Oct merge; the mutated code paths are unchanged except the rate test, whose new behaviour the cases below pin.Impact on the certified pair
Real
Microsimulationruns onenhanced_frs_2024_25.h5from policyengine-uk-data 1.58.1 (sha256056e80fd8e91…, the dataset in policyengine.py's certified UK bundle). Base: main76cfc9e79. Head: this PR at10e6b338c(d75ab7ac3changes only a test fixture). Four runs: base and head, each also with no one claiming (would_claim_marriage_allowancefalse for everyone; head no-claim atd75ab7ac3), 2025-26 to 2030-31. All trees clean. These figures predate the 28 Oct merge below; the re-run on the merged head is pending.£m a year, head minus base, weighted. Households, benefit units and people are weighted counts; the dataset records behind them follow in brackets.
"Marriage Allowance cost" is income tax with no one claiming (
would_claim_marriage_allowancefalse for everyone) minus income tax as modelled, in the same model.Decomposition, 2026-27 (income tax change, head minus base; couple counts are weighted couples, and the records behind them are spouses, two per couple):
Poverty rates, percentage points, head minus base:
2026-27 levels (base → head): BHC all 14.94% → 14.94%; BHC children 20.35% → 20.35%; BHC working age 13.51% → 13.51%; BHC pensioners 13.11% → 13.11%; AHC all 20.37% → 20.37%; AHC children 29.26% → 29.26%; AHC working age 19.44% → 19.44%; AHC pensioners 12.26% → 12.26%.
Household net income by decile of base household income, 2026-27:
Runtime and memory (one process per run, same dataset bytes):
Poverty changes for 2025-26 and 2029-30 rest on fewer than three households and are suppressed (–); every other year is unchanged to 0.001pp. Cells resting on fewer than three dataset records are shown as counts only.
What moves:
The 3 Oct figures this body used to carry were on a different dataset (uk-data private copy
e433e532…) and an older main; they are replaced.Not modelled, and limits inherited from the model
Not modelled:
The election is judged on the model's own income tax, so it inherits these pre-existing departures, each filed as a follow-up (all rechecked on 2026-10-12 main):
Main has since fixed six limits this PR first listed:
35f992c6a). The model no longer elects for a transferor with £12,500 of earnings and £5,500 of interest married to a £30,000 earner: forcing the election costs the couple £124 (£3,610 against £3,486, checked atfae3baaea);fae3baaea).The remaining two were rechecked on 12 Oct main, not since.
One election per couple. s. 55E limits each individual to one election and one reduction but does not bar elections both ways. Both ways lowers tax where giving up allowance costs less than the basic rate. For example, two spouses each with £20,000 of dividends (2026) pay £1,489.95 with no election, £1,373.40 with the model's single election, and £1,256.85 with both electing. The model allows one election per couple, the single transfer from the lower earner that HMRC's guidance describes.
Double taxation relief should come after the Marriage Allowance (s. 27(6)), but
other_tax_creditslumps it with other reductions. Total liability is unaffected; only the split of the reduction is.SPI dataset:
policyengine-uk-data's SPI build suppliesmarriage_allowanceforMAIND == 1. SPI: keep Marriage Allowance on recipients (MAIND) and give transferors their cut allowance policyengine-uk-data#502 remains a draft intended to keep that recipient input and supply the transferor allowance cut. It depends on this PR and the separate batched data release (d833); this continuation does not establish approval, data landing or consumer adoption.The 20 Oct merge of main
Merging main
76cfc9e79conflicted only in the import block ofreforms/cps/marriage_tax_reforms.py(both sides kept). Main had since reworked the income tax pipeline, and one change broke this PR's rate test. Adjusted net income now deducts the pension relief that allowances also deduct, and Gift Aid now extends the rate limits instead of coming off income (#2271). The rate test took allowances off adjusted net income. So it deducted pension relief twice, which let a higher-rate pension saver pass s.55B(2)(b), and it took the gift off income below dividends instead of extending the basic rate limit, which failed a donor with dividends under s.55B(2)(ba).10e6b338cmakes the rate test readnon_savings_taxable_income, the base that savings and dividend tax use, and extend the non-savings and dividend limits by the grossed-up gift (ITA 2007 s.414(2)(b)), asearned_income_taxdoes. The election's branches also holdnet_income, so they don't rebuild it from the held adjusted net income. Main'suc_income_tax_on_earningsnow lives inutils/uc_income_tax.py, and it already subtracts every Step 6 reduction inincome_tax_subtractionsfrom the tax on earnings first, so this PR's own UC edit fell away in the merge. The commit also removes the #1947 xfail entry from.github/test_outcome_allowlist.toml, which the regression checker requires once the xfail is gone.Tests after the merge:
829fccae4(the merge, before the fix): 530 passed. The new rate-test cases fail there (4 failed, 3 passed) and pass at10e6b338c.10e6b338c: 64 passed.76cfc9e79, only test files copied): the Marriage Allowance YAML gives 10 failed and 4 passed; the de-xfailedtest_state_pension_does_not_change_partners_marriage_allowancefails (UC earned income £18,033.60 against £18,171.60 in law).Overlaps
Limit Income Tax reductions to the Step 5 tax so they don't offset Step 7 charges #2545 (draft, addresses Income tax reductions offset the Step 7 charges (HICBC, annual allowance charge) #2544) renames
income_tax_subtractionstostep_6_tax_reductionsand splits Steps 5 and 7. Whichever lands second rebases. If Limit Income Tax reductions to the Step 5 tax so they don't offset Step 7 charges #2545 lands first: addmarriage_allowance_tax_reductiontostep_6_tax_reductions.yaml, rename the list inmarriage_allowance_tax_reduction_limit.pyandproperty_finance_cost_relief.py, and keep the Marriage Allowance reduction last and UC's earnings-first deduction.SPI: keep Marriage Allowance on recipients (MAIND) and give transferors their cut allowance policyengine-uk-data#502 (draft) keeps the SPI's Marriage Allowance on recipients (
MAIND) and gives transferors their cut. It waits on this PR and lands in the batched uk-data release (d833).Stop the statutory Marriage Allowance rate from switching on the CPS expanded Marriage Allowance #2553 merged on 23 Oct. Its CPS trigger (
rate != default_rate, from the CPS parameters) is kept in the 28 Oct merge, with this PR'sremove_income_condition=bool(remove_income_condition)argument.Model the 25p State Pension age addition at 80 #2340 (State Pension age addition at 80, handed to the UK hub) edits the same lines of
basic_state_pension.pyandtest_state_pension_components.pyas the float64 change here. Whichever lands second merges main and keeps both.Axiom
axiom: TheAxiomFoundation/rulespec-uk#362 + TheAxiomFoundation/rulespec-uk#437 queued
pe-parity): s.55C, extended on 2026-09-30 for this PR with s.55C(1)(ca) and s.55B(2)(ba) and seven companion cases.pe-parity): the s.55B(2)(ba) dividend nil-rate look-through for the s.55B re-encode.bulk/worklist.yaml(batch PE-UK-CHILD-DEFS, queued by rulespec-uk#354, closed 2026-09-30). It already requires the fixed transferable amount, the s.55B(6) cut and the s.55B(1) reduction.rulespec-uk's s.55B module still caps the transfer at the partner's unused allowance: it encodes the same departure PolicyEngine had. The worklist entry and #362 both called the PolicyEngine side "a separate follow-up"; this is it.
Review
dafe6301con 12 Oct, then of99182e396) were cut off before they reported. The first found the property-route interaction above and six smaller points, all addressed.10e6b338con 21 Oct: REQUEST_CHANGES on one P2, a test-fixture defect, not a model defect. The property fixture leftis_claimant_or_partnerto inference, which reads a married couple aged 18 and 100 as parent and child.d75ab7ac3sets the roles and pins that couple as an example; it fails without the roles and passes with them. The review found the statutory mechanics, the election engineering and the merge fix sound. It labelled two rules as modelling choices: one election per couple, and an election that weighs income tax only, not UC.d75ab7ac3(21 Oct) was served by Opus 5.5, not Sol (the Codex lanes were closed): APPROVE, with five P3s on this body. They are fixed here: uk-data#502 is a draft, the Axiom line cites Remove benunit- and household-level saved random numbers #437 and Validate against HMRC 'ready runner' #354, decomposition counts are spouse records, property 7 is listed, and the head no-claim run is pinned.8117a4462: REQUEST_CHANGES on three P2s and two P3s, all in tests and this body (see "The 28 October review" below). It found the merge itself sound: the before-charge objective is labelled, the private branches hold the inputs the election does not change, and the guard, fiscal partition and workflow equal main's.21977b274andfae3baaeaaddress all five.fae3baaeaon 28 Oct: APPROVE, all five fixed, with two P3s. One, a sub-penny gap in the conditions property, is fixed in5cedfaf9f. The other, on this body's CI paragraph, is corrected in "CI and memory" below.69c919f60frees the memory the election's branches held (see "CI and memory"). It needs its own review before this PR leaves draft.Reproducer:
policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml,policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/marriage_allowance.yaml,policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/meets_marriage_allowance_income_conditions.yaml,policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/property_income_band_base.yaml::Case 7, Marriage Allowance leaves the band base alone (#1947).,policyengine_uk/tests/policy/reforms/cps/marriage_tax_reforms.yaml::Expanded MA — rate-only expansion keeps the higher-rate bar,policyengine_uk/tests/test_marriage_allowance_election_branches.py::test_results_do_not_depend_on_query_order22 October continuation
The continuation at
00bf62a81008d3439ae8a43e1c4cf32412ab4ff8preserves the original PR ancestry and the pinned main integration. Private election counterfactuals discard an inherited final-income-tax input while the real simulation retains explicit inputs. Fiscal declarations now document the CGT-to-income-tax eligibility read. BASIC and NEW State Pension calculations promote both operands to float64 before division to preserve finite tiny-cap inputs. The Pension Credit Class 2 regression explicitly isolates a married couple with neither partner claiming Marriage Allowance. The original one-election-per-couple and income-tax-only objective remain.Validation actually ran 807 selected Python IDs in the last continuation: 806 passed and one credential-dependent test skipped. Eleven actual affected State Pension YAML calls passed. The unmodified strict checker exited 1 solely for the unchanged Darwin/glibc
malloc_trimskip; no local strict pass is claimed. Formatting changed no Python AST, and runtime stat/link witnesses remained unchanged.The accounting ledger combines those 807 IDs with 7,434 qualified retained outcomes, covering 8,241 unique IDs (8,137 passed and 104 skipped). Retained per-node evidence keeps its original source/runtime/impact qualifications. The original full and earlier bounded runs timed out; their raw outcomes remain preserved. These overlapping batches are not added or represented as whole-head execution.
Independent code and source review of committed head
00bf62a81008d3439ae8a43e1c4cf32412ab4ff8completed with no open findings or material coverage gaps. The fix verifier reported CLEAN; C1, C2 and C3 remain recorded as resolved. The source review validated 30 distinct cached official originals (83 hash records); it did not rerun model tests or establish encoded parity.The historical population, mutation and author-review figures above retain their dated pins. This continuation did not rerun the private population comparison, establish encoded parity, or certify deployed/owner acceptance. Axiom follow-up, rulespec-uk #362/worklist debt, data #502/d833 release and consumer rebind/adoption, and #2545/#2553 coordination remain open. Actual resulting-head Linux CI, READY, post-ready Fix and the correct Vahid review request are later gates.
CI investigation update — 23 October 2026
Commit
c4cfc9489becb74c616e1f988fef4377793a8b46adds a bounded accounting-guard failure message that includes only trusted public identifiers and privacy-suppressed aggregate counts. The assertion, tax calculations, guard rules and tolerances remain unchanged. Both source and wheel test jobs hit their existing one-hour limit on the preceding head; the shared matrix now uses eight shards with the same four workers, strict outcome audit and time limit. This adds runner capacity; it is not a demonstrated runtime or compute saving.The meaningful regression reproduced the original failure message exposing a synthetic private-field sentinel. The other new API cases failed before the helper existed. After implementation, all 123 selected Python cases and the strict selected-outcome checker passed. These cases include the 17 earlier RED/control cases; counts are not added. A formatter-only proof preserves the tested Python semantics. Independent code and privacy/source review of this committed update completed with no open findings or material gaps, and C4/C5 verification is CLEAN.
Focused regression checks:
policyengine_uk/tests/aggregate_impact/test_aggregate_impact_measure.py::test_certified_guard_failure_message_excludes_private_fieldspolicyengine_uk/tests/code_health/test_ci_test_shards.py::test_configured_source_and_wheel_shards_account_for_every_nodeThe original runtime stat/link guard stopped on generated NumPy caches. Its failed result is preserved. An independent, bounded source/header/code/installed-RECORD check qualified exactly 82 new bytecode caches, seven cache directories and seven parent-directory metadata changes; it did not establish the creator PID or whole-runtime byte identity. No tests were replayed to dismiss that guard.
The certified per-household accounting failure on
00bf62a8remains causally unresolved. The public synthetic cases did not reproduce it. This diagnostic update does not fix or waive that invariant, and this PR remains draft until actual resulting-head Linux CI and the accounting gate pass. No full suite, certified/private dataset or encoded-parity run was performed locally for this update. All earlier policy, source, Axiom and adoption qualifications remain.The 28 October merge of main
1cd797715merges main41fa5ddb5intoc4cfc9489. Five files conflicted:.github/workflows/test-suite.yaml,docs/engineering/skills/testing.md: main's six shards. The 23 Oct eight-shard change is dropped: main has re-sharded since, and the workflow is not this PR's to change. The 23 Oct shard-accounting test reads the shard count from the workflow, so it stays.policyengine_uk/data/fiscal_partition.yaml: main's file. The 22 Oct declaration that income tax reads capital gains tax is dropped (see "The accounting failure" below).uses_property_allowance.py: main now weighs each landlord's route on income tax including the winter fuel payment charge, with ceiling bounds on the charge. The merge keeps that and, as this PR did before, sums both routes' tax over the benefit unit, because the route can move the spouse's tax through the election.test_property_income_properties.py: main's pensioner property test, asserting the benefit unit's tax instead of the landlord's own, and this PR's couple test.The election compares tax before the winter fuel payment charge. On main,
income_taxnow adds the charge (winter_fuel_payment_charge, ITEPA 2003 s.681I), and the charge reads Pension Credit and UC. Left comparingincome_tax, the election's branches would compute benefits for every couple. They compareincome_tax_before_winter_fuel_payment_chargeinstead:This is a modelling choice, labelled as one.
test_final_tax_input_does_not_change_the_election_or_capital_gains_taxnow runs for bothincome_taxand the before-charge tax.The accounting failure. On
00bf62a8andc4cfc948, CI failedtest_every_tax_component_is_counted_exactly_once: perturbingincome_taxwas reported to movecapital_gains_taxone for one in 89 households.capital_gains_taxat every guard step showed it moved in no household at any step.76108fafe), andguard.pyhere is main's.The 22 Oct private-branch
delete_arrayschange, the float64 State Pension change, the Pension Credit Class 2 fixture and the 23 Oct diagnostics inmeasure.pyare kept.Main's new household presumption.
613326be1presumes a member 20 or more years younger than the claimant is their child, at any age. The YAML case "Pensioner transferor with State Pension" is a couple aged 70 and 45, so it now suppliesis_claimant_or_partnerfor both (8117a4462). Without the roles, the case fails on the merge (no election); with them, it passes.Tests at
8117a4462:policyengine-core testontests/policy/baseline/gov/hmrc/income_taxandtests/policy/reforms/cps: 460 passed.1cd797715, before the role fix, the 70/45 case was the one failure.test_marriage_allowance_properties.py,test_marriage_allowance_election_branches.py,test_property_income_properties.py,test_uc_state_pension_properties.py,test_state_pension_components.py,code_health/test_ci_test_shards.pyandaggregate_impact/test_aggregate_impact_measure.py, with-n 3: 186 passed in 13 min 27 s. The run started at1cd797715;8117a4462changes only a YAML case and a comment.Impact. The certified-pair figures above are at
10e6b338con main76cfc9e79. The re-run on the merged head is held by the host's Budget-day reservation until 29 Oct 07:00Z, and this body will carry it before the PR leaves draft.The 28 October review
GPT-6.1 Sol's findings at
8117a4462, and what21977b274does about each. Only tests, one comment and this body change; the model code is as at8117a4462.fae3baaeathen fixes one more test, which CI found, and5cedfaf9fthe round-4 review's P3 on the conditions property (see "CI and memory").test_marriage_allowance_cps_switch.py:remove_income_condition, the £60,000 earner's tax is 11,432 − 1,260 × 20% = £11,180, not £10,928.remove_income_conditionlifts it), so the higher-rate earner gets nothing.eligible_bandsparameter is no longer read; the conditions test names the electing bands itself.test_a_couple_can_stop_electing_as_the_statutory_rate_risespins that couple as intended.test_pension_credit_earnings_disregard_properties.pycomputed the notional tax deduction on a full £12,570 allowance. It now uses the allowance aftermarriage_allowance_relinquished, as the model does (s. 55B(6)). The reviewer's example, a Scottish couple each with £15,000 of self-employment income, does not elect: their taxable profit is above the £2,306 starter band either way, so the £1,260 given up costs 20%, the same as the reduction, and electing saves nothing. The test pins a couple that does elect: an English couple aged 67 with £12,000 and £15,000 of self-employment income in 2024. Deductions are £138 and £234; the old oracle expected £0 for the transferor.test_a_couple_can_stop_electing_as_statutory_paternity_pay_risespins that couple.test_electing_can_start_the_winter_fuel_payment_chargepins the reviewer's couple: electing saves £114 before the charge, ends £1 of UC and starts a £200 charge.fae3baaea.A further finding while fixing the first P2:
tax_bandplaces taxable income exactly at a band's lower limit in that band, though none of it is charged at that rate (s. 10). At £50,270 of earnings (£37,700 of taxable income, all at the basic rate) it reports HIGHER, and the model correctly lets the earner gain.test_taxable_income_at_the_basic_rate_limit_still_meets_the_conditionspins £50,270 and £50,271. At21977b274the conditions property judged bands a penny lower; the round-4 review found that this leaves income under a penny over the limit unchecked, and5cedfaf9freplaces it (see "CI and memory").Tests:
21977b274, the four changed test files (test_marriage_allowance_cps_switch.py,test_marriage_allowance_properties.py,test_pension_credit_earnings_disregard_properties.py,test_statutory_paternity_pay_income_tax_properties.py), with-n 3: 71 passed in 12 min 15 s.21977b274, the other 15 Python test files that compute the Marriage Allowance (benefit cap, dated reform routes, deterministic variables, Pension Credit Class 2, reform representations, trading losses and the UC property files): all passed. That run also held the pre-fix versions of three of the changed files; its 15 failures were all in the old CPS switch file, which the 71 above replace (324 tests, 1 h 7 min).fae3baaea,test_aggregate_impact_childcare_provenance.py: 42 passed. The rest ofpolicyengine_uk/tests/aggregate_impactexcept the certified-dataset test (which runs in its own CI job), with-n 3: 321 passed.8117a4462, whose YAML results above stand. Ruff format and lint pass on the changed files.CI and memory
Runners lost to memory. Every head since the 28 Oct merge lost test shards to memory within seven minutes: GitHub reported "The runner has received a shutdown signal" as an error, and each shard's last once-a-minute reading was 14.6 to 15.8 GB used of 16 GB.
8117a4462(run 40754493119, release run 40754493529): test shards 3, 4 and 5 and release-test shards 4 and 5. Shard 4 recorded passes before it stopped. Test shards 1, 2 and 6 and release-test shards 3 and 6 ran to the end. Release-test shard 2 finished its tests (1,722 passed; four failed: three CPS switch cases and the provenance test below) before the run was cancelled, and release-test shard 1 was cancelled before it finished.fae3baaea(run 40771609348): test shards 3 and 5. Shards 1, 4 and 6 and the YAML tests passed; shard 2 was cancelled when5cedfaf9fwas pushed.5cedfaf9f(run 40781132542, release run 40781132665): test shards 3 and 5 and release-test shards 3 and 4.Main's runs pass close to that limit. On two passing runs on 28 Oct (the merge queue's run 40775419493 and
savings-income-uprating-obr's run 40771218539), shard 3 read 15.6 and 15.4 GB used in its third minute. When5cedfaf9f's shard 3 stopped, one worker was runningmicrosimulation/test_reform_impacts.py, which calculates a reform and its baseline on the full dataset.Correction. Earlier versions of this section said main and two other PRs' runs lost shards the same way. They did not. Run 40393666784 is PR #2661's, not main's. The runs on
fix/2541-ca-earnings-penceandobr-october-2026-efo-2711lost no runner: a search of their logs for the message had matched the workflow's own comment, which quotes it.The cause: the election's branches outlived the election. policyengine-core records each new branch in every calculation then under way (
get_branchadds it to each open frame'screated_branches), and those frames keep it until they finish. So the two branches behind each election lasted until the calculation that asked for the election (household net income, say) ended, thoughtax_positionhad already removed them fromsimulation.branches. They held their copies of the simulation's arrays and their own results.69c919f60clears their storage whentax_positiondrops them (releaseinmakes_marriage_allowance_election.py). It clears the storage directly:delete_arraysanddrop_computed_arrayswould record a change in each of those frames, though nothing reads the branch again.Measured with tracemalloc on a sample of the certified dataset (every 26th household: 2,033 households, 4,346 people), calculating 2026 household net income in a fresh simulation:
41fa5ddb5fae3baaea(the model code of5cedfaf9f)69c919f60"Held afterwards" is after
gc.collect(). Agc.collect()after each branch was dropped, without clearing it, left the peak at 77.2 MB, because the branches were still referenced, not garbage. On that sample, household net income, income tax, the election, the Marriage Allowance, UC, Pension Credit and council tax reduction for 2025, 2026 and 2029 are bit-for-bit the same atfae3baaeaand69c919f60.test_the_election_releases_its_brancheschecks that the election's two branches hold nothing once it is made. It fails at5cedfaf9fand passes at69c919f60.CI on
69c919f60(run 40784891433, release run 40784891458) is running. Four minutes in, past the point where shards 3 and 5 had stopped on every earlier head, all shards were still running. This body will carry the result.Other failures on
8117a4462. The finished shards' failures were the CPS switch cases fixed above, and one more in shard 2:test_partial_setter_failure_restores_only_touched_input(test_aggregate_impact_childcare_provenance.py, unchanged from main) hit Python's recursion limit. The test replacedchild.set_inputwith a plain closure.Simulation.clonecopies instance attributes and rebinds only bound methods, so each branch the election made from the child kept the closure and wrote its inputs to the child. The branch never held its election, and the election's formula branched again until the stack ran out. No model code puts a closure on a simulation'sset_input.fae3baaeabinds the test's setter to the child, soclonerebinds it to each branch; only the child's own write fails, as the test intends. It fails at21977b274and passes atfae3baaea.The round-4 P3 on the conditions property. At
fae3baaea, the conditions property intest_marriage_allowance_cps_switch.pyjudgedtax_banda penny lower. A London earner on £50,270.005 has under half a penny of taxable income over the basic rate limit, but the penny-lower run reported BASIC, so a £0.01 tolerance in the income conditions could have passed.5cedfaf9fcompares each person's taxable income (earnings less personal allowance) with the basic rate limit itself (£37,700 in 2025-26, or £31,092, where the Scottish higher rate starts), and requires that everyone at or under it meets the conditions. It pins the £50,270.005 earner as an example and intest_taxable_income_at_the_basic_rate_limit_still_meets_the_conditions. A copy of the model with a £0.01 tolerance fails both tests.Tests:
69c919f60,test_marriage_allowance_election_branches.py -k "releases or no_branch": 2 passed. With5cedfaf9f's model code and the new test file,test_the_election_releases_its_branchesfails (the branches still hold their populations' values) and the other passes.5cedfaf9f's tests with69c919f60's model code, the 22 Python test files that compute the Marriage Allowance andtest_aggregate_impact_childcare_provenance.py, with-n 3, and the 12 YAML files that test it: running; this body will carry the result.🤖 Generated with Claude Code