Repository navigation
Conversation
Option 1 of the pensioner calibration review: measure each person's 2023-24 total income, income tax, State Pension and private pension on the engine (spi_2023_* dated measures), band at the published 2023-24 edges and bind the published values with no uprating_index. Counts and amounts carry each person's ONS population factor (sex by age band, 1 October 2023 over mid-2025). Work in progress: target references not yet regenerated, vendored ONS headers not yet regenerated, measurements pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oves The measurement-period guard now runs before a row's band edges are read, so a band row whose variables sit at two years is refused rather than skipped for want of its edges. The carried-back inputs go through the same renames policyengine-uk applies when it loads a dataset (employment income to its pre-response twin, and three others), so the 2023-24 annual simulation reads pay through its partial-year shares. A guard refuses any input left without a value at the dataset year. Council tax, rent, water charges and student loans are projected by region, tenure or new-borrower rules after the index step; the round trip records them as held, with the largest miss, and an engine test shows holding them moves no dated measure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ections uk_runtime.itl_differential reads HMRC's July 2026 projections at the calibration year's calendar window: Table 2.5 taxpayers by total-income band and in all bands; Table 2.1 United Kingdom taxpayers of State Pension age, aged 65 and over and below State Pension age; Table 2.2 the two age groups in each region-tier area and summed over them. Each cell carries a tolerance fixed in the module (5% national, 10% per area) and an in-sample flag from the contract. tools/check_uk_itl_projections.py runs it on a national H5; an env-gated slow test asserts no cell misses. Invariants under property tests: the window is 3/12 and 9/12 of the two tax years; cells and model counts do not depend on row or person order (model counts are exactly rounded sums); band cells with the taxpayers below the first edge, area cells, and State Pension age with below it partition their totals; a cell is within tolerance exactly when its relative miss is no larger than its tolerance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--dated-year 2023 reads each person's 2023-24 taxpayer status and total income from the dated spi_2023_* measures, weighted by household weight times population factor, and sets the Table 2.5 band counts beside the year HMRC observed. The two checks share one measure resolver. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows Inventories the SPI band families and how each reaches calendar 2025, says which carry the band-projection problem and what dating them needs, and reads the US SOI AGI band path: stale nominal bins are already refused, and the rescued rows bind as source-year shares of a bound total, so they keep the share drift but not the stale levels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main measures the four Table 3.6 pension rows on 2025 incomes against the projected values; the pilot measures them on the dated spi_2023_* measures against the published values. The script reads one H5 both ways, so each build is also scored on the definition it was not fitted to, and checks its own reading against the solver's final estimates on the native definition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The round trip now misses by infinity when one side of a value is missing, and compares student loan repayments on the rows whose plan the engine kept (uprate_student_loan_plans zeroes a repayment exactly where it reassigns the plan), instead of exempting them. The resolver refuses a row that dates SPI measures and reads a variable of another year, a dated SPI measure bound through a provider, and a row measured at its fact's year that also projects the fact. Materialization checks a row's years on a frame without period metadata too. The ITL check windows only the rows its cells read, refuses Table 2.5 bands outside ITL_BANDS, marks the all-band count in sample when its bands are, and reads in-sample from a run's diagnostics with --diagnostics. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools/generate_uk_target_references.py on the 272c987 national cut, and the two ONS vendored resources for their new consumer (tools/vendor_uk_ledger_facts.py). Only the 52 references of the four SPI 2023-24 pension band families move. Each now has period 2025, metadata.measurement_period 2023, source_period_policy exact_observation and the published 2023-24 value, with no uprating_index. The membership report holds them 2023 to 2025 without an index, as the CGT liability row is held. The State Pension count rows now total the published 7,977,000. Before, they were projected to 8,361,001. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…SPI rows The compile parity surface drops the 52 calibration-drift rows of the four SPI 2023-24 pension band families: they now bind the published 2023-24 values, which the frozen incumbent fixture also holds, so ledger and fixture agree. No other row moved. The UK spine graph fixture repins hmrc_spi_income_spine's stage contract hash, which covers the ONS population resource whose consumer list this branch extended. The population-targets closed-world test admits the new taxpayers_tax_year_2023_24_at_own_year observation basis. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… engine moves them The probe on the national spine found two stored inputs the dated SPI carry-back does not return through policyengine-uk's own forward projection, so the resolver would refuse every dated measure in a national build: - bsp_reported: its declared index is CPI, but the engine holds it flat once the file stores both separated bereavement benefits (BEREAVEMENT_SPLIT_INPUTS), as the FRS spine does. The carry-back now holds it under the same condition and records the rule in the receipt (held_by_engine_rule). - council_tax_benefit_reported: uprate_council_tax moves it with its household's council tax factor. It is held and exempted from the round trip as council_tax itself is; no dated measure reads it. The engine test covers split and legacy BSP and a positive council tax benefit. Also found independently by the round-2 review (P1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a proposed SPI hold The round-2 review of 2aa2607 found guard gaps in the dated SPI rows and the ITL check (the P1, council tax benefit and Bereavement Support Payment, landed in 058690b). This answers the rest: - Concept filters: the mixed-year checks read one inventory of executable reads (_binding_reads): a predicate's variable or concept, on its own entity, as _predicate_mask reads it. The resolver reads concept too. - Provider fields: uk_spi_dated_measures_named scans every string in a binding, so a provider that names a dated SPI measure in a field of its own (affected_flag_variable, count_of, value_reduction) is refused. - Metadata-less frames: a row declaring a non-calibration period whose value terms read a variable no name dates and the frame does not itself store is refused. "Stores" reads the adapter's stored_columns, not has_column, which also sees the resolver's injected engine columns. Filters stay read at the frame, as at the frame's own period. - ITL in sample: run_bound_targets keeps each weighted row's name, so a cell is in sample by its own band or area; the all-band, residents', Table 2.1 UK and below-State-Pension-age cells need every part bound. Rows of another geography are no longer read. - Student loans: a reassigned plan's repayment must come back exactly zero, as pe-uk sets it, before the kept rows are compared. - uk_runtime.spi_dated_measures is listed among the UK target compile modules: ledger_targets imports it for value_period. - uk/uprating_holds.json proposes the matched_period hold for the four SPI families, unsigned, and a test requires a declaration for every national reference measured at an earlier year with no index. Invariants: a mixed-year or provider-read dated row is refused; an engine value under a declared non-calibration period is refused without frame metadata; a sum is in sample only when every part is; the compile refuses the SPI rows until a person signs the hold. test_every_committed_hold_is_signed fails on exactly the four SPI families until then, on purpose. axiom: n/a: calibration target materialization and diagnostics, no policy rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
13 tasks
The round-3 review of 073e9b5 found four P2s and three P3s. This answers them: - Provider periods: the measurement-period guard now runs for every binding that declares a period. A provider's value reads (_provider_value_reads, mirroring parameter_gated_threshold and baseline_flag_crosstab) must each be held at the declared period by one rule, _read_honours: an entity count holds none, a stored or name-dated read holds its own, a stored column on a metadata-less frame is read as it is, and the engine computes the rest at the calibration period. A provider whose reads are not known here (a counterfactual delta, a caller's kind) may declare only the calibration period. - Counts: the entity-count exemption applies in every branch, so a count reads alike with and without frame metadata; the resolver still refuses a count banded by a dated SPI measure. - ITL sums: a sum cell carries its parts. differential keeps it in sample only when every part is and the file holds nobody it counts outside them (outside_parts), so a taxpayer below the lowest band edge, or one in no area, keeps the all-band or the UK count out. Families report in_sample_cells and are in sample only when every cell is. - run_bound_targets binds a diagnostics row only at its own period (target_name and period, or the name's trailing @year). - uk_spi_dated_measures_named skips annotation fields (metric_name, notes, source_lines) at every depth. - The hold preflight reads measurement periods from the executable bindings and takes a null index as none. - same_labels compares round-trip labels by value (bytes decoded, numbers as numbers), for label columns and the student-loan plan. - held_by_engine_rule names Bereavement Support Payment only when the file stores it. - The ITL test header no longer says the bands partition all taxpayers: with the taxpayers below the first edge they do. docs/evidence/uk-spi-dated-bands/README.md records the A/B on one spine (aggregates only). The pilot fits its 2023-24 rows but puts taxpaying State Pension recipients at 9.26m against HMRC's implied 8.65m-8.81m (#1414's re-index: 8.74m), misses three ITL Table 2.2 cells main passes, and lowers the sole-income allowance to 1.117m people and GBP 75m in 2027-28 (main 1.235m, GBP 84m; #1414 1.326m, GBP 93m). It should not replace #1414's re-index. Invariants: a declared period no read honours refuses the run, for plain and provider rows alike; a count holds no period; a sum in sample has its model count fixed by its in-sample parts (property test); a family is in sample exactly when all its cells are. axiom: n/a: calibration target materialization and diagnostics, no policy rule Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Closing unmerged: the option 1 pilot lost the A/B. What happened. The pilot fits the four SPI pension band rows at 2023-24, but on the out-of-sample tests it is worse than main:
#1414's re-index (merged) is inside both ranges, so it stands. Where things are.
Methodology call. Whether to bind any SPI or SOI band family at its own year is queued for Max as d2111, with a recommendation of no. If he rules to adopt option 1, reopen this PR. The branch is kept. Reusable pieces. Two parts could land on their own, each proposed separately:
|
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Pilot of option 1 of the UK pensioner calibration review (approved by Max, 2026-10-27). Result: the pilot does not win, so this PR should not merge. It fits the four rows it binds, but on every out-of-sample test it is worse than main and further from the re-index that #1414 merged for #1069. Closed unmerged on that result; the branch is kept, so reopen it if Max rules to adopt option 1 (decision d2111).
A/B result (aggregates only; full tables in
docs/evidence/uk-spi-dated-bands/README.md)Three national builds on one FRS 2024-25 spine, weights solved at 2025 over 1,273 targets: baseline (main 7d45c0c), pilot (this branch plus a "PILOT BUILD ONLY" hold fill, local 71856c6fa), and #1414's candidate.
What changes
The four SPI 2023-24 Table 3.6 band rows (
hmrc.spi.{state_pension,private_pension_income}.{count,amount}_by_total_income_band) stop being projected to calendar 2025 by HMRC's all-taxpayer growth per nominal band. Instead they bind the published 2023-24 values at 2023-24, following the CGT dated-measure pattern (cgt_2024_*):spi_2023_{total_income,income_tax,state_pension,private_pension_income,person_count}(uk_runtime/spi_dated_measures.py, resolver routes inuk_runtime/measure_simulation.py).uprating_indices.yamlinput divided by its year-on-year growth;_before_lsr/_reportedbases, asSimulation.__init__moves them.extend_single_year_datasetmust return the carried-back file to the stored one for every input column. The resolver refuses otherwise.ons.population.*targets._binding_reads): value terms, each filter'svariableorconcepton its own entity, and the band.cgt_<y>_*) may now declare its year too. That is intended.stored_columns; a column the resolver injected does not count as stored.spi_<y>_*measure alongside a variable of another year (values, filters, band andvalue_expressionterms);uprating_index, because that would move the fact twice.Invariants (property-tested in
tests/engine_free/uk/test_uk_spi_dated_measures.py)uprating_index. Each is bound atmeasurement_period2023 withrequire_matching_fact_period, and every variable of each row isspi_2023_*.student_loan_repaymentsis compared on the rows where the engine kept the loan plan, and must come back exactly zero where it reassigned it, asuprate_student_loan_planssets it.ITL differential (
uk_runtime/itl_differential.py,tools/check_uk_itl_projections.py)The ITL July 2026 projections become a test of a calibrated file, not targets:
--diagnostics, "bound" means bound in that run, cell by cell: a diagnostics row (named by band or area) with a missing or positive loss weight.outside_partsis zero). The sums are: the all-band count over its bands, the residents' count over its areas, Table 2.1's UK age cell over its twelve areas, and the count below State Pension age over that cell and the all-band count.target_nameandperiod, or the name's trailing@year).in_sample_cells.--dated-year 2023checks the file's own 2023-24 taxpayers by band, on thespi_2023_*measures, against Table 2.5's 2023-24 outturn.tests/engine/uk/test_uk_itl_differential_build.pyruns this on a built file whenMICROCOSM_UK_ITL_DIFFERENTIAL_H5is set.Invariants, property-tested in
tests/engine_free/uk/test_uk_itl_differential.py:ITL_BANDSare refused.Hold
uk/uprating_holds.jsoncarries the four families as a proposedmatched_periodhold: unsigned, statusproposed. The compile refuses a target held at an older period without a signed hold (microcosm#1123), sotest_every_committed_hold_is_signedfails on exactly these four until María or Max signs. That is the intended blocker. A new test,test_every_reference_measured_at_an_earlier_year_is_declared, requires a declaration for every national reference measured at an earlier year with no index; it catches the failure that stopped the first pilot national.The A/B pilot national was built from a local branch, never pushed, that fills the hold's approval fields with an explicit "PILOT BUILD ONLY, not a register signature" label.
Review
uk_runtime.spi_dated_measuresamong the UK target compile modules, sinceledger_targetsimports it._provider_value_reads,_read_honours);outside_parts;metric_name,notes,source_lines) are skipped by the dated-measure scan;in_sample_cells;same_labels).Status
docs/evidence/uk-spi-dated-bands/README.md).A signed hold: moot unless Max adopts option 1 (decision d2111).axiom: n/a: microsim calibration-target measurement change, no policy encoded
🤖 Generated with Claude Code