Skip to content

Model Marriage Allowance as the statutory s.55B transfer and Step 6 reduction - #1963

Draft
MaxGhenis wants to merge 32 commits into
mainfrom
marriage-allowance-election
Draft

MaxGhenis wants to merge 32 commits into
mainfrom
marriage-allowance-election

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

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):

Before After (= law)
Transferable amount min(partner's unused allowance, 10% of PA), rounded up to £10 10% of the s. 35(1) allowance, rounded up to £10 (s. 55B(4)(b), (5)): £1,260 in 2026-27. 2015-16 is fixed at £1,060 whatever the allowance (s. 55B(4)(a), a new fixed_amount parameter)
Transferor allowance unchanged allowance cut by the transferable amount (s. 55B(6))
Recipient taxable non-savings income cut by the amount Step 6 tax reduction of the basic rate, or Scottish basic rate, × the amount (s. 55A(2), s. 55B(1), (3)), capped at the Step 5 tax left after other reductions (s. 29(2)-(3), s. 27(2))
Eligibility tax band of the recipient (NONE/STARTER/BASIC/INTERMEDIATE) recipient not liable above the listed basic rates (s. 55B(2)(b), (ba)); transferor the same once their allowance is cut (s. 55C(1)(c), (ca)) and entitled to an allowance (s. 55C(1)(b)); no married couple's allowance claimed by either (s. 55B(2)(d))
Who claims anyone married with a partner who has unused allowance, both partners if both are below the allowance the couple elects only if it lowers their combined income tax, in whichever direction lowers it more. One election per couple: s. 55E limits each individual to one election and one reduction, and does not bar elections both ways (which could lower some couples' tax further); HMRC's guidance describes a single transfer from the lower earner
Take-up the recipient's would_claim_marriage_allowance the couple's election uses the gaining spouse's would_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:

  • Dividends: the dividend nil rate does not help. s. 55B(2)(ba) tests the dividend upper rate "if section 13A (dividend nil rate) were omitted".
  • Savings: the savings nil rate does help. s. 55B(2)(b) lists it, and nothing like (ba) applies to savings. (Compare s. 12B(8)(b)(ii), which does look through s. 12A.)

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's is_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 to income_tax_subtractions.

Changed:

  • marriage_allowance is 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 from earned_taxable_income_exclusions.
  • personal_allowance subtracts marriage_allowance_relinquished.
  • meets_marriage_allowance_income_conditions is the rate test above.

Removed:

  • partners_unused_personal_allowance: it is not part of the law and nothing else used it.
  • the eligible_bands parameter.

The CPS expanded-MA contrib reform now overrides meets_marriage_allowance_income_conditions and marriage_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 when remove_income_condition is set; before, raising only ma_rate lifted 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_election runs two policyengine-core branches, the pattern PE-US uses for itemisation:

  1. No election in force.
  2. Every spouse's own allowance cut by the transferable amount, with no reductions granted.

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:

  • the result doesn't depend on which variable a caller asks for first;
  • a choice made upstream in branches of its own (a reform's split of adjusted net income, a labour supply response, the property allowance route) isn't remade for the counterfactual;
  • on policyengine-core ≥3.33, where set_input on 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 calls policyengine_uk/utils/uc_income_tax.py) sets the Step 6 reductions in income_tax_subtractions against 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 xfail test_state_pension_does_not_change_partners_marriage_allowance now passes.

Known interaction (intended): the election weighs the couple's income tax, 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_tax pins 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.

Departures in the issue, each now a YAML test

Issue item Test (marriage_allowance.yaml) Old model Law = new model
Worked example (30,000 / 12,000) "Issue example" 3,372 / 0 3,234 / 138
1. Transferor with £15,000 / £12,000 of interest "Issue departure 1" (×2) recipient 3,486 / 3,372 3,234
2. Recipient with £12,570 earnings, £5,000 dividends "Issue departure 2" 483.75 231.75
3. Scottish intermediate-rate recipient "Issue departure 3" saves 264.60 saves 252 (3,199.07)
4. Rounding of the partial unused amount every case: the amount is always 1,260
5. Double counting when both are below the allowance "Issue departure 5" 1,260 each 0
6. Married couple's allowance not checked "Issue departure 6" MA given no election

Also covered:

  • the transferor just under / at the allowance (elect / not);
  • a higher-rate recipient;
  • take-up follows the gaining spouse;
  • direction regardless of age order;
  • the cap at Step 5 tax (86);
  • other tax reductions first (186);
  • a tie (the lower-income spouse elects, then the elder);
  • children in the unit;
  • the dividend nil-rate vs savings nil-rate straddles;
  • Scottish savings banded on UK rates;
  • the pensioner case from the UC test;
  • 2015-16 (£1,060) and 2019-20 (£1,250).

meets_marriage_allowance_income_conditions.yaml is 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:

  1. The couple's income tax is never higher with the model's election than with none. When the couple elects, it is at least a penny lower.
  2. Differential against brute force: the model's couple tax equals the minimum over {no election, first spouse elects, second spouse elects}, each run with the election fixed and filtered by the statutory conditions computed from those runs.
  3. With the election fixed, the gaining partner's Marriage Allowance, reduction, allowance and income tax do not change when the elector's income of any type rises. The elector's allowance is exactly the uncut allowance less the transferable amount.
  4. Accounting:
    • only spouses elect, at most one per couple;
    • the amount given up equals the amount received in each couple, and is 0 or the transferable amount;
    • the reduction is at most the appropriate percentage × the amount and the Step 5 limit;
    • the gainer's tax falls by exactly the reduction;
    • the elector never pays less.
  5. The couple's income tax, with the model's election, never falls when either spouse gains income.
  6. No one outside a married couple elects, gives up or receives anything.

UC properties now run with Marriage Allowance claimed:

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 Microsimulation runs on enhanced_frs_2024_25.h5 from policyengine-uk-data 1.58.1 (sha256 056e80fd8e91…, the dataset in policyengine.py's certified UK bundle). Base: main 76cfc9e79. Head: this PR at 10e6b338c (d75ab7ac3 changes only a test fixture). Four runs: base and head, each also with no one claiming (would_claim_marriage_allowance false for everyone), 2025-26 to 2030-31. All trees clean.

£m a year, head minus base, weighted. Households, benefit units and people are weighted counts; the dataset records behind them follow in brackets.

Year Income tax Household net income Universal Credit Pension Credit Housing Benefit Council Tax Reduction Government balance
2025-26 -37.6 +37.4 -0.2 -0.0 -0.0 +0.0 -37.4
2026-27 -45.4 +45.1 -0.3 -0.0 -0.0 -0.0 -45.1
2027-28 -75.0 +74.8 -0.3 +0.1 -0.0 +0.0 -74.8
2028-29 -75.4 +75.1 -0.3 +0.0 -0.0 -0.0 -75.1
2029-30 -73.5 +73.0 -0.3 -0.1 -0.0 +0.0 -73.0
2030-31 -68.1 +67.5 -0.3 -0.1 -0.1 -0.0 -67.5
Year Marriage Allowance cost, main Marriage Allowance cost, this PR Couples electing Reductions granted (£m) Gaining spouses with the full reduction People with Marriage Allowance on main
2025-26 334.4 372.0 1.71m (3,480 records) 426.4 1.67m (3,284 records) of 1.71m (3,480 records) 2.47m (5,179 records)
2026-27 308.9 354.3 1.61m (3,355 records) 397.7 1.56m (3,119 records) of 1.61m (3,355 records) 2.18m (4,789 records)
2027-28 273.1 348.1 1.54m (3,239 records) 380.6 1.48m (2,977 records) of 1.54m (3,239 records) 1.99m (4,538 records)
2028-29 271.0 346.4 1.59m (3,159 records) 380.1 1.47m (2,924 records) of 1.59m (3,159 records) 1.90m (4,452 records)
2029-30 268.4 341.9 1.56m (3,059 records) 375.8 1.44m (2,856 records) of 1.56m (3,059 records) 1.87m (4,286 records)
2030-31 266.4 334.5 1.51m (2,945 records) 370.5 1.40m (2,795 records) of 1.51m (2,945 records) 1.78m (4,121 records)

"Marriage Allowance cost" is income tax with no one claiming (would_claim_marriage_allowance false for everyone) minus income tax as modelled, in the same model.

Year People whose income tax changes People whose tax moves between partners, couple unchanged Households gaining > £1 Households losing > £1 Benefit units whose UC changes
2025-26 1.01m (1,680 records) (0 records) 0.40m (571 records) 0.19m (240 records) 18.37k (22 records)
2026-27 0.87m (1,473 records) (2 records) 0.41m (513 records) 0.14m (233 records) 13.56k (21 records)
2027-28 0.82m (1,410 records) (0 records) 0.43m (485 records) 0.10m (232 records) 12.95k (20 records)
2028-29 0.86m (1,380 records) 0.00m (4 records) 0.43m (469 records) 0.14m (213 records) 14.09k (24 records)
2029-30 0.96m (1,293 records) (0 records) 0.39m (420 records) 0.13m (215 records) 11.66k (26 records)
2030-31 0.87m (1,203 records) (0 records) 0.40m (416 records) 0.10m (196 records) 10.68k (20 records)

Decomposition, 2026-27 (income tax change, head minus base):

  • Electing spouses' tax: +43.4m; gaining spouses' tax: -89.0m.
  • Couples that elect now but had no Marriage Allowance on main: 0.21m (518 records), tax -27.1m.
  • Couples with Marriage Allowance on main that do not elect now: 0.63m (2,548 records), tax +0.2m. On main, 0.29m (838 records) people had it on both partners (the double counting).
  • Couples with Marriage Allowance on both: 1.40m (6,192 records), tax -18.5m.
  • Scottish gaining spouses: 0.18m (397 records), tax -7.9m.

Poverty rates, percentage points, head minus base:

Year BHC all BHC children BHC working age BHC pensioners AHC all AHC children AHC working age AHC pensioners
2025-26 – – – – – – – –
2026-27 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000
2027-28 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000
2028-29 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000
2029-30 – – – – – – – –
2030-31 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000 +0.000

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:

Decile Change (£m) Mean change (£ per household) Households changed
1 +0.5 +0.14 6.99k (55 records)
2 +0.5 +0.15 32.25k (140 records)
3 +1.3 +0.43 93.44k (199 records)
4 +1.8 +0.61 132.82k (160 records)
5 +2.5 +0.85 43.75k (100 records)
6 +17.8 +5.39 105.66k (104 records)
7 +4.7 +1.54 44.97k (80 records)
8 +0.7 +0.24 6.82k (15 records)
9 +15.2 +5.19 127.66k (14 records)
10 +0.0 +0.00 0.30k (4 records)

Runtime and memory (one process per run, same dataset bytes):

Run Load (s) 2025 2026 2027 2028 2029 2030 Total (s) RSS after each year (GB) Peak RSS (GB) Branches left
base 7.9 2.1 1.9 2.1 2.1 2.0 2.2 24.3 2.804, 3.019, 3.181, 3.425, 3.708, 3.957 3.957 none
head 11.0 2.6 2.4 2.4 2.5 2.5 4.1 36.0 2.99, 3.224, 3.624, 3.911, 3.957, 4.306 4.306 none
base_noma 9.1 2.3 2.1 2.7 2.3 2.5 2.3 29.2 2.687, 2.837, 3.061, 3.286, 2.439, 2.38 3.286 none
head_noma 6.5 1.5 1.7 1.5 1.5 1.6 1.5 18.6 2.96, 3.213, 3.478, 3.776, 3.92, 4.303 4.303 none

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:

  • Income tax falls £37.6m in 2025-26, £45.4m in 2026-27 and £68-75m a year from 2027-28. The Marriage Allowance costs £354m in 2026-27 against £309m on main, measured each way as income tax with no one claiming minus income tax as modelled.
  • More couples gain the reduction. 0.21m couples elect now that had no Marriage Allowance on main, which gave it only where the transferor had unused allowance; they pay £27.1m less.
  • Double counting goes: on main 0.29m people had Marriage Allowance on both partners.
  • Tax moves between partners. Electing spouses pay £43.4m more and gaining spouses £89.0m less in 2026-27, as s.55B(6) cuts the transferor's allowance.
  • UC changes for 11-18k benefit units (20-26 records), net −£0.2m to −£0.3m a year, through the tax on earnings that UC deducts, which the reduction lowers.
  • Runtime: the election's branches add about 0.3-1.9 s a year to a full run, and peak memory is 4.3 GB against 4.0 GB.

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:

    • residence (s. 55B(2)(c), s. 55C(1)(d), (2)-(4)): the model assumes UK residents;
    • death during the year (s. 55B(7));
    • backdating and continuity of elections (s. 55D);
    • marriages for part of the year;
    • spouses outside the benefit unit (s. 55C(1)(a) needs no cohabitation, but the absent spouse is rarely in the survey).
  • 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):

    • Starting rate for savings: the band is not reduced by non-savings taxable income below £17,570 (s. 12). In that case the model can elect where the law would not. Example: a transferor with £12,500 of earnings and £5,500 of interest, and a recipient on £30,000 (2026). The model elects, saving the couple £14. In law, the cut allowance shrinks the starting-rate band, the transferor pays £376 not £238, and electing would cost the couple £124. This is the largest inherited limit.
    • Allowance order: allowances go against savings before dividends rather than in the order that most reduces tax (s. 25(2)).
    • Dividend upper rate: its own threshold lags the basic rate limit before 2026-27. The rate test uses the basic rate limit.
    • 2015-16 basic rate limit is £32,000 in the parameter; Finance Act 2014 s. 2 set it at £31,785.

    Main has since fixed four limits this PR first listed: savings and dividends now band above property income; a Scottish taxpayer's personal savings allowance is set on UK bands; Gift Aid extends the rate limits instead of coming off income (Give Gift Aid relief by raising the rate limits, not by deducting the donation #2271); and the 2016-17 allowance is £11,000, so that year's transferable amount is £1,100. The remaining ones 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_credits lumps it with other reductions. Total liability is unaffected; only the split of the reduction is.

  • SPI dataset: policyengine-uk-data's SPI build supplies marriage_allowance for MAIND == 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 76cfc9e79 conflicted only in the import block of reforms/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).

10e6b338c makes the rate test read non_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)), as earned_income_tax does. The election's branches also hold net_income, so they don't rebuild it from the held adjusted net income. Main's uc_income_tax_on_earnings now lives in utils/uc_income_tax.py, and it already subtracts every Step 6 reduction in income_tax_subtractions from 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:

  • YAML at 829fccae4 (the merge, before the fix): 530 passed. The new rate-test cases fail there (4 failed, 3 passed) and pass at 10e6b338c.
  • Marriage Allowance YAML at 10e6b338c: 64 passed.
  • Fail on main (76cfc9e79, only test files copied): the Marriage Allowance YAML gives 10 failed and 4 passed; the de-xfailed test_state_pension_does_not_change_partners_marriage_allowance fails (UC earned income £18,033.60 against £18,171.60 in law).
  • The independent review re-ran the five changed YAML files at the head (97 passed) and the head's YAML on the base (18 failed, 23 passed). It also checked query-order equality and baseline isolation under a £15,000 personal allowance reform.

Overlaps

Axiom

axiom: needed

Previously recorded queue claim (pending parity debt; dispatch readiness and encoded parity remain unverified): TheAxiomFoundation/rulespec-uk#362 queued (s.55C; extended on 2026-09-30 for this PR with s.55C(1)(ca) and s.55B(2)(ba) and seven companion cases) + the s.55B re-encode queued in rulespec-uk bulk/worklist.yaml (batch PE-UK-CHILD-DEFS), which 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

  • Two earlier GPT-6.1 Sol reviews (of dafe6301c on 12 Oct, then of 99182e396) were cut off before they reported. The first found the property-route interaction above and six smaller points, all addressed.
  • GPT-6.1 Sol reviewed 10e6b338c on 21 Oct: REQUEST_CHANGES on one P2, a test-fixture defect, not a model defect. The property fixture left is_claimant_or_partner to inference, which reads a married couple aged 18 and 100 as parent and child. d75ab7ac3 sets 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. A delta review of d75ab7ac3 follows.

🤖 Generated with Claude Code

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_order

22 October continuation

The continuation at 00bf62a81008d3439ae8a43e1c4cf32412ab4ff8 preserves 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_trim skip; 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 00bf62a81008d3439ae8a43e1c4cf32412ab4ff8 completed 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 c4cfc9489becb74c616e1f988fef4377793a8b46 adds 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_fields
  • policyengine_uk/tests/code_health/test_ci_test_shards.py::test_configured_source_and_wheel_shards_account_for_every_node

The 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 00bf62a8 remains 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.

MaxGhenis and others added 7 commits September 30, 2026 16:25
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>
MaxGhenis added a commit to PolicyEngine/policyengine-uk-data that referenced this pull request Oct 1, 2026
…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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

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 policyengine_uk/tests/test_uc_state_pension_properties.py (#MA_ISSUE → #1947 in the docstring and in the strict xfail's reason) and one sentence in the UC docs page. No formula changed. A rebase onto 514132f should conflict only where this PR removes that xfail marker. Further review fixes to #1949, if any, will also arrive as added commits.

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

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.

MaxGhenis and others added 2 commits October 3, 2026 17:21
…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>
MaxGhenis and others added 8 commits October 11, 2026 12:32
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>
MaxGhenis and others added 3 commits October 11, 2026 16:18
The two take-up cases both had the gaining spouse as the elder, so they
could not tell the gaining spouse's draw from the elder spouse's. Add the
same cases with the electing spouse older. A mutant that reads the elder
spouse's draw survived the YAML before and fails both now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-core 3.33.0 makes an input set on a branch invalidate the
values derived from it. The election's branches set the election and the
allowance cut, so under 3.33 they dropped the property allowance route
they had inherited and chose one of their own for their counterfactual.
CI (core 3.33.2) caught it in test_no_one_lowers_their_tax_by_switching_
route_alone: a partner with receipts within the allowance was taxed as
electing on actual expenses (269.79), where the couple would not elect.

Pass the settled route to both branches as an input. Under 3.32.27 the
branches already inherited it, so nothing changes there; under 3.33.2 the
failing example now gives the same taxes as under 3.32.27. A 2027 YAML
case pins it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cleanup test's couple never elected (electing cost exactly what it
saved) and its landlord had no receipts, so neither branch ran. Use a
couple who elect every year, with receipts within the property allowance
and finance costs. Give generated adults gross receipts too, so the
property route is a choice weighed in branches in most examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 5 commits October 11, 2026 22:12
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
policyengine-core 3.33 (#560) makes an input set on a branch drop what the
branch holds that may have been calculated from the value it replaces. The
election's branches set the election itself, which the simulation may first
have calculated inside branches of its own: the property allowance route's,
a CPS marriage-neutral reform's split of adjusted net income, or a labour
supply response's measurement. Everything stored after that is dropped in
the election's branches and calculated again there, with the election held
off, so each of those choices could be made afresh for the counterfactual.
0c9c4d6 held the route; this holds the rest of what the formula resolves
before branching (now including employment income) at the simulation's
values. Under 3.32.27, which the lockfile pins, branches already inherit
these values, so nothing changes there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Marriage Allowance received is a Step 6 tax reduction (ITA 2007 s.55A(2),
s.55B(1)), so it leaves the non-savings band base at 30,000 - 12,570 =
17,430; the case pinned the old 16,170 until this fix.

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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

The last description update dropped this PR's Reproducer: line, so the Fix evidence check failed. I've added it back. Please keep it in the description file you edit from, or narrow it to the test that reproduces the bug:

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_order`

🤖 Generated with Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Coordination note: #2545 (fixing #2544, where Step 6 reductions offset the Step 7 charges) changes the lists this PR reads. Whichever lands second rebases.

  • gov.hmrc.income_tax.income_tax_subtractions becomes step_6_tax_reductions, with the same contents. income_tax_additions is split into step_5_tax_components and step_7_additional_tax.
  • New income_tax_reductions = min(Step 6 list, Step 5 tax); income_tax = max(0, income_tax_pre_charges − income_tax_reductions) + income_tax_charges.
  • capped_mcad is capped at max(0, income_tax_pre_charges − other_tax_credits).

If #2545 lands first, this PR's rebase:

  1. adds marriage_allowance_tax_reduction to step_6_tax_reductions.yaml; income_tax_subtractions.yaml is deleted there, so a modify/delete conflict is expected;
  2. changes income_tax.income_tax_subtractions to income_tax.step_6_tax_reductions in marriage_allowance_tax_reduction_limit.py and property_finance_cost_relief.py.

If this PR lands first, #2545 carries marriage_allowance_tax_reduction into the new list.

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>
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: FAIL

The report did not run, so this change has no report. See the workflow run.

…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.
@juaristi22

Copy link
Copy Markdown
Collaborator

At 00bf62a8, the continuation preserves the original branch history, clears inherited final income tax only in private election branches, declares the evidenced fiscal read, promotes pension division operands to float64, and isolates the Pension Credit no-Marriage-Allowance fixture. The original one-election and income-tax-only modelling choices remain.

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.

@juaristi22

Copy link
Copy Markdown
Collaborator

Published c4cfc948 with privacy-safe accounting diagnostics and eight-way source/wheel test sharding. All 123 selected Python checks and the strict selected-outcome checker passed; independent engineering review is CLEAN. The preceding certified accounting failure remains under investigation: this update preserves its invariant and keeps the PR draft pending actual current-head CI.

This branch has not been deployed

No deployments
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.

Marriage Allowance is booked on the recipient as the transferor's unused allowance, not the fixed s.55B transfer

2 participants