Skip to content

Encode fuel duty as its dated statutory schedule - #1883

Draft
MaxGhenis wants to merge 2 commits into
mainfrom
fuel-duty-dated-schedule
Draft

MaxGhenis wants to merge 2 commits into
mainfrom
fuel-duty-dated-schedule

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1882

Summary

gov.hmrc.fuel_duty.petrol_and_diesel stored calendar-year averages dated 1 January. Model years run from 6 April to 5 April, so each model year read the wrong average. Model year 2027 (2027-28) read 59.25p a litre, although every day of 2027-28 falls after the April 2027 RPI uprating. This PR keys each rate to its effective date, cites its instrument, and turns on fiscal_year_blend: true, which day-weights the schedule across each model year.

Effective date Rate (£/litre) Source
23 Mar 2022 0.5295 SI 2022/365, continued by SI 2023/329, 2024/300, 2025/228 and SI 2026/164 art. 3 as amended by SI 2026/555 (to 31 Dec 2026)
1 Jan 2027 0.5595 SI 2026/164 art. 8, as amended by SI 2026/555
1 Mar 2027 0.5795 Hydrocarbon Oil Duties Act 1979 s.6(1A); no reduction order applies
1 Apr 2027 0.5975 Forecast: 57.95p × (1 + 3.1%, 2026 RPI)
1 Apr 2028 0.6154 Forecast: × (1 + 3.0%)
1 Apr 2029 0.6326 Forecast: × (1 + 2.8%)
1 Apr 2030 0.6509 Forecast: × (1 + 2.9%)
1 Apr 2031 0.6659 Forecast: × (1 + 2.3%)

HMRC's Amended Fuel Duty rates: 2026 to 2027 (22 May 2026) and SI 2026/555 removed the 1 September 2026 step and moved the December step to 1 January 2027.

The April steps are a forecast, not law. Budget 2025 says rates will be uprated "by Retail Prices Index (RPI) from April 2027". HM Treasury's Budget 2025 policy costings, page 14 dates the uprating from 1 April 2027. HMRC's May 2026 note says final rates will be confirmed at Budget 2026. No official source I found (Budget 2025 costings, OBR November 2025 and March 2026 EFOs, the OBR fuel duties page) names the RPI measure. The model keeps its existing convention: each 1 April applies the previous calendar year's growth from gov.economic_assumptions.yoy_growth.obr.rpi, which holds the OBR March 2026 calendar-year series at one decimal place. It rounds to the nearest 0.01p. calculate_fuel_duty_rates.py now writes those steps to 1 April 2031. After that, cadence uprating (at_defined_interval) continues each 1 April.

Model-year rates (£/litre)

Model year Before After Hand computation
2025 0.5295 0.5295 365 days at 0.5295
2026 0.5345 0.539527 (0.5295×270 + 0.5595×59 + 0.5795×31 + 0.5975×5) / 365
2027 0.5925 0.597745 (0.5975×361 + 0.6154×5) / 366
2028 0.6151 0.615636 (0.6154×360 + 0.6326×5) / 365
2029 0.6332 0.632851 (0.6326×360 + 0.6509×5) / 365
2030 0.6516 0.651105 (0.6509×360 + 0.6659×5) / 365

A reform holding 55.95p for 2027 now saves 3.82p a litre (59.77p − 55.95p), where it previously saved 3.30p.

Other changes

  • Road fuel gases: lpg and natural_gas move to the amended dates (0.3052 and 0.2385 per kg from 1 Jan 2027; SI 2026/164 art. 8, Table E). Model year 2026 gas duty on 1,000 kg of each falls from £528.94 to £524.01.
  • History: the petrol and diesel history drops a 2012 rate of 60.97p that was never charged. Finance Act 2011 s.20 wrote it into the Act, but SI 2011/2904, Finance Act 2012 s.188 and SI 2012/3055 meant 57.95p applied throughout. Earlier steps now cite their Finance Act sections.
  • Model year 2021: the blend now counts the 14 days of the 5p cut from 23 March 2022, giving 0.577582 rather than 0.5795.
  • Docs: docs/engineering/excise-duties.md and the fuel duty notebook describe the dated schedule, and no longer say petrol and diesel use calendar years.

Invariants

Each is tested in policyengine_uk/tests/test_fuel_duty_schedule.py.

  1. Statutory dates: the raw parameter gives the statutory rate on each side of every change. Covered dates are 22/23 Mar 2022, 1 Sep 2026, 1 Dec 2026, 31 Dec 2026/1 Jan 2027, 28 Feb/1 Mar 2027 and 31 Mar 2027, plus 57.95p in 2012.
  2. Hand-computed years: model years 2025–2030 equal the day-weighted averages above, and the YAML policy tests check fuel_duty on 1,000 litres for the same years.
  3. Differential against a daily sum: for every converted year from 2015 to 2040, the model-year rate equals a brute-force average over each day of the dated schedule (relative tolerance 1e-12).
  4. Bounds: every model-year rate lies between the lowest and highest rate in force during that year (2015–2040).
  5. RPI staleness guard: the written April steps equal 57.95p compounded by yoy_growth.obr.rpi, a differential against the generator's inputs. The test fails with a regeneration command when the RPI forecast changes.
  6. Cadence: after 2031, each 1 April applies the previous calendar year's RPI growth, and the rate is unchanged on 31 March.
  7. Monotonicity (intended while all steps and forecast RPI are positive): model-year rates do not fall from 2022 to 2040.
  8. Reform strength (Hypothesis, years 2026–2030, rates 0–2): an annual Scenario override sets that model year's rate and fuel_duty exactly, and leaves every other model year from 2015 to 2040 unchanged.

Mutation check: against main's parameter file, 32 of the 73 new Python tests fail.

Independent review (Subfleet, Opus) of 9390415 confirmed every rate, date, reference and computed value, and the code paths. Its valid findings are fixed in 0cfd91e: reference titles, the notebook, testing every year for reform isolation, and the limitation's scope. One finding was refuted: Budget 2025 does contain the RPI statement.

Known limitation

The limitation applies to Scenario.parameter_changes overrides on model year 2031 or later, where the steps come from uprating metadata. Those overrides make every later year one 1 April step low, because uprating restarts after the reformed year from the last written rate. Executed examples:

  • A 0.70 override for 2031 gives 2032 = 0.66612, against a baseline of 0.68142.
  • Overrides for 2029 and 2030 leave all other years unchanged.
  • A legacy dict reform for 2031 leaves 2032 onwards at baseline.

This is how core uprating treats any uprated parameter, and the docs note it.

Commands run

  • uv run pytest policyengine_uk/tests/test_fuel_duty_schedule.py: 73 passed (at 0cfd91e, 106 with the fiscal-year, excise and reform suites)
  • uv run pytest policyengine_uk/tests/test_fiscal_year_parameters.py test_excise_schedules.py test_fuel_duty_litres.py test_road_fuel_volume_uprating.py test_parametric_reform_impacts.py: 36 passed
  • uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/fuel_duty .../excise_integration.yaml -c policyengine_uk: 16 passed
  • Full policy YAML suite: result in the first comment
  • uvx ruff format . and ruff check on changed Python: clean
  • calculate_fuel_duty_rates.py --update reproduces the committed block byte for byte

The full pytest suite and the microsimulation tests run in CI.

hypothesis>=6 is added to the dev extra for the property test.

axiom: TheAxiomFoundation/rulespec-uk#349 queued

🤖 Generated with Claude Code

Key each petrol and diesel rate to its effective date and day-weight it
across each model year with fiscal_year_blend, instead of storing
calendar-year averages dated 1 January. Model year 2027 (2027-28) was
59.25p; it is now 59.77p, the April 2027 RPI-uprated rate.

The schedule follows SI 2026/164 as amended by SI 2026/555 (HMRC amended
rates, 22 May 2026): 52.95p to 31 December 2026, 55.95p from 1 January
2027, 57.95p from 1 March 2027. RPI steps each 1 April from 2027 are
generated from the OBR March 2026 RPI series by
calculate_fuel_duty_rates.py and extended by cadence uprating. LPG and
natural gas move to the same dates. The 2012 rate of 60.97p, never
charged, is removed.

Fixes #1882

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

Copy link
Copy Markdown
Collaborator Author

Full policy YAML suite on 9390415 (uv run policyengine-core test policyengine_uk/tests/policy -c policyengine_uk): 1261 passed.

- Cite HM Treasury's Budget 2025 costing (page 14) for the 1 April 2027
  uprating date and HMRC's amended note for final rates at Budget 2026;
  the generator takes the RPI forecast citation from yoy_growth.yaml.
- Complete statutory references: heavy oil in FA 2010 s.13(2)(c), and
  FA 2012 s.188 and SI 2012/3055 for the 2012 rate never charged.
- Notebook shows the dated schedule from the parameter file, re-executed.
- Test that an annual override leaves every other model year unchanged.
- Docs: keep the original HMRC note for RPI uprating from 2027/28, name
  the Scenario.parameter_changes path in the 2031+ limitation, and note
  the RPI index ends in 2039.

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

Copy link
Copy Markdown
Collaborator Author

Axiom differential check for this fix. TheAxiomFoundation/axiom-oracles#565 rebuilds Axiom's uk-fuel-duty comparison, so it no longer feeds PolicyEngine's rate into Axiom. It compares pe-uk's monthly fuel_duty, per litre, at every month start from January 2025 to December 2028 against the HODA 1979 s.6(1A) standing rate less the reduction in force on that date. The reductions are compiled from the orders on legislation.gov.uk (reference/uk-fuel-duty-schedule/schedule.yaml in that PR):

Period Reduction Rate Instrument
to 31 Dec 2026 8.63% 52.95p SI 2022/365 art 3, continued by SI 2023/329, 2024/300, 2025/228, and SI 2026/164 art 3 as amended by SI 2026/555 art 2(3)
1 Jan to 28 Feb 2027 3.45% 55.95p SI 2026/164 art 8, with the period in art 1(4) as amended by SI 2026/555 art 2(2)(c)
from 1 Mar 2027 none 57.95p HODA 1979 s.6(1A)

This agrees with the statutory part of this PR's schedule.

Against pe-uk 2.102.0:

One nuance: the orders deduct a percentage from the amount payable, and column (D) is "only for ease of reference". So the operative charge is 0.5795 × 0.9137 = 0.52948915 a litre, not 0.5295 (and 0.55950725, not 0.5595). The gap is about £0.01 per 920 litres. HMRC publishes the column (D) figures, so the Axiom comparison uses them and compares at a £0.00005 per-litre tolerance.

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.

Fuel duty rates are calendar-year averages of an outdated schedule

1 participant