Repository navigation
UK region-grain activation: fan out the 18 two-level contract targets over a region roster and give the cross-grain rule a region leg #905
Description
The 18 two-level UK contract targets — the nine ons.population.age_*_by_region rows and the nine voa.council_tax_stock.band_a..h / total rows, all declared geography_levels: ["country", "region"] — still compile to one country-grain reference each. Region grain has never activated. #736 carries the deferral as one unchecked line, #665 lists it as remaining, and #890 §B covers only the two single-level DfT London bus rows; the two region_pin_unsupported signed exclusions point at #790, which closed on 2026-09-09 without touching region. This issue owns the activation.
What "not active" means on main (6f7571e)
ons.population.age_0_9_by_regioncompiles to a single reference pinnedgeography_level: country,geography_id: K02000001, resolved value 7,553,013 (the UK-wide 0–9 total from the mid-2024 fact). Same for the other eight bands. The nine VOA band targets pin England, E92000001, one row each (uk/target_reference_membership.json→geography_pins).- The incumbent's 108 region×age rows (12 areas × 9 bands,
ons/london_age_0_9…) are signedfixture_onlyin both parity registers (ledger_compile_parity_production_2023_signed_differences.json,ledger_compile_parity_incumbent_2025_signed_differences.json), and the nine by-region references areledger_only. The 2025 fixture carries 108 such rows (WS-C deferred adjudications and the reconciliation layer #736 says 131). - The 55k rebuild report (https://uk-dataset-evaluation.vercel.app/55k-rebuild-2026-09-10/) records that the enhanced FRS "keeps its edge only on the region-grain controls it binds and we do not (VOA council tax stock and ONS population by region)".
- Corrections to standing text: Bind DfT, HMRC, DESNZ, Ofgem and ORR facts for bus fares, road fuel, domestic energy and rail support (#790 ruling: bind facts and calibrate) #890's body says the ONS by-region rows "resolve English regions at region level" — they do not on
main, and PR-T (populace-890t) leaves the two-level targets at country because its pin rule islevel = "region" if levels == ("region",) else "country". Thegeography_ladder.pydocstring calls region "a calibrated dimension of the national build"; it is not yet.
What already exists (nothing needed from Chronicle or the spine)
- Facts. The pinned feed (chronicle ec7169b) carries
ons.mid_year_population_estimateby single year of age for all nine English regions (E12000001–E12000009,geography_level: region), Wales, Scotland, Northern Ireland and the UK (country) at mid-2024 (packages/ons/mye_2024_uk), plus the mid-2023 regions package.packages/voa/council_tax_bands_2025carries the nine English regions at region level plus England, Wales and England-and-Wales. The fact value ids are the lower-cased GSS codes (e12000007) for regions andwales/scotland/northern_ireland/ukfor the nations. - Spine. The national frame carries
regionfrom FRSgvtregnoviaREGION_MAP(uk_runtime/frs_spine.py:76, column list at :225). The OA ladder carries a GSSregion_codeper household (uk_runtime/geography_ladder.py:128,UK_ENGLAND_WALES_REGION_CODES;rowwise_geography.pyregion_code_oa). - Compiler. The selector already matches
geography_level/geography_idagainst the fact stamp (microcosm/build/ledger_targets.py:2147-2150);UK_NATIONAL_TARGET_GEOGRAPHY_LEVELS = {"country", "region"}already admits region references (uk_runtime/ledger_targets.py:57);_spec_geographyvalidates a reference's level against the contract's declared levels. The registry-parity map already carries all 108 incumbent region names → the nine contract ids. - Fan-out machinery. The local generator fans a target over
geography_levels × roster area_idsand names rowstarget_id@geography_id(target_reference_authoring.py:262,author_area_target_references); the national generator never calls it.
The three missing pieces
- A region roster and a geography fan-out in the national generator.
_geography_pins(tools/generate_uk_target_references.py:187) gives every national target exactly one geography, and_fanout_namefans out only over dimension tokens (SPI bands, UC award bands) while explicitly skipping geography value ids (_GEOGRAPHY_VALUE_IDS, :517). Needed: a declared roster of the twelve areas the incumbent binds — nine E12 codes at region level and W92000004 / S92000003 / N92000002 at country level, matching Chronicle's stamps — and one reference per area for each two-level target. The ONS family fans over all twelve (108 rows); the VOA family, whose binding filterscountry == ENGLAND, fans over the nine English regions (81 rows). PR-T'sUK_NATIONAL_REGION_ROSTER(nine E12 codes) is the seed for the roster. - A per-reference region predicate at materialization.
_prepared_column_values(microcosm/build/target_materialization.py:632) builds the measured column from the binding's ownfilters/household_conditionsonly; nothing translates a reference'sledger_selector.geography_idintoregion == LONDON. The London bus rows work on PR-T only because the contract hard-codes that filter, which a fan-out from one contract entry cannot do. Needed: either the generator stamps a geography predicate into each fanned reference's binding (E12 code →regionenum name: E12000007 →LONDON, W92000004 →WALES, …;NORTHERN_IRELANDis a live enum value on the spine only wheregvtregno= 13 is present), or the UK adapter derives the mask from the reference's geography metadata. The incumbent does the latter (policyengine_uk_data/targets/compute/demographics.py:22,compute_regional_age: person maskregion == X ∧ age ∈ band, mapped to households;compute/council_tax.pyuses the same household-level region mask for the VOA rows). - A region grain in the cross-grain rule.
UK_CROSS_GRAIN_RULE(uk_runtime/ledger_targets.py:159) declaresgrain_precedence = ("country", "constituency", "la"); its eight age bridges (national_age_0_9_vs_local_age_0_10…) name the by-region targets as their higher control, andparent_geography_legsmaps only K02/K03/E92/W92/S92/N92 to country legs.cross_grain.py:806raises"cross-grain parent geography … declares no legs"for any other control geography, and nine English region controls all mapping to oneEnglandleg would trip the two-values check atcross_grain.py:280. So fanning out the age targets without a region grain breaks the constituency/LA age reconciliation. Needed:regionin the precedence (country > region > constituency > la), E12 entries inparent_geography_legs(twelve legs instead of four), and aleg_of_areathat reads the ladder'sregion_codefor English constituencies and authorities instead of the E/W/S/N prefix rule (local_targets.py:367,_country_from_area_code). This is the "cross-grain leg" the signed-exclusion rationale names, and the same gap is already on the parity register ascross_grain_council_tax_stock_england_region(uk_runtime/data_target_parity.py:394: "the Declare cross-grain target reconciliation for the UK local surface: national wins, sub-national rescales (#495 increment 6 companion) #802 rule declares only country > constituency > LA and cannot accept region rows").
Rulings needed
- Country rows after the fan-out. The nine K02000001 by-band rows are redundant with the fanned rows (the twelve areas sum to the UK total in the same publication) and with the contract's national gender×age 5-year bands and
ons.population.uk_total; the nine England VOA rows are the sum of the nine region rows. Retire the 18 country rows (reference count 415 → 586) or keep them as consistency assertions (→ 604)? Underfamily_equalthe 189 new rows also change theons_populationandcouncil_tax_stockfamily weights. - Naming.
target_id@geography_id(the local convention,_spec_geographyalready reads it) with a fixture-alignment map so the 108 incumbent rows compare as matched rather thanfixture_only, or incumbent-compatible names (ons/london_age_0_9) as the SPI fan-out does? - Predicate placement. Generator-stamped binding predicate (visible in the committed register, no adapter change) or adapter translation from geography metadata (one code path for national and local)?
What moves when this lands
ACTIVE_REFERENCE_COUNT = 415 (tests/test_uk_national_calibration.py:44), both parity signed-difference registers (108 fixture_only + 9 ledger_only → matched), target_reference_membership.json counts and geography_pins, the bridge test near tests/test_uk_ledger_targets.py:1077, and the two PR-T region_pin_unsupported retirements (they retire in PR-T anyway). Re-measure on the next candidate: this is the family where the report says the incumbent leads.
Refs: #736, #665, #890, #802 (cross-grain ruling), #147 (local activation), PR #831, PR #735.
🤖 Posted from the region-grain investigation session (Claude Code).
Activity
Record the region-tier calibration comparison on the spine-q pool (#905)
Fan out the two-level UK targets over the region tier (#905)
Scope region-tier cells with a per-reference geography predicate (#905)
Make region a cross-grain tier with tier-code legs (#905)
Record the region-tier calibration comparison on the spine-q pool (#905)
Regenerate the region-tier receipts on the 474a0ae feed (#905)
Move the region tier constants to the shared geography constants modu…
Fan out the two-level UK targets over the region tier (#905)
Scope region-tier cells with a per-reference geography predicate (#905)
Make region a cross-grain tier with tier-code legs (#905)
Record the region-tier calibration comparison on the spine-q pool (#905)
Regenerate the region-tier receipts on the 474a0ae feed (#905)
Move the region tier constants to the shared geography constants modu…
Sign the three band-H region cells with no spine support (#905)
Resolve region legs from the run's ladder, with the crosswalk as fall…
Assign each lower leg to its nearest control grain (#905)
Address the rest of Vahid's review of #906 (#905)
Declare the council-tax stock totals' universe so they leave the part…
Derive leg licences with the resolver the surface reconciles with (#905)
Merge pull request #906 from PolicyEngine/uk-region-grain-905