Skip to content

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

Closed

Description

@juaristi22

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_region compiles to a single reference pinned geography_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 signed fixture_only in 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 are ledger_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 is level = "region" if levels == ("region",) else "country". The geography_ladder.py docstring 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_estimate by 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_2025 carries 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 and wales / scotland / northern_ireland / uk for the nations.
  • Spine. The national frame carries region from FRS gvtregno via REGION_MAP (uk_runtime/frs_spine.py:76, column list at :225). The OA ladder carries a GSS region_code per household (uk_runtime/geography_ladder.py:128, UK_ENGLAND_WALES_REGION_CODES; rowwise_geography.py region_code_oa).
  • Compiler. The selector already matches geography_level / geography_id against 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_geography validates 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_ids and names rows target_id@geography_id (target_reference_authoring.py:262, author_area_target_references); the national generator never calls it.

The three missing pieces

  1. 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_name fans 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 filters country == ENGLAND, fans over the nine English regions (81 rows). PR-T's UK_NATIONAL_REGION_ROSTER (nine E12 codes) is the seed for the roster.
  2. A per-reference region predicate at materialization. _prepared_column_values (microcosm/build/target_materialization.py:632) builds the measured column from the binding's own filters / household_conditions only; nothing translates a reference's ledger_selector.geography_id into region == 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 → region enum name: E12000007 → LONDON, W92000004 → WALES, …; NORTHERN_IRELAND is a live enum value on the spine only where gvtregno = 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 mask region == X ∧ age ∈ band, mapped to households; compute/council_tax.py uses the same household-level region mask for the VOA rows).
  3. A region grain in the cross-grain rule. UK_CROSS_GRAIN_RULE (uk_runtime/ledger_targets.py:159) declares grain_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, and parent_geography_legs maps only K02/K03/E92/W92/S92/N92 to country legs. cross_grain.py:806 raises "cross-grain parent geography … declares no legs" for any other control geography, and nine English region controls all mapping to one England leg would trip the two-values check at cross_grain.py:280. So fanning out the age targets without a region grain breaks the constituency/LA age reconciliation. Needed: region in the precedence (country > region > constituency > la), E12 entries in parent_geography_legs (twelve legs instead of four), and a leg_of_area that reads the ladder's region_code for 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 as cross_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)? Under family_equal the 189 new rows also change the ons_population and council_tax_stock family weights.
  • Naming. target_id@geography_id (the local convention, _spec_geography already reads it) with a fixture-alignment map so the 108 incumbent rows compare as matched rather than fixture_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

  1. added 20 commits that reference this issue on Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions