Skip to content

Carry unused Income Tax trading losses forward once - #2661

Open
juaristi22 wants to merge 10 commits into
mainfrom
fix-income-tax-loss-carry-forward-2087
Open

juaristi22 wants to merge 10 commits into
mainfrom
fix-income-tax-loss-carry-forward-2087

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses #2087
axiom: needed

Track unused Income Tax trading losses as an opening stock and consume each loss once. Only losses explicitly supplied for their tax year enter the current-year claim. Carry forward the remainder after actual general-income and trading-profit deductions, account for ordinary net-pay pension relief, and preserve explicit opening-balance overrides, including zero. Monthly reads retain the annual stock. Class 4 keeps its separate balance.

Carry-forward eligibility uses the shared trading allowance election. Full relief makes a qualifying trade's loss nil even when the allowance deduction arises only on miscellaneous income (ITTOIA 2005 s783AF). The £400 receipts / £200 loss / £600 miscellaneous-income example carries £0 into 2026. Under ordinary treatment, an £800 loss with £600 general-income relief retains £200.

General-income relief returns zero before resolving the allowance election when no new loss was supplied or every supplied raw loss clamps to zero. If the election suppresses every current loss, it returns zero before resolving net income, net-pay pensions and the cap. Mixed and positive effective losses retain the existing vector calculation and limits; earlier positive losses continue through the carry-forward recurrence. Explicit relief inputs retain their override behavior.

UK cleanup delegates to Core's public array-cleanup API, preserving supplied and model-owned input arrays by period and branch while clearing derived periods. The existing reset_calculations method uses the same helper. Its None return, changed_from_baseline flag and input-name selectors are preserved. This cleanup can recompute more values after input changes and resets.

For example, with a supplied £7,000 opening balance, a warmed next-year balance must refresh when a public input change replaces a zero current loss with £50,000. After £40,000 actual general-income relief, the next opening is £17,000. The regression also checks branches, reforms and explicit overrides without inserting a manual reset.

Integrates official main through 52b30809. The shared supplied-input helper retains the official implementation plus the loss_relief registration required by opening-stock overrides.

The model retains its current-year section 64 claim convention and requires callers to assess same-trade eligibility. Aggregate inputs cannot reconstruct unrelated trades. Previous-year elections, historical, terminal and foreign loss rules, and full pension claim optimisation remain outside scope. Explicit zero resets and later reseeding reconcile external history. Copying a loss input into another year supplies a new loss. Public-fixture validation makes no population or private H5/dataset compatibility claim. This PR does not claim full closure of #2087. See docs/engineering/income-tax-trading-losses.md for the contract and limitations.

Validation: 221 distinct public-fixture cases pass on Python 3.13.15, locked Core 3.34.1 and PolicyEngine UK 2.172.0: 150 Python and 71 native-Core YAML cases. These include loss/carry/net-pay controls, all three cleanup routes, the warmed £17,000 stock mutation, state-pension and positive-loss horizon constructors, cache callers and stored-household-total controls. The positive horizon fixture verifies £2,000 supplied, effective and general-income relief for each of five people in 2024, with opening stock zero by 2032. Formatting leaves all files unchanged; Ruff lint, frozen lock consistency and staged/unstaged whitespace checks pass. Required hosted checks for the updated head remain separate from local focused validation.

Companion Axiom parity remains needed. rulespec-uk #429 tracks the related loss-pool, carry-forward, reset and net-pay companions, with #348 as related context. Their queue labels do not establish encoded parity.

Reproducer: policyengine_uk/tests/test_trade_loss_relief_short_circuits.py::test_zero_and_positive_relief_refresh_after_public_changes; policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/reliefs/trade_loss_carry_forward.yaml::Shared trading allowance carry-forward under full relief on miscellaneous income; policyengine_uk/tests/test_projection_horizon.py::test_positive_effective_losses_preserve_the_projection_horizon

@juaristi22 juaristi22 self-assigned this Oct 25, 2026
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Aggregate impact on certified data: pass

Base 52b30809ff against head 4659784299, both on enhanced_frs_2024_25.h5 (policyengine-uk-data 1.58.1, sha256 056e80fd8e91), fiscal years 2025-26 to 2031-32. Each year is a full microsimulation run on each side.

Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in report_spec.yaml; a flag does not fail the job).

Workflow run and full report

Notes

  • In the years a figure from fewer than 10 records is suppressed (spending.disabled_students_allowance), every total containing it, and the decile incomes, are rebuilt from their other components, so no published total includes it. Those totals are marked †. A change is withheld where the two sides leave out different figures and can't be lined up.

Moves flagged for review

None.

Spending by benefit and service

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Caseloads

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed

Standard reform costings (change in gov_balance)

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10% 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp 0 0 0 0 0 suppressed suppressed

The report suppresses a reform's costing and target change in these years, for the reasons below (privacy.py):

  • Raise Class 1 main employee NICs rate to 10%, 2030-31 to 2031-32 (base and head): the reform changes income_tax in fewer than 10 records.
  • Raise additional rate by 3pp, 2030-31 to 2031-32 (base and head): the reform changes universal_credit in fewer than 10 records.

Standard reforms: change in each reform's target

Measure 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise Class 1 main employee NICs rate to 10%: change in national_insurance 0 0 0 0 0 suppressed suppressed
Raise additional rate by 3pp: change in income_tax 0 0 0 0 0 suppressed suppressed

Household net income by decile

No household's net income changes in any year.

No change in any year: receipts by tax head; government balance, each component counted once; model totals; household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles.

Checks failing on both base and head (1)
  • VAT excluding private school fees is never negative (sign:vat): 2025-26: 76 units below zero, a weighted £-53,888,684 in total; 2026-27: 76 units below zero, a weighted £-55,777,260 in total; 2027-28: 76 units below zero, a weighted £-57,727,917 in total
Counted-once check

Each of 92 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.

  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; gov_spending; not a failure)
  • widowed_parents_allowance: pension_credit responds only through other heads in fewer than 10 households (other heads that moved there: carers_allowance; household_benefits; not a failure)
Provenance
Side Commit Runtime Environment hash Partition hash Time
base main 52b30809ff uk 2.172.0 (files f8400aea019e), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2074.99s
head #2661 merged into main 4659784299 uk 2.172.0 (files 218ea913da65), core 3.34.1, py 3.13.18 aarch64 18199314be15 7cb61324a46b 2011.06s

Dataset: hf://policyengine/policyengine-uk-data-private/enhanced_frs_2024_25.h5@2526edbbb687a3b54dbb11272e5389e4b62d2ac2, sha256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 (verified on both sides), policyengine-uk-data 1.58.1.

Receipts by tax head: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Final charge on cash interest in non-cash ISAs £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Lifetime ISA withdrawal charge £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Income tax £294.46bn £315.23bn £335.82bn £351.28bn £366.96bn £385.36bn £400.00bn
National Insurance (employee and self-employed) £51.70bn £54.14bn £56.13bn £57.73bn £59.76bn £61.64bn £63.51bn
National Insurance (employer) £148.71bn £155.37bn £160.88bn £165.09bn £172.06bn £177.36bn £182.57bn
Employer NI change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Employer NI change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Capital gains tax £12.60bn £13.15bn £13.60bn £14.11bn £14.67bn £15.24bn £15.80bn
Student loan repayments £7.82bn £8.13bn £8.39bn £8.58bn £8.79bn £9.04bn £9.29bn
VAT excluding private school fees £249.37bn £256.87bn £266.33bn £272.70bn £278.97bn £285.24bn £291.59bn
Change in VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
VAT on private school fees £1.95bn £2.01bn £2.09bn £2.14bn £2.20bn £2.26bn £2.31bn
Change in private school VAT from its baseline £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Fuel duties £24.71bn £24.13bn £26.00bn £26.83bn £26.51bn £25.78bn £24.63bn
Alcohol duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tobacco duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Vehicle excise duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Betting and gaming duties £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
TV licence fee £4.26bn £4.40bn £4.41bn £4.42bn £4.43bn £4.44bn £4.45bn
Council tax £49.21bn £52.16bn £54.89bn £57.95bn £61.16bn £64.47bn £67.91bn
High value council tax surcharge £0.00bn £0.00bn £0.00bn £0.48bn £0.49bn £0.50bn £0.51bn
Northern Ireland domestic rates £0.55bn £0.58bn £0.60bn £0.63bn £0.66bn £0.69bn £0.72bn
Business rates £34.68bn £36.20bn £38.00bn £39.46bn £42.27bn £43.25bn £44.29bn
Stamp duty land tax on the household's own purchases £6.88bn £7.32bn £7.66bn £8.06bn £8.47bn £8.91bn £9.35bn
Stamp duty land tax paid by companies £4.72bn £4.57bn £5.29bn £6.03bn £6.37bn £6.92bn £7.45bn
Land transaction tax (Wales) £0.07bn £0.08bn £0.08bn £0.09bn £0.10bn £0.10bn £0.11bn
Land and buildings transaction tax (Scotland) £0.31bn £0.32bn £0.33bn £0.35bn £0.36bn £0.38bn £0.40bn
Unspecified tax change borne by capital £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Unspecified tax change borne by consumers £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Wealth tax excluding the main residence £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Land value tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Carbon tax £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Spending by benefit and service: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
State Pension £121.68bn £125.92bn £126.72bn £129.49bn £132.99bn £136.66bn £140.42bn
Pension Credit £6.66bn £7.03bn £7.29bn £7.48bn £7.60bn £7.84bn £8.03bn
Winter Fuel Payment £1.86bn £1.85bn £1.79bn £1.77bn £1.78bn £1.78bn £1.78bn
Pension Age Winter Heating Payment (Scotland) £0.19bn £0.20bn £0.20bn £0.21bn £0.21bn £0.22bn £0.22bn
Universal Credit £75.48bn £80.52bn £82.95bn £84.96bn £86.11bn £87.61bn £88.21bn
Housing Benefit £8.56bn £8.89bn £9.23bn £9.46bn £9.69bn £9.99bn £10.27bn
Council Tax Reduction £2.07bn £2.20bn £2.31bn £2.45bn £2.58bn £2.72bn £2.86bn
Employment and Support Allowance (income-related) £0.79bn £0.82bn £0.84bn £0.86bn £0.88bn £0.90bn £0.92bn
Employment and Support Allowance (contribution-based) £5.20bn £5.37bn £5.56bn £5.69bn £5.82bn £5.95bn £6.09bn
Jobseeker's Allowance (income-based) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Jobseeker's Allowance (contribution-based) £0.29bn £0.30bn £0.31bn £0.31bn £0.32bn £0.33bn £0.34bn
Income Support £0.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Working Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Tax Credit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Incapacity Benefit £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Personal Independence Payment £26.95bn £28.03bn £29.05bn £30.09bn £30.75bn £31.51bn £32.21bn
Disability Living Allowance £7.49bn £7.79bn £8.07bn £8.36bn £8.55bn £8.76bn £8.95bn
Attendance Allowance £8.68bn £9.03bn £9.36bn £9.69bn £9.90bn £10.15bn £10.38bn
Carer's Allowance £4.48bn £4.66bn £4.83bn £5.00bn £5.11bn £5.27bn £5.42bn
Carer Support Payment (Scotland) £0.31bn £0.32bn £0.33bn £0.34bn £0.35bn £0.36bn £0.37bn
Carer's Allowance Supplement (Scotland) £0.00bn £0.04bn £0.04bn £0.05bn £0.05bn £0.05bn £0.05bn
Severe Disablement Allowance £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn £0.03bn
Industrial Injuries Disablement Benefit £0.50bn £0.52bn £0.54bn £0.55bn £0.56bn £0.58bn £0.59bn
Armed Forces Compensation Scheme £0.61bn £0.63bn £0.65bn £0.66bn £0.68bn £0.69bn £0.71bn
War Widow's or Widower's Pension £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Child Benefit £16.19bn £16.82bn £17.39bn £18.00bn £18.39bn £18.84bn £19.25bn
Scottish Child Payment £0.49bn £0.52bn £0.53bn £0.55bn £0.56bn £0.57bn £0.56bn
Two Child Limit Payment (Scotland, counterfactual) £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Healthy Start vouchers £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn £0.05bn
Maternity Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Maternity Pay £3.30bn £3.41bn £3.54bn £3.62bn £3.70bn £3.78bn £3.87bn
Statutory Paternity Pay £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Statutory Sick Pay £1.12bn £1.16bn £1.20bn £1.23bn £1.26bn £1.29bn £1.32bn
Sure Start Maternity Grant £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn £0.01bn
Bereavement Support Payment £0.67bn £0.69bn £0.71bn £0.73bn £0.75bn £0.76bn £0.78bn
Widowed Parent's Allowance £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Tax-Free Childcare £0.56bn £0.57bn £0.57bn £0.58bn £0.60bn £0.61bn £0.59bn
Extended childcare entitlement £2.99bn £3.50bn £3.59bn £3.71bn £3.78bn £3.86bn £3.93bn
Universal childcare entitlement £2.06bn £2.22bn £2.29bn £2.37bn £2.43bn £2.48bn £2.53bn
Targeted childcare entitlement £0.50bn £0.51bn £0.52bn £0.54bn £0.55bn £0.57bn £0.56bn
Childcare Offer for Wales £0.26bn £0.28bn £0.28bn £0.29bn £0.30bn £0.31bn £0.32bn
Care to Learn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Childcare Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Parents' Learning Allowance £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn £0.20bn
Adult Dependants' Grant £0.06bn £0.06bn £0.04bn £0.04bn £0.04bn £0.04bn £0.04bn
Travel Grant £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
NHS spending £203.89bn £204.30bn £204.56bn £204.91bn £205.34bn £205.85bn £206.35bn
State education spending £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn £0.02bn
Transport subsidy spending £25.05bn £25.10bn £25.14bn £25.18bn £25.23bn £25.30bn £25.36bn
Cost-of-living support payment £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Bills Rebate £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Household Energy Payment £0.00bn £2.11bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Energy Price Guarantee subsidy £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Disability basic income £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Other public spending budget change £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Caseloads: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Lifetime ISA bonus 0k 0k 0k 0k 0k 0k 0k
State Pension 11,264k 11,145k 10,819k 10,700k 10,723k 10,750k 10,776k
Pension Credit 1,486k 1,480k 1,472k 1,452k 1,450k 1,455k 1,464k
Winter Fuel Payment 7,844k 7,783k 7,473k 7,375k 7,391k 7,409k 7,427k
Pension Age Winter Heating Payment (Scotland) 803k 802k 800k 797k 799k 801k 802k
Universal Credit 6,342k 6,347k 6,336k 6,297k 6,292k 6,271k 6,223k
Housing Benefit 1,249k 1,245k 1,244k 1,227k 1,227k 1,229k 1,232k
Council Tax Reduction 3,176k 3,195k 3,200k 3,214k 3,224k 3,235k 3,242k
Employment and Support Allowance (income-related) 71k 71k 71k 71k 71k 71k 71k
Employment and Support Allowance (contribution-based) 719k 720k 721k 722k 724k 726k 728k
Jobseeker's Allowance (income-based) 0k 0k 0k 0k 0k 0k 0k
Jobseeker's Allowance (contribution-based) 67k 67k 67k 67k 67k 68k 68k
Income Support 25k 0k 0k 0k 0k 0k 0k
Working Tax Credit 0k 0k 0k 0k 0k 0k 0k
Child Tax Credit 0k 0k 0k 0k 0k 0k 0k
Incapacity Benefit 0k 0k 0k 0k 0k 0k 0k
Personal Independence Payment 3,567k 3,575k 3,579k 3,585k 3,593k 3,602k 3,611k
Disability Living Allowance 1,291k 1,294k 1,296k 1,298k 1,300k 1,304k 1,307k
Attendance Allowance 1,695k 1,698k 1,700k 1,703k 1,707k 1,711k 1,715k
Carer's Allowance 1,034k 1,037k 1,038k 1,040k 1,042k 1,060k 1,062k
Carer Support Payment (Scotland) 71k 71k 71k 72k 72k 72k 72k
Carer's Allowance Supplement (Scotland) 0k 71k 71k 72k 72k 72k 72k
Severe Disablement Allowance 4k 4k 4k 4k 4k 4k 4k
Industrial Injuries Disablement Benefit 137k 137k 137k 138k 138k 138k 139k
Armed Forces Compensation Scheme 93k 93k 94k 94k 94k 94k 94k
War Widow's or Widower's Pension 0k 0k 0k 0k 0k 0k 0k
Child Benefit 8,123k 8,120k 8,120k 8,130k 8,145k 8,161k 8,178k
Scottish Child Payment 351k 352k 352k 353k 350k 348k 337k
Two Child Limit Payment (Scotland, counterfactual) 0k 0k 0k 0k 0k 0k 0k
Healthy Start vouchers 200k 200k 200k 201k 201k 201k 202k
Maternity Allowance 0k 0k 0k 0k 0k 0k 0k
Statutory Maternity Pay 333k 333k 334k 334k 335k 336k 337k
Statutory Paternity Pay 0k 0k 0k 0k 0k 0k 0k
Statutory Sick Pay 181k 182k 182k 182k 182k 183k 183k
Sure Start Maternity Grant 26k 26k 26k 26k 26k 26k 26k
Bereavement Support Payment 98k 98k 98k 98k 98k 99k 99k
Widowed Parent's Allowance 0k 0k 0k 0k 0k 0k 0k
Tax-Free Childcare 1,043k 1,027k 1,014k 1,009k 1,011k 1,009k 993k
Extended childcare entitlement 795k 800k 796k 797k 798k 798k 792k
Universal childcare entitlement 1,071k 1,072k 1,072k 1,073k 1,076k 1,079k 1,074k
Targeted childcare entitlement 165k 156k 154k 154k 155k 155k 150k
Childcare Offer for Wales 39k 39k 39k 39k 39k 39k 39k
Care to Learn 0k 0k 0k 0k 0k 0k 0k
Childcare Grant 0k 0k 0k 0k 0k 0k 0k
Parents' Learning Allowance 98k 98k 98k 99k 99k 99k 99k
Adult Dependants' Grant 18k 18k 12k 12k 12k 12k 12k
Travel Grant 0k 0k 0k 0k 0k 0k 0k
Disabled Students' Allowance suppressed suppressed suppressed suppressed suppressed suppressed suppressed
16 to 19 Bursary Fund 0k 0k 0k 0k 0k 0k 0k
Cost-of-living support payment 0k 0k 0k 0k 0k 0k 0k
Energy Bills Rebate 0k 0k 0k 0k 0k 0k 0k
Household Energy Payment 0k 21,065k 0k 0k 0k 0k 0k
Energy Price Guarantee subsidy 0k 0k 0k 0k 0k 0k 0k
Basic income 0k 0k 0k 0k 0k 0k 0k
Disability basic income 0k 0k 0k 0k 0k 0k 0k
Government balance, each component counted once: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Government receipts, each component once £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
Government spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
Government balance, each receipt once £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
Benefits in household_benefits but not in gov_spending £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn £0.00bn
Model totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
gov_tax £892.00bn £934.66bn £980.50bn £1,015.93bn £1,054.24bn £1,091.56bn £1,124.89bn
household_tax £491.97bn £520.41bn £551.21bn £576.01bn £601.00bn £626.70bn £648.42bn
pre_budget_change_household_tax £484.15bn £512.28bn £542.82bn £566.95bn £591.72bn £617.16bn £638.61bn
gov_spending £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
household_benefits £529.38bn † £545.66bn † £550.77bn † £559.51bn † £567.20bn † £575.95bn † £583.60bn †
gov_balance £362.62bn † £389.00bn † £429.74bn † £456.42bn † £487.04bn † £515.61bn † £541.29bn †
Household income totals: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Household market income £1,637.96bn £1,702.77bn £1,757.35bn £1,806.41bn £1,855.97bn £1,912.43bn £1,970.44bn
Household net income £1,636.69bn † £1,687.82bn † £1,715.45bn † £1,747.49bn † £1,778.69bn † £1,817.02bn † £1,859.69bn †
Household net income (HBAI definition) £1,449.57bn £1,499.83bn £1,530.86bn £1,565.17bn £1,597.89bn £1,635.60bn £1,676.61bn
Population: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
People 69,245k 69,383k 69,473k 69,592k 69,738k 69,912k 70,080k
Benefit units 35,110k 35,181k 35,226k 35,286k 35,360k 35,449k 35,534k
Households 31,046k 31,108k 31,149k 31,201k 31,267k 31,345k 31,420k
Poverty rates: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 16.3% 15.5% 16.4% 16.3% 16.5% 16.5% 16.6%
Absolute poverty, before housing costs: children 24.7% 22.1% 23.3% 22.9% 23.1% 23.2% 23.5%
Absolute poverty, before housing costs: working-age adults 14.1% 13.8% 14.7% 14.7% 14.8% 14.8% 15.0%
Absolute poverty, before housing costs: pensioners 13.7% 13.4% 14.0% 13.8% 13.9% 13.9% 13.9%
Absolute poverty, after housing costs: everyone 21.6% 21.0% 22.0% 22.6% 22.9% 23.1% 23.4%
Absolute poverty, after housing costs: children 33.3% 31.3% 32.3% 32.5% 32.9% 33.4% 34.2%
Absolute poverty, after housing costs: working-age adults 19.9% 19.8% 20.8% 21.6% 21.9% 22.0% 22.3%
Absolute poverty, after housing costs: pensioners 12.7% 12.4% 13.2% 13.2% 13.3% 13.4% 13.6%
Relative poverty, before housing costs: everyone 17.0% 16.0% 16.4% 16.3% 16.4% 16.3% 16.6%
Relative poverty, before housing costs: children 25.9% 22.9% 23.3% 22.9% 23.0% 22.9% 23.5%
Relative poverty, before housing costs: working-age adults 14.5% 14.1% 14.6% 14.7% 14.7% 14.7% 15.0%
Relative poverty, before housing costs: pensioners 14.5% 14.0% 13.9% 13.8% 13.8% 13.9% 13.9%
Relative poverty, after housing costs: everyone 21.4% 21.0% 21.3% 21.3% 21.3% 21.5% 21.7%
Relative poverty, after housing costs: children 33.1% 31.1% 31.2% 31.0% 31.1% 31.2% 31.8%
Relative poverty, after housing costs: working-age adults 19.9% 19.8% 20.2% 20.4% 20.5% 20.6% 20.8%
Relative poverty, after housing costs: pensioners 12.5% 12.4% 12.2% 11.7% 11.7% 11.8% 12.0%
People in poverty: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Absolute poverty, before housing costs: everyone 11,278k 10,779k 11,402k 11,365k 11,476k 11,513k 11,651k
Absolute poverty, before housing costs: children 3,688k 3,314k 3,496k 3,445k 3,480k 3,502k 3,549k
Absolute poverty, before housing costs: working-age adults 5,965k 5,893k 6,308k 6,362k 6,420k 6,430k 6,521k
Absolute poverty, before housing costs: pensioners 1,624k 1,572k 1,598k 1,558k 1,576k 1,580k 1,581k
Absolute poverty, after housing costs: everyone 14,944k 14,600k 15,310k 15,721k 15,937k 16,141k 16,426k
Absolute poverty, after housing costs: children 4,975k 4,682k 4,837k 4,888k 4,945k 5,040k 5,179k
Absolute poverty, after housing costs: working-age adults 8,463k 8,461k 8,971k 9,344k 9,491k 9,584k 9,706k
Absolute poverty, after housing costs: pensioners 1,506k 1,457k 1,502k 1,490k 1,501k 1,517k 1,542k
Relative poverty, before housing costs: everyone 11,750k 11,084k 11,388k 11,364k 11,416k 11,415k 11,651k
Relative poverty, before housing costs: children 3,872k 3,423k 3,496k 3,445k 3,466k 3,454k 3,549k
Relative poverty, before housing costs: working-age adults 6,159k 6,013k 6,301k 6,362k 6,389k 6,389k 6,521k
Relative poverty, before housing costs: pensioners 1,720k 1,648k 1,590k 1,557k 1,562k 1,572k 1,581k
Relative poverty, after housing costs: everyone 14,849k 14,552k 14,776k 14,809k 14,874k 15,014k 15,234k
Relative poverty, after housing costs: children 4,942k 4,660k 4,673k 4,663k 4,679k 4,707k 4,806k
Relative poverty, after housing costs: working-age adults 8,426k 8,440k 8,711k 8,823k 8,873k 8,967k 9,062k
Relative poverty, after housing costs: pensioners 1,482k 1,452k 1,392k 1,323k 1,322k 1,339k 1,365k
Inequality: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Gini coefficient, equivalised HBAI income before housing costs 0.3080 0.3052 0.3058 0.3058 0.3063 0.3069 0.3085
Mean household net income in each side's own deciles: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Decile 1: mean household_net_income per household £17,681 † £17,997 † £17,915 † £18,152 † £18,307 † £18,531 † £18,717 †
Decile 2: mean household_net_income per household £27,939 † £29,023 † £28,711 † £29,013 † £29,276 † £29,691 † £30,150 †
Decile 3: mean household_net_income per household £34,320 † £35,485 † £36,073 † £36,611 † £37,197 † £37,737 † £38,187 †
Decile 4: mean household_net_income per household £38,933 † £40,687 † £41,723 † £42,727 † £43,334 † £44,132 † £44,432 †
Decile 5: mean household_net_income per household £43,239 † £45,567 † £46,014 † £46,959 † £47,712 † £48,584 † £50,282 †
Decile 6: mean household_net_income per household £47,927 † £49,210 † £50,892 † £51,939 † £52,945 † £53,942 † £54,680 †
Decile 7: mean household_net_income per household £55,662 † £56,571 † £57,442 † £57,670 † £58,729 † £59,657 † £61,082 †
Decile 8: mean household_net_income per household £68,789 † £70,803 † £71,858 † £73,466 † £73,880 † £75,320 † £77,178 †
Decile 9: mean household_net_income per household £78,890 † £81,013 † £80,861 † £82,646 † £85,464 † £86,946 † £89,509 †
Decile 10: mean household_net_income per household £117,005 † £120,339 † £124,742 † £126,632 † £125,971 † £128,600 † £130,938 †
Standard reform costings (change in gov_balance): head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp £6.41bn £6.66bn £6.63bn £6.85bn £7.03bn £7.25bn £7.49bn
Raise higher rate by 1pp £3.61bn £3.87bn £4.00bn £4.22bn £4.44bn £4.71bn £4.83bn
Raise personal allowance by ~800GBP/year £-3.80bn £-3.92bn £-3.98bn £-4.04bn £-4.10bn £-4.23bn £-1.95bn
Raise child benefit by 25GBP/week per additional child £-1.99bn £-1.75bn £-1.55bn £-1.38bn £-1.30bn £-1.18bn £-1.07bn
Reduce Universal Credit taper rate to 20% £-20.77bn £-21.32bn £-21.75bn £-21.99bn £-22.34bn £-22.75bn £-22.94bn
Raise Class 1 main employee NICs rate to 10% £10.89bn £11.30bn £11.71bn £12.01bn £12.31bn suppressed suppressed
Raise VAT standard rate by 2pp £24.89bn £25.71bn £26.63bn £27.24bn £27.87bn £28.50bn £29.14bn
Raise additional rate by 3pp £2.90bn £3.15bn £3.22bn £3.41bn £3.61bn suppressed suppressed
Raise the Universal Credit standard allowance by 10% £-3.06bn £-3.18bn £-3.26bn £-3.35bn £-3.44bn £-3.49bn £-3.50bn
Raise the State Pension by 10% £-7.78bn £-8.01bn £-8.24bn £-8.47bn £-8.82bn £-8.55bn £-8.89bn
Standard reforms: change in each reform's target: head levels
Measure (head) 2025-26 2026-27 2027-28 2028-29 2029-30 2030-31 2031-32
Raise basic rate by 1pp: change in income_tax £6.55bn £6.86bn £6.79bn £7.00bn £7.20bn £7.44bn £7.66bn
Raise higher rate by 1pp: change in income_tax £3.61bn £3.92bn £4.01bn £4.23bn £4.45bn £4.72bn £4.84bn
Raise personal allowance by ~800GBP/year: change in income_tax £-3.93bn £-4.07bn £-4.13bn £-4.18bn £-4.22bn £-4.40bn £-1.75bn
Raise child benefit by 25GBP/week per additional child: change in child_benefit £2.33bn £2.14bn £1.94bn £1.76bn £1.65bn £1.52bn £1.40bn
Reduce Universal Credit taper rate to 20%: change in universal_credit £20.74bn £21.31bn £21.78bn £22.04bn £22.37bn £22.78bn £22.99bn
Raise Class 1 main employee NICs rate to 10%: change in national_insurance £11.15bn £11.63bn £12.00bn £12.29bn £12.61bn suppressed suppressed
Raise VAT standard rate by 2pp: change in vat £24.69bn £25.51bn £26.42bn £27.03bn £27.65bn £28.28bn £28.91bn
Raise additional rate by 3pp: change in income_tax £2.90bn £3.15bn £3.22bn £3.42bn £3.62bn suppressed suppressed
Raise the Universal Credit standard allowance by 10%: change in universal_credit £3.06bn £3.19bn £3.28bn £3.37bn £3.43bn £3.49bn £3.51bn
Raise the State Pension by 10%: change in state_pension £10.68bn £11.17bn £11.28bn £11.60bn £12.03bn £12.47bn £12.97bn

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 1 at 3bc00db94.

Verdict: the law is right and "once" holds in every case I tried. Two shoulds before this leaves draft: one model choice in a year that has a new loss, and CI that has not finished at this head. No musts.

Law checked on legislation.gov.uk

  • ITA 2007 s.83: (1) and (2) carry forward only the part not yet relieved; (3) deduct only from profits of the trade; (4) deduct before any other relief from those profits. s.84 deducts year by year until the loss is used up. loss_relief.py:45-66 follows this: opening stock, less min_(opening, profits) absorbed, plus the new loss left after s.64.
  • s.83 is not in the s.24A(6) list, and the PR does not cap loss_relief. The part of a s.64 claim that the cap blocks carries forward, which matches s.83(1)(b). The YAML "Cap-limited Income Tax loss and Class 4 loss have distinct balances" covers this.
  • s.64(2): current year only. The PR states this, and the previous-year option is still out of scope.
  • Net-pay ceiling (trade_loss_relief_against_general_income.py:46-50): taxable_employment_income is gross of employee contributions (taxable_employment_income.py:17-19 deducts only employment_expenses). So taking off net_pay_pension_relief before the s.25(4) limit is right under FA 2004 s.193(2), and does not double-deduct.

Findings

  1. should: in a year with a supplied loss, the brought-forward stock is set against other trades' profits. trading_loss.py:10 defines self_employment_income in such a year as "profits of their other trades", but loss_relief.py:49 absorbs the opening stock against it. s.83(3) allows a deduction "only from profits of the trade". My check: £10,000 loss in 2025 with no other income, then in 2026 a new £4,000 loss and £8,000 profit from another trade. The model gives 2026 taxable trading income £0, s.64 relief £0 and a 2027 opening stock of £6,000. If the stock came from the trade that lost again in 2026, the law gives s.64 relief of £4,000, taxable income of £4,000 and a stock of £10,000. The total relief is the same; the year and the trade are not. The docs (docs/engineering/income-tax-trading-losses.md:17-18) cover this with the "caller-preassessed pool" assumption. Either stop absorbing the stock in a year with a supplied current loss, or say this case plainly in the docs and add a YAML test that pins the chosen behaviour.
  2. should: CI at 3bc00db94 is red, but no test failed. Python 2/5, 4/5 and 5/5 failed and 3/5 was cancelled, each on "The runner has received a shutdown signal". The logs show 15.6 to 15.8 GB used and 125 to 311 MB free just before (for example, job 121292799781). Other PRs on the same workflow passed in the same window. I cannot say whether the branch caused this. Please re-run, and if it happens again, check whether the new Hypothesis tests raise runner memory. The PR body already says hosted CI at the new head is unverified.
  3. nit: losses supplied every year build up stock. extend_single_year_dataset copies every column into each later year (economic_assumptions.py:255-256), and simulation.py:1207 calls set_input for each one. A future dataset with a trading_loss column would then make each uprated year a new loss. My check: £5,000 loss and £2,000 pay every year gives an opening stock of £3,000, £6,000 and then £9,000. The current enhanced_frs_2024_25.h5 has no trading_loss or loss_relief column, so there is no population effect today, which fits the Aggregate impact check. The Class 4 fold from Charge Class 4 NICs on Chapter 2 profits, after capital allowances, the trading allowance and Schedule 2 losses #1919 has the same exposure, so this predates the PR. One line in the docs would do.
  4. nit: loss_relief.py:27 keeps quantity_type = FLOW on what is now an opening stock. It does no harm for a YEAR variable, but STOCK would match the new label.
  5. nit: the validation ledger in the PR body is hard to review. A short "what ran at this head" list would be enough.

"Once" checks (household, local)

  • 2025: £10,000 loss and £5,000 pay, then trade profits of £3,000 in 2026 and £3,000 in 2027. s.64 relief is £5,000 in 2025. The opening stock is £5,000, then £2,000, then £0. Taxable trading income in 2027 is £1,000. Relief adds up to the loss.
  • An explicit zero loss_relief in 2026 resets the stock: 2027 profits of £5,000 are fully taxed.
  • The PR's conservation property test and the YAML "New loss use does not add the same loss to an old balance" cover the same property.

What I ran: a fresh clone at 3bc00db94, a fresh uv venv, policyengine-core 3.33.12, Python 3.13.

  • PR YAML (trade_loss_carry_forward, trade_loss_net_pay_carry_forward, loss_relief): 16 passed.
  • PR Python (test_income_tax_loss_carry_forward.py, test_ni_class_4_profits_properties.py): 34 passed.
  • All 36 YAML files that touch trading_loss, loss_relief or employee_pension_contributions: 495 passed.
  • Related Python (trading loss, trading allowance, annual allowance, net pay, ANI pension relief, income concepts, uprating declarations): 54 passed.
  • The four household edge cases above. No microsimulation.

Not read or not run: the full Python test file line by line, the rest of extend_single_year_dataset, and the docs index wiring. No parameter values changed, so I did not run the reference check. I did not run the full test suite.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 2 at d3d2bbc55.

Status of round 1 (round 1). The new commit changes only the docs, one YAML case and the changelog fragment; the model code is the same as at 3bc00db94.

  1. should, resolved by documentation. The code still sets the opening stock against all trading profits in a year with a new loss (loss_relief.py:45-49). The commit takes the second route we offered: docs/engineering/income-tax-trading-losses.md:57-63 now works through both cases, says the old-loss-and-new-loss-in-the-same-trade case is outside what the model can rebuild, and tells callers to supply reconciled balances there. The new YAML case (trade_loss_carry_forward.yaml:251) pins the behaviour: £10,000 stock against £8,000 profit, £0 s.64 relief, £6,000 carried to 2027. I think that is acceptable. The model has no trade identity, so it cannot know which trade a stock belongs to. The case needs two trades with a profit and a loss in the same year, and the certified data has no trading_loss column, so there is no population effect today. One caveat for the docs, not a blocker: the total relief is only the same if trade A later makes enough profit. If A never does, the law never relieves that old stock, while the model has already used it against B.
  2. should, still open. CI at this head is red again, and again no test failed. Python 2/5, 3/5, 4/5 and 5/5 each ended with "The runner has received a shutdown signal", after memory reached 15.6 to 15.9 GB of 16 GB (49 MB free on shard 5 just before). Test / Result then fails because those shards did not finish. Details below.
  3. nit, fixed. income-tax-trading-losses.md:65-67 now warns that copying trading_loss or loss_relief into later years creates a new loss or a new override each year.
  4. nit, open. loss_relief.py:27 still has quantity_type = FLOW on an opening stock.
  5. nit, partly fixed. The PR body has a "Validation" list now, but it is still hard to follow (for example "nine scalar outputs are not nine tests"). A plain list of what ran at this head would do.

CI at d3d2bbc55 (run 40318044087)

Job Result Cause
Python 2/5 failed at 54 min runner shutdown at 83% of tests; 15.7 GB used shortly before
Python 3/5 failed at 44 min runner shutdown at 77%; 15.6 GB used
Python 4/5 failed at 58 min runner shutdown at 86%; 15.7 GB used
Python 5/5 failed at 68 min runner shutdown at 95%; 15.9 GB used, 49 MB free
Test / Result failed follows from the four shards above

None of these is a test failure or a timeout (limit 75 min). All tests in the logs up to the shutdowns passed, including the PR's Class 4 property tests.

Is it the branch? I cannot prove it, but the pattern points that way:

  • Every head of this branch has lost runners. At 4400b67e1, shard 1/5 had a real test failure (later fixed) and shards 2 to 5 were cancelled (I did not read their logs); at 3bc00db94 and now d3d2bbc55, four of five shards lost their runners each time.
  • In the same window (25 Oct 09:00 to 26 Oct 13:10) the workflow had 90 successful runs. I checked every other failed run: each was a real test failure (the test step ended in "failure", not "cancelled"), or, in one case, a run cancelled as a whole. None lost a runner.
  • The suite is close to the limit anyway. Passing runs today peaked at 11.4 to 15.3 GB per shard (Uprate reported Council Tax Reduction with household bills #2680, Extend fiscal parameter conversion beyond 2040 #2649's queue run, fix-uc-advance-cash-2410), and the workflow comment cites The Python test shards run out of memory under sys.monitoring coverage #2330 for worker memory growth.
  • What I could not find: a reason in the code. A household simulation computes the same 1,008 variables at this head and at the merge base, with the same traced peak memory. One run of three unrelated property test files in a single process peaked at 3.48 GB RSS at this head and 3.00 GB with the base model code. That is one run each, so treat it as a hint, not proof.

Suggestion: re-run the failed jobs once. If they lose their runners again, compare the memory lines in the logs with a main run, shard by shard, before marking ready. A green run is still needed before this leaves draft.

New finding

  1. nit: trade_loss_carry_forward.yaml:248 starts the new case with "# PROPOSED ONLY". It is not clear what that means in a baseline test. If it means the trade identities are assumptions of the test, the next two comment lines already say so.

What I ran, at d3d2bbc55 in a fresh worktree and venv (Python 3.13):

  • PR YAML (trade_loss_carry_forward, trade_loss_net_pay_carry_forward, loss_relief): 17 passed.
  • All YAML under income_tax/reliefs: 74 passed.
  • PR Python (test_income_tax_loss_carry_forward.py, test_ni_class_4_profits_properties.py): 34 passed, 0.88 GB peak RSS.
  • Memory checks above: test_marriage_allowance_cps_switch.py, test_uc_income_from_capital_properties.py and test_mixed_age_couple_properties.py (52 passed with both versions), and one household simulation for 2026, 2028 and 2030.
  • The CI logs of all four failed shards, the memory lines of 15 passing shards from three other runs, and the job results of every failed run of the workflow in the window.

Not read or not run: no microsimulation and no full suite run. I did not re-read the model code, which is unchanged since round 1, and I did not find the cause of the higher CI memory.

Verdict: should 1 is resolved by a clear, tested statement of the assumption. Should 2 still blocks: CI needs a green run. Still a draft, so this is a comment, not an approval.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 3 at 16a12b93d.

Status of round 2 (round 2). The new commit changes loss_relief to a stock, adds one test, drops the YAML label and extends the changelog fragment. The docs are unchanged.

  1. should, still open (docs only). The same-trade assumption is settled as round 2 said, but the caveat we asked for is not in yet. docs/engineering/income-tax-trading-losses.md:63 says the old stock is "unrelieved" in the A/A/B case. It does not say that, under s.83, that relief is lost for good if trade A never makes enough profit again, while the model has already used it against B's profit. One sentence at line 63 would close this.
  2. should, still open. CI at this head is red again (run 40393666784). Details and a correction below.
  3. nit, fixed (in round 2).
  4. nit, fixed. loss_relief.py:27 is now STOCK, and test_income_tax_loss_carry_forward.py:179 pins £8,000 for the year, January and December. That is the right behaviour for an opening balance. The PR body says January returned £666.67 before the change, so this was a real bug, not just a label.
  5. nit, still open. The PR body is now longer and harder to follow. The "Validation" list reads as a run log ("1/1/1 exits", "D3D", "artifact-unqualified"). A short list of what ran at this head and what passed would do.
  6. nit, fixed. trade_loss_carry_forward.yaml:248 no longer says "PROPOSED ONLY".

CI and memory

At this head, Python 2/5, 3/5 and 5/5 lost their runners ("The runner has received a shutdown signal"), 4/5 was cancelled (its log is no longer available), and 1/5 passed. Again, no test failed.

Shard This PR, peak Passing runs the same evening (pr-2679 queue, esa-permitted-work PR)
1/5 13,979 MB, passed 13,989 / 13,952 MB
2/5 15,874 MB, lost 15,008 / 14,304 MB
3/5 15,586 MB, lost 14,983 / 13,315 MB
4/5 log gone 12,495 / 12,713 MB
5/5 15,752 MB, lost 14,022 / 15,028 MB

A correction to round 2: I said no other run in the window had lost a runner. That no longer holds. The merge-queue run for #2105 (run 40395541241, 26 Oct 21:27) lost Python 2/5 at 15,501 MB. So the suite on main is now at the 16 GB limit too. This branch still loses more shards (three or four of five on each head, against one of five there), and its peaks are 0.6 to 0.9 GB above passing runs, but I can no longer say the problem is only this branch.

I also looked for a cause in the code and did not find one:

  • The certified dataset (1.58.1, enhanced_frs_2024_25.h5) has no trading_loss or loss_relief column, so supplied_input_periods finds no earlier years and the carry-forward loop in loss_relief.py:29-66 never runs in a microsimulation. It does not build simulations per year, and the per-year values it reads go into the ordinary cache, so it is linear in years, not recursive over all years.
  • The PR's own tests are small: test_income_tax_loss_carry_forward.py and test_ni_class_4_profits_properties.py together peaked at 0.91 GB RSS in one process.
  • Those tests run in every shard, including 1/5 (three loss and four Class 4 tests there), and shard 1/5 did not move. So "the PR's tests land in the full shards" does not explain it either.

My view: this looks like the general worker memory growth the workflow already notes (#2330, and the comment above the Python job), with this branch tipping it over. That is a CI capacity question rather than a defect in this PR. I would not ask for code changes for it. It still needs a green run before this leaves draft, and that may need a sixth shard or a fix on the CI side first.

New findings

None.

What I ran, at 16a12b93d in a fresh clone and venv (Python 3.13), without data:

  • test_income_tax_loss_carry_forward.py and test_ni_class_4_profits_properties.py: 35 passed, 0.91 GB peak RSS.
  • All YAML under income_tax/reliefs: 74 passed.
  • The column list of the certified dataset (names only).
  • The logs of shards 1, 2, 3 and 5 of run 40393666784, the shard peaks of two passing runs from the same evening, and the failed shard of the Uprate the Scottish Carer Supplement by September CPI with 5p rounding #2105 queue run.

Not run: no microsimulation and no full suite. I started a base-against-head memory run on test_dated_reform_routes.py but stopped it after 14 minutes on a shared machine, so I have no paired number this round.

Verdict: the code changes since round 2 are right and add no new findings. Two things remain: the one-sentence docs caveat (1), and a green CI run (2), which now looks like a CI capacity problem more than a PR problem. Still a draft, so this is a comment, not an approval.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 4 at bcf0fb7cd.

Status of round 3 (round 3). The only change since 16a12b93d is one sentence in the docs. No new comments from you since then, so I have read the commit and the PR body.

  1. should, fixed. docs/engineering/income-tax-trading-losses.md:63 now says that if A never earns enough future profits, the excess of its old loss stays unrelieved under ss.83 to 84, while the aggregate calculation has already used part of it against B's profit. That is the caveat we asked for. Thank you.
  2. must, open (was should). CI at this head is red again, and this time it is not the runners. Details below. We were wrong in round 3 to call this only a CI capacity issue: the runner shutdowns hid real test failures.
  3. nit, fixed (in round 2).
  4. nit, fixed (in round 3).
  5. nit, still open. The "Validation" list in the PR body still reads as a run log ("16A Code attempt 1", "COMPLETE / APPROVE; local verification is CLEAN"). Please also drop "COMPLETE / APPROVE": no GitHub reviewer has approved this PR, and readers may take it that way.
  6. nit, fixed (in round 3).

CI at bcf0fb7cd

Code run 40424136853 failed: Python shards 2, 3, 4 and 5 failed, shard 1, YAML, lint and docs passed. Aggregate impact passed. All four shards ran to the end (67 to 71 minutes) and each failed on RecursionError in Microsimulation tests. CI tests this head merged with current main (refs/pull/2661/merge, 8056c6d):

  • shard 2: test_projection_horizon.py::test_legacy_reported_capital_expires_only_after_the_horizon
  • shard 3: test_tax_credits_state_pension_step_one_properties.py::test_state_pension_change_preserves_claimant_scope_and_deductions, plus an error in test_projection_horizon.py::test_a_year_past_the_horizon_is_served_from_the_last_projected_year
  • shard 4: test_projection_horizon.py::test_tax_free_savings_formula_takes_over_after_the_horizon
  • shard 5: errors in test_projection_horizon.py::test_every_year_to_the_horizon_is_a_dataset_year and ::test_simulation_serves_projected_values_to_the_horizon

The printed stack is only the last frames, but in shards 2 and 4 it runs loss_relief 2025 → trade_loss_relief_against_general_income 2024 → net_income_before_trade_loss_relief 2024 → the full 2024 income and benefit chain, inside a pension credit branch. That cross-year link is new in this PR: on main loss_relief is a plain input with no formula. Here loss_relief.py:40-60 walks back over every earlier year with a supplied loss, calls loss_relief for that year (:45) and reads trade_loss_relief_against_general_income for it (:58-60), which pulls in that year's whole income chain. My guess is that the stack depth then grows with the number of years walked back, on top of an already deep pension credit branch, and the horizon tests project far enough out to pass the limit. I have not proved this. In shards 3 and 5 the visible frames are all UC and pension credit, so I cannot show the PR's variables there, but the failures are the same kind.

The same tests pass without this PR: the #2690 run 40422010249, on recent main plus the shard change, passed all shards. So this looks caused by the PR's carry-forward chain once the dataset has supplied losses in earlier years. Two things would help: reproduce one of the test_projection_horizon.py failures on the merge with main, and see whether the earlier-year relief can be computed without nesting a full prior-year calculation inside the current one (or cap the years it walks back).

At round 3 (16a12b93d, run 40393666784) shards 2, 3 and 5 hit runner shutdowns near 15.7 GB and shard 4 was cancelled, before the logs show these tests. The code is the same at both heads, so these failures were most likely there then too.

What I ran at bcf0fb7cd (fresh checkout and uv venv, Python 3.13):

  • pytest policyengine_uk/tests/test_income_tax_loss_carry_forward.py policyengine_uk/tests/test_ni_class_4_profits_properties.py: 35 passed.
  • policyengine-core test on loss_relief.yaml, trade_loss_carry_forward.yaml and trade_loss_net_pay_carry_forward.yaml: 17 passed.
  • I did not run the failing Microsimulation tests locally (no microsimulation on the shared laptop), so the cause above is from the CI logs, not a local reproduction.

Verdict: not ready. Item 2 is a must: the PR's tests pass, but the full suite fails on this head merged with main, with recursion through the new carry-forward chain. It is still a draft, so no approval.

🤖 Generated with Claude Code

MaxGhenis added a commit that referenced this pull request Nov 1, 2026
The registration sat where #2765 adds self_employment_income, so the two
conflicted. The set's order means nothing; this place merges cleanly
with #1969, #2661, #2765 and #2776.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22
juaristi22 marked this pull request as ready for review November 2, 2026 18:27
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator

Round 5 at f40ba32eb.

I checked the commits since round 4 (bcf0fb7cd) against ITA 2007 ss.64 and 83 to 84 and ITTOIA 2005 Part 6A, looked at how they interact with #2765 (now on main), and read the CI logs at this head. Status of round 4:

  1. should, fixed (in round 4).
  2. must (recursion in the full suite), fixed. trade_loss_relief_against_general_income.py:44-60 now returns early when no loss was supplied for the year, or when the allowance election removes every loss, before it reads that year's net income. That is the code change I can point to; I have not proved it is the only reason. The recursion failures are gone in CI at this head: every test_projection_horizon.py test passes in CI at this head, including the new positive-loss horizon test, and so does test_tax_credits_state_pension_step_one_properties.py. Test / Result: 11,711 passed, 1 failed (item 7).
  3. nit, fixed (in round 2).
  4. nit, fixed (in round 3).
  5. nit, fixed. The PR body no longer reads like a run log, and "COMPLETE / APPROVE" is gone. One small gap: the "221 cases pass" list does not include test_trading_allowance_claim_status_data_year.py, which fails at this head (item 7).
  6. nit, fixed (in round 3).

New finding

  1. must: a Add a trading allowance claim status, so allowance reforms reach claimants without turnover data #2765 test fails at this head because of this PR. The fix belongs in the test, not the model. policyengine_uk/tests/test_trading_allowance_claim_status_data_year.py:283 (test_a_raise_never_withdraws_a_situation_claimants_carried_loss) fails in Python 7/7: assert 3486.0 == 3086.0 ± 1. I reproduced it locally at f40ba32eb. At the merge base 52b30809f it passes. The test came in with Add a trading allowance claim status, so allowance reforms reach claimants without turnover data #2765 and reached this branch in the 05:48 merge, unchanged.
    • Case: £30,000 pay, a £5,000 trading loss in 2025, £5,000 profit in 2026, CLAIMANT, and the allowance raised from £1,000 to £3,000 in 2026.
    • On main, loss_relief is an input with default 0, so the 2025 loss never reaches 2026. The raise then cuts 2026 tax by £400 (£2,000 × 20%).
    • At this head, the 2025 loss gets no s.64 relief, because there is no 2025 income, so loss_relief in 2026 is £5,000. Under current law that stock takes all £5,000 of profit. With the raise, it takes the £3,000 left after the larger allowance. Either way taxable trading income is £0 and Income Tax is £3,486, which is (£30,000 − £12,570) × 20%. The raise's £2,000 now shows up a year later: loss_relief in 2027 is £0 under current law and £2,000 with the raise. uses_trading_allowance is still True in both, so the test's own point, that a carried loss does not enter the election, still holds.
    • The PR's result is right in law. Under partial relief, ITTOIA s.783AI(2) gives the trade's profit as receipts less the allowance, in place of expenses. ITA s.83(3) then deducts the loss brought forward from "profits of the trade" in the later year. Nothing in ss.783AF and 783AI to 783AK stops that deduction, and trading_allowance.py:262-269 on main already says "Prior s.83 losses remain separate and may follow partial relief". Under full relief (s.783AF(2)) the profit is nil, so nothing is absorbed and the stock carries on. I checked that below.
    • Fix: update the test to state the new behaviour. Assert that 2026 Income Tax is equal under current law and with the raise, and that loss_relief for 2027 is £2,000 higher with the raise. If you would rather keep the test about the 2026 election only, set "loss_relief": {2026: 0} in the situation instead. I prefer the first, because it pins how the allowance and the carried loss work together.

Checked and fine

  • s.83 "once", local household runs: a £10,000 loss in 2025 with £6,000 pay gives s.64 relief of £6,000 and a £4,000 stock. A 2026 year within full relief (£800 receipts) leaves the stock at £4,000. As a CLAIMANT in 2027 and 2028 (profit already after the £1,000 allowance), £3,000 of 2027 profit absorbs £3,000, and 2028 absorbs the last £1,000. Total relief is £6,000 + £3,000 + £1,000 = £10,000, the size of the loss.
  • uses_trading_allowance in place of trading_allowance_deduction > 0 (loss_relief.py:59, trade_loss_relief_against_general_income.py:54): this is right for full relief where the deduction falls only on miscellaneous income. Under s.783AF(2) a qualifying trade's loss is nil. The YAML case for £400 receipts and £600 miscellaneous income pins this.
  • No cycle with Add a trading allowance claim status, so allowance reforms reach claimants without turnover data #2765's election. The election reads loss_relief for the current year, and loss_relief reads only earlier years' uses_trading_allowance.
  • The s.64 and net-pay ordering is unchanged since round 1.

CI at f40ba32eb (run 41586248470, on the merge with main): two checks fail, Python 7/7 and Test / Result. Python 7/7 has one real test failure (item 7), with no runner loss or timeout, and the log shows about 11.9 GB of 16 GB in use near the end. Test / Result fails only because of that shard. Python 1/7 to 6/7, YAML, lint, docs, smoke imports, changelog and Aggregate impact (pass, no moves flagged) are all green. The first "Fix evidence" run was cancelled and the re-run passed, so it is not a failing check.

What I ran, at f40ba32eb in a fresh clone and uv venv (Python 3.13), with no data:

  • test_trading_allowance_claim_status_data_year.py: 17 passed, 1 failed (item 7). The failing test passes at 52b30809f.
  • test_income_tax_loss_carry_forward.py, test_trade_loss_relief_short_circuits.py and test_ni_class_4_profits_properties.py: 40 passed.
  • All YAML under income_tax/reliefs: 76 passed.
  • The household cases above. No microsimulation and no full suite.

Not read line by line: the cache-cleanup changes in simulation.py, utils/measurement_branches.py and utils/pensions.py, and test_scenario_modifier_inputs.py. CI passes everything outside item 7 at this head, which is my only evidence for them.

Verdict: one must left, the #2765 test in item 7. It is a test update, not a model change. Everything from earlier rounds is fixed.

🤖 Generated with Claude Code

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.

2 participants