Skip to content

Compute the State Pension triple lock from its statutory inputs - #1939

Merged
MaxGhenis merged 7 commits into
mainfrom
triple-lock-statutory-inputs
Oct 3, 2026
Merged

MaxGhenis merged 7 commits into
mainfrom
triple-lock-statutory-inputs

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The State Pension triple lock is now computed from the statutory inputs the uprating review uses, not from calendar-year OBR growth:

  • September CPI (ONS D7G7, 12-month rate);
  • May-July earnings growth (ONS KAC3, AWE total pay, whole economy), the figure each review used;
  • the 2.5% floor.

The April uprating in year Y uses year Y-1's inputs, each rounded to 0.1pp as published. add_triple_lock takes the highest of the included elements and the floor.

Why:

  1. The April 2027 baseline rise was 3.4% (OBR calendar-year 2026 earnings). The 15 September 2026 first estimate of May-July total pay growth is provisionally 3.9%, pending the October labour market release (20 October), the vintage used by the uprating review. With the CPI forecast currently below that, this provisionally gives a new State Pension of £250.71 a week for 2027-28, rather than about £249.50; the £250.71 and all dependent impact figures remain provisional.
  2. Macro scenarios could only move calendar-year growth. Now they move the statutory inputs in a documented way. They can also set the inputs directly, e.g. from a model of the monthly series: the triple lock ratchets on the reversals between September CPI and May-July earnings.
  3. Rules like the plan announced on 29 September 2026 had to be set as hand-coded flat rates in PolicyEngine/uk-triple-lock. Now they are a parameter reform.

Statutory inputs

New node gov.economic_assumptions.statutory_uprating_inputs. Each input is keyed to its observation month and has preserve_calendar_dates.

Input Outturn Forecast (March 2026 EFO) After the EFO
cpi_september ONS D7G7, Sep 2010 to Sep 2025. ONS does not amend published CPI. Calendar-year CPI plus a gap, so that the baseline equals the OBR's September CPI forecast (receipts Table 3.19, "CPI used to uprate thresholds") for Sep 2026-2029 and its Q3 CPI (economy Table 1.7) for 2030 Calendar-year CPI
awe_total_pay_may_july The figure each review used, from the Explanatory Memorandum to each Up-rating Order, May-Jul 2010 to 2025. Then the provisional 15 September 2026 first estimate for 2026 (3.9%), pending the October labour market release (20 October), the vintage used by the review. Calendar-year earnings plus a gap, so that the baseline equals the OBR's Q2 average earnings growth (economy Table 1.6), 2027-2030 Calendar-year earnings

Notes:

  • Vintage. The stored May-July 2026 value of 3.9% is the provisional 15 September 2026 first estimate and stays provisional until the October labour market release (20 October), the vintage used by the review. Reviews use the latest estimate before the autumn statement: the October labour market release's figure, unchanged in November in every year checked. That is not the September first print or the latest ONS vintage. May-July 2024 was 4.0% in September, 4.1% in October (used) and is 4.4% now; May-July 2025 was 4.7% in September and 4.8% in October (used). Driven by the September prints, the rule would miss April 2020, 2025 and 2026; by the latest vintage, it would miss April 2016, 2019, 2020, 2025 and 2026. DWP's own record of the figures used (Abstract of DWP benefit rate statistics 2025, table 5) agrees in every year that set a rate.
  • The OBR does not forecast the ONS AWE series. Its Table 1.6 "average weekly earnings growth" is wages and salaries per employee (national accounts), and Q2 is the quarter nearest May-July. In six EFOs (November 2023 to March 2026), every OBR triple-lock forecast (annex Table A.3) equals the highest of Q2 Table 1.6 earnings growth, Q3 CPI and 2.5%. Only six of those 30 cells distinguish Q2 from calendar-year earnings, and CPI never binds in any of them.
  • Forecasts are stored as gaps (forecast_gap/), measured from the calendar-year growth stored in yoy_growth.yaml. A scenario that edits yoy_growth.obr.consumer_price_index or .average_earnings therefore moves the inputs one for one. A value set directly on an input replaces the forecast for that year only; the null after the outturn makes Parameter.update's restore entry a null, so nothing leaks into later years.
  • Importer. import_obr_forecasts.py regenerates the gaps from the same EFO (--receipts-file/--receipts-url for September CPI; --gaps-only after editing yoy_growth.yaml by hand). It renders and checks every file before writing any.

Rule

  • Differential check. The rule reproduces every published uprating from April 2012 to April 2026, and, rounded to the nearest 5p, every published basic (2011-2026) and new (2017-2026) State Pension weekly rate.
  • Weekly amounts. Modelled weekly rates are not rounded to the nearest 5p; the historical differential tests apply that rounding when comparing uprated amounts with published rates.
  • Overrides. The only override left in outturn.yaml is April 2011, when the basic State Pension rose by September 2010 RPI (4.6%) during the switch to CPI (EM to SI 2011/821, para 7.4). April 2022 is include_earnings false, citing the Social Security (Up-rating of Benefits) Act 2021 s1, and needs no override. The include flags now start in 2011, when the triple guarantee first applied.
  • Horizon. Rates run from April 2011 to one year past the end of the economic-assumption series (2074), instead of stopping at 2034.
  • active had no effect before. False now means no triple lock: the statutory minimum from the review under SSAA 1992 s150A as it stands, i.e. earnings growth, and nothing when earnings fall. The floor, the include flags and the one-year 2020 and 2021 Act modifications then do not apply.
  • New earnings_path_guarantee (off under current law). Each year it applies, the pension also rises enough to stay on an earnings path started from its level in the year before the guarantee first applies, rounded up to 0.1pp. With include_earnings false from 2030, that is one reading of the September 2026 plan; the speech gave no formula. It is the same rule as burnham_2030 in uk-triple-lock's rules.py.
  • Removed policyengine_uk/utils/create_triple_lock.py, a stale script that printed a calendar-year triple lock on import. Nothing imported it.
  • How to change these. Levers and inputs take effect through Scenario(parameter_changes=...), which rebuilds the rates. A reform= dictionary edits parameters after processing, so it changes the parameter but not the rates. That is pre-existing and verified; the docs and the lever metadata now say so.

Baseline before and after

The table records historical runs before the merge of current main, using the Enhanced FRS 2024-25 (sha256 e433e532): the then-current origin/main (44240bd) and this branch (f5629fd). These aggregate results describe those original commits and are not validation of the current merged branch; no new microsimulation was run for this review follow-up. Every figure below came from those runs; nothing is scaled or interpolated. "Net fiscal cost" is the fall in gov_balance. The scripts and outputs are on the author's machine and are not available in this repository.

Provisional figures: the April 2027 uprating and all 2027-28 onward after/change/net-cost pension levels and aggregate impacts depend on the 15 September 2026 first estimate of May-July earnings (3.9%), including £250.71 a week and the £0.47bn net fiscal cost in 2027-28; those figures remain provisional until the October labour market release (20 October), the vintage used by the uprating review, and the qualification applies to the 2027 uprating and the subsequent level and impact comparisons below.

Fiscal year April rise before after New SP £/wk before after Basic SP £/wk before after State Pension £bn before after change Net fiscal cost £bn
2026-27 4.8% 4.8% 241.30 241.30 184.90 184.90 133.7 133.7 +0.00 0.00
2027-28 3.4% 3.9% 249.50 250.71 191.19 192.11 139.8 140.5 +0.68 0.47
2028-29 2.5% 2.5% 255.74 256.98 195.97 196.91 144.8 145.5 +0.70 0.20
2029-30 2.5% 2.5% 262.14 263.40 200.87 201.84 150.1 150.8 +0.73 0.51
2030-31 2.5% 2.5% 268.69 269.99 205.89 206.88 155.7 156.5 +0.75 0.52
2031-32 2.5% 2.5% 275.40 276.74 211.03 212.06 161.3 162.0 +0.78 0.53
2032-33 3.3% 3.3% 284.49 285.87 218.00 219.05 170.0 170.8 +0.82 0.57
2033-34 3.7% 3.7% 295.02 296.45 226.06 227.16 180.8 181.6 +0.88 0.61
2034-35 3.7% 3.7% 305.94 307.42 234.43 235.56 188.2 189.1 +0.91 0.63

Invariants and tests

Stated invariants, each with tests:

  • Triple lock. The rate is at least each included element and the floor, and equals one of them. It never falls when an element rises. With the triple lock off, it is max(earnings, 0). (Hypothesis)

  • Earnings-path guarantee.

    • The level is never below the earnings path.
    • Each year's rise is at least the rule's own rate, so the level is never below the max(CPI, floor) path either.
    • The top-up is the smallest 0.1pp step that reaches the path.
    • With the guarantee off, the path is the year-by-year rule.

    All four are Hypothesis tests.

  • Differential against uk-triple-lock rules.rates_matrix('burnham_2030'). An independent transcription agrees on random paths. (Hypothesis)

  • Differential against published figures.

    • Every April 2011-2026 rate, and its binding element.
    • Every weekly rate to the nearest 5p.
    • The inputs match an independent table of the figures each review used.
    • Only April 2011 is overridden.
    • The rate is never below the s150A statutory minimum (for April 2022, the 2021 Act's max(CPI, 2.5%)).
  • Forecasts.

    • Forecasts equal calendar growth plus the gap to 2073.
    • Baseline forecasts reproduce the OBR statutory-basis figures to 1e-5.
    • With the May-July 2026 outturn removed, the rule reproduces the OBR's own triple-lock forecast for April 2027-2031 (3.7%, then 2.5% four times).
  • Scenarios.

    • A macro Scenario applied before the data load moves the April 2028 rate, and the new State Pension a pensioner is paid in a small-dataset microsimulation.
    • A direct input override wins over calendar growth, for that year only.
    • Zero inputs and zero overrides are kept as values, not treated as missing.
    • Levers apply from the April of the year they name, read on 30 April.
    • The plan is a parameter reform with hand-checked rates.
    • active false follows earnings.
    • no_economic_assumptions zeroes the forecast inputs and leaves only the floor.
    • numpy scalars are accepted.
  • Importer.

    • Gap extraction for quarters and September CPI.
    • Gaps measured from stored growth.
    • Values written in fixed point.
    • A failed run writes nothing.
    • Gap files are written next to --yaml-path.
    • The receipts tables are required when writing gaps.

Review. Five independent lenses reviewed the first commit: rule correctness, data and claims against primary sources, mutation testing, integration, and design and docs. Each finding then went to a skeptic.

  • Findings fixed:
    • gaps measured against rounded calendar values;
    • exponent-notation gap values that YAML reads as strings;
    • numpy inputs;
    • non-atomic importer writes;
    • an overstated double-lock lever;
    • several wording overstatements.
  • Mutation testing. It tried 58 mutants of the new code, and 18 survived. Each surviving one now has a test that kills it.
  • Full suite. Locally, on the head before the rebase and the test-only last commit, the non-microsimulation suite gave 385 passed and 19 skipped, and the YAML policy tests 1,264 passed. Those are historical results for the earlier branch state, not a rerun after merging current main. Current focused review-follow-up results are reported in the review reply and handoff.

Not in this PR (follow-ups)

  • Pension Credit standard minimum guarantee: choosing between the triple-lock index and the bare earnings floor is a methodology decision for Max; its uprating is unchanged here. The law, current parameter values, and options are tracked in the follow-up issue.
  • reform= dictionaries don't rebuild derived parameters (pre-existing).
  • Uprating indices stop at 2039, so indexed parameters freeze after that (pre-existing, global).
  • Most benefits are uprated by September CPI, but the model uses lagged calendar CPI. A stacked branch uses the new cpi_september input to fix that.
  • Update May-July 2026 earnings to the October release (20 Oct) and add September 2026 CPI (21 Oct).
  • PolicyEngine/uk-triple-lock migration: the downstream follow-up covers its engine.path_following reproduction check, config.py parameter readers and horizon compatibility guards when bumping the policyengine-uk pin. Consumers must use the observation-year statutory inputs, the following April's generated uprating rate, and the include flags; published inputs take precedence over forecast gaps. yoy_growth.triple_lock is the generated April rate, not a same-year calendar-growth maximum: each prior-year input is rounded to 0.1pp before applying the rule, with forecast gaps only for unpublished inputs (CPI observation years 2026–2030 and earnings observation years 2027–2030), and April 2027 earnings currently using the provisional May-July 2026 first estimate. triple_lock.outturn is null from 2012; generated rates continue through April 2074 and follow prior-year rounded earnings after 2034 under the stored baseline. The provisional State Pension levels are about 0.48% above the previous baseline in 2027–2034; later rates also change. Existing uprating indices still stop at 2039, as noted separately above.
  • Context, not changed here: the OBR's long-run projections add a 0.56pp a year triple-lock wedge over earnings (Fiscal risks and sustainability, July 2026, footnote 52). PolicyEngine's smooth forecasts have none; supply simulated input paths to capture it.

axiom: TheAxiomFoundation/rulespec-uk#366 queued (SSAA 1992 s150A earnings review, with the 2020 c.23 and 2021 c.32 one-year modifications; s150A is not yet in the Axiom corpus). The in-force 2026-27 rates are unchanged and already encoded-correct: uk:regulations/uksi/2026/148/article/6 (241.30) and article/4 (184.90), each with companion tests.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits September 30, 2026 11:34
The April uprating now uses September CPI (ONS D7G7) and May-July AWE
total pay growth as used in each review (ONS KAC3, per the Up-rating Order
explanatory memoranda), not calendar-year OBR growth. The inputs live in
gov.economic_assumptions.statutory_uprating_inputs with ONS outturn, then
forecasts: calendar-year growth plus the OBR's gap between its
statutory-basis and calendar-year forecasts (September CPI from receipts
Table 3.19, Q3 CPI and Q2 earnings from economy Tables 1.7 and 1.6), then
calendar-year growth after the EFO horizon. Macro scenarios that edit
calendar-year growth move them; scenarios can also set them directly.

The rule reproduces every published uprating from April 2012 to April
2026. April 2011 (RPI) is the only override; April 2022 is
include_earnings false. The rates run to the end of the economic
assumptions. The active toggle now means the s150A statutory minimum, and
an optional earnings-path guarantee models plans that keep the pension in
line with earnings over time. The OBR importer regenerates the forecast
gaps.

April 2027 rises by 3.9% (published May-July 2026 earnings) rather than
3.4%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Measure each forecast gap from the calendar-year growth stored in
  yoy_growth.yaml, so the baseline inputs equal the OBR's statutory-basis
  figures (May-July 2027 earnings was 2.5% after rounding, OBR 2.4%).
- Importer: render and check every file before writing any; write gaps in
  fixed point (3e-05 loaded as a string); put gaps next to --yaml-path;
  require the receipts tables unless gaps are skipped; add --gaps-only.
- Accept numpy scalars in the rounding helpers.
- Docs: a double lock is the floor at 0; active=false ignores the floor,
  the include flags and the one-year 2021 and 2022 changes to s150A;
  calendar-year edits need year:YYYY-01-01:1 keys; levers work through
  Scenario, not reform dictionaries; the baseline has no triple-lock
  ratchet premium (OBR: 0.56pp); earnings are as used, not first
  published.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mutation testing of the first commit left 18 mutants alive. New tests
kill them: an independent table of the inputs each review used (typos in
non-binding years), exact OBR gaps, zero inputs and zero overrides kept as
values, an override under the earnings-path guarantee, levers read on
30 April of the year they name, input rounding, the horizon following the
shorter calendar series, the missing-input guard, no_economic_assumptions
zeroing the gaps, half-up ties, and the receipts header row.

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

Copy link
Copy Markdown
Collaborator

Reviewed: approve with nits.

Driving the April rise from September CPI, May–July AWE total pay and 2.5%, with the inputs rounded to 0.1pp before the max, is the right structure. From the stored inputs I recomputed every April from 2012 to 2026 and got the published rate each time; only 2011 needs an override.

  1. Should fix: s.150A(1)(d) also covers the Pension Credit standard minimum guarantee, but parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml:17,33 still uprates it by gov.benefit_uprating_cpi. Now that April 2027 is earnings-led (3.9%), the guarantee rises by CPI only. That understates Pension Credit, and part of the £0.47bn net cost is offset by Pension Credit that wouldn't fall. Uprate it with the triple-lock index, which is what happened in practice in 2023 to 2026, or with max(earnings, 0), or file a follow-up. A separate, pre-existing problem in the same file: the 2025-04-01 values (lines 14, 30: 221.85 / 338.61) should be 227.10 / 346.60 (DWP benefit rates 2025-26).

  2. Should fix: awe_total_pay_may_july.yaml:31 holds the 15 September first estimate, but the file's own rule says reviews use the October release. Label 3.9%, £250.71 and £0.47bn as provisional in the changelog until 20 October.

  3. Should fix: list the downstream breaks in the changelog:

    • yoy_growth.triple_lock no longer equals round(max(calendar earnings, calendar CPI, 2.5%)). It now adds the forecast gaps for 2026–2030 and fixes April 2027 at outturn.
    • triple_lock.outturn is null from 2012.
    • The series runs to 2074 and follows earnings after 2034.
    • Baselines rise 0.48% from 2027.

    uk-triple-lock's engine.py 1e-12 check fails on a bump. It should read statutory_uprating_inputs instead.

  4. Nit: the description includes record-level survey details that should be removed (the 2028-29 bullet). The ~/reviews/... paths can't be reproduced.

  5. Nit: weekly rates aren't rounded to 5p (250.71 vs 250.70). Rounding is optional under s.150A(4).

Verified: I read s.150A on legislation.gov.uk. It is an earnings floor with no cut, the 2020 and 2021 Acts modify it for one year each, and the AWE and CPI series are policy rather than statute. The new and changed tests passed (95/95) and CI is green.

MaxGhenis and others added 3 commits October 1, 2026 09:46
Independent review (Astra gate, round 1) found that an OS error while
writing the second or third file left yoy_growth.yaml and one gap file on
the new forecast and the other on the old. Each file is now staged beside
its target and moved into place; any failure restores the files already
moved and removes the staged copies. Tests fail the move at each of the
three files, and fail the staging step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review (gate round 2) found that restoring an original with
write_text truncates the target first, so a persistent I/O fault during
rollback could leave a file empty with no copy of the original. Each
existing target is now copied to a backup beside it before anything moves,
and rollback restores it with an atomic move. If a restore fails, its
backup is kept and named in the error. A new test fails a move and the
first restore and checks no file is emptied and the original survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Label the September 2026 earnings estimate and derived pension rates as
provisional, describe the exact compatibility changes, and document that
projected weekly State Pension amounts are not rounded to 5p.

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

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi, addressed your 1 October review in c45f23f2a, following merge 054d68de.

  1. Pension Credit uprating stays unchanged for Max's methodology decision: #2072 records s.150A, current CPI projections and both options; Set DWP benefit rates to the announced amounts, not CPI projections #1925 was still open and the 2025 rates remained incorrect at the main commit merged here.
  2. Labelled 3.9% as the provisional 15 September first estimate until the October review vintage, including dependent weekly rates and historical fiscal figures in the PR body.
  3. Added the compatibility changelog: statutory-input maximum, null outturn from 2012, generated rates through 2074, rounded earnings from 2035, and roughly 0.48% higher pension levels through 2034; downstream #20 covers the verified path_following check, readers and horizon guards.
  4. Removed the 2028-29 survey-record decomposition; retained only the aggregate net figure and replaced local script paths with an author's-machine note.
  5. Documented that projected weekly rates are not rounded to 5p, citing s.150A(4), read this session.

Triple-lock mechanics are unchanged. Format/lint passed; 79 rule/outturn/importer tests and one statutory-input case passed in fresh runs. The combined run was interrupted after 86 passes under extreme memory pressure; remaining statutory-input, DWP YAML and parameter/metadata/code-health checks are pending because the sandbox denies the required swap-usage reading.

@MaxGhenis
MaxGhenis merged commit 963b9ce into main Oct 3, 2026
7 checks passed
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged at c45f23f2 under Max's merge-on-gates rule for PolicyEngine-UK fixes (2 October 2026; decisions d726 and d854).

@MaxGhenis
MaxGhenis deleted the triple-lock-statutory-inputs branch October 3, 2026 02:02
MaxGhenis added a commit that referenced this pull request Oct 3, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/book/programs/gov/dwp/universal-credit.ipynb
#	policyengine_uk/tests/test_uc_state_pension_properties.py
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