Skip to content

Spread each UK household's BRMA across its local clones - #1273

Closed
MaxGhenis wants to merge 11 commits into
mainfrom
uk-brma-clone-spread-v2
Closed

MaxGhenis wants to merge 11 commits into
mainfrom
uk-brma-clone-spread-v2

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Draft. The code is ported to current main and tested. An independent review at this head is running. Calibrated outcomes have not been measured (see "What this does not show").

Problem

frs_brma gives each UK spine household one Broad Rental Market Area (BRMA) draw. The local candidate clones every spine household K = 15 times, and today every clone keeps that one BRMA.

In policyengine-uk-data the same single draw is noisy. With weights held fixed, one re-draw has an SD of £177m for UK Universal Credit and 53k for people in AHC poverty, and a few heavily weighted private renters carry most of it.

Change

The local pool spreads each household's BRMA across its K clones.

  • uk.full.brma_table, before expansion, engine. For each normalized spine household it computes:
    • its exact single-draw BRMA distribution: the mean of its benefit units' census private-rented-household cells by LHA_category, as frs_brma draws;
    • an order over its region's BRMAs by household-mean uncapped_BRMA_LHA_rate, falling back to BRMA_LHA_rate;
    • a uniform u_h keyed by the household's pre-geographic lineage, with seed 0 and salt brma:clone_spread.
  • uk.full.brma, after expansion, engine-free, Owned("household", "brma", "string", rewrite=True). Clone c takes the BRMA at quantile (u_h + c/K) mod 1 in that order. It adds no rows and changes no weights or other columns.
  • uk_local_brma_enum_domain, a terminal release-blocking gate. It rechecks the rewritten candidate against the engine's brma enum. The assembled spine check stays as it is.
  • household_lineage_keys factors out the geography identity kernel's key loop, including main's CGT residential-split lineage. It is used both before and after expansion. The geographic keys are byte-identical to main's: a golden test holds 78 keys generated by main's own kernel.

The spine frs_brma stage, uk_spine.json, source_stages.json and the coverage manifest are unchanged. The national line keeps the spine draws.

Invariants (tested)

  1. Exact marginals. Each clone's BRMA has exactly its household's single-draw distribution, whatever the walk order.
  2. Support. A zero-probability or padded column is never chosen. This includes boundary quantiles: a quantile exactly on a cumulative boundary takes the next BRMA.
  3. Spacing. A household's K quantiles fall one in each interval of width 1/K. For any outcome monotone in the walk order, the clone mean is within range/K of its expectation.
  4. Identity. Uniforms depend only on lineage, never on row order. SPI, CGT and residential copies get distinct keys. Parents are recovered by the exact relation household_id = pre_id + c * id_multiplier in every expansion path.
  5. Conservation. Only brma changes, and every downstream reader of household brma depends on uk.full.brma.

Mutation checks killed:

  • the CDF comparison <= → <, in both the helper and the production assignment;
  • a fixed u_h;
  • dropping + c/K;
  • a wrong parent offset;
  • pooled counts instead of averaged normalized cells.

Verification

  • 5,979 engine-free tests pass: the data shard, gate battery, country spec, Logbook isolation and the whole UK engine-free directory. The UK-engine BRMA test also passes.
  • ruff check and ruff format --check pass, and so does tools/ci_test_plan.py verify.
  • Gate-battery, dense and certification digests were regenerated from the merged uk/gates.json with the live producer, and are mirrored in the data shard's lockstep fixtures.

What this does not show

Clone weights are still calibrated independently, and microcosm-calibrate has no weight-tie feature. In uk-data, calibrating split records independently shifted outcomes; grouped calibration fixed that (policyengine-uk-data#579). The variance gain here is shown at fixed weights only. Solved outcomes need a measured local build, and a per-household clone weight tie is the natural follow-up.

Companion: PolicyEngine/policyengine-uk-data#579, which splits private renters into k records with grouped calibration.

🤖 Generated with Claude Code

MaxGhenis and others added 11 commits October 14, 2026 03:25
The engine contract now checks prepare_brma_spread end to end: the engine's
LHA categories select the right census cells, the household distribution
averages normalized unit cells (not pooled counts), the rate order matches
lha_rate_keys, a single-BRMA region pads with an unselectable zero column,
and the lineage uniforms match stable_identity_uniforms. The UK gates
manifest lists the new local BRMA enum-domain gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The data shard's gate-battery mirrors still held the previous digests and
lacked uk_local_brma_enum_domain, so the lockstep test and the signed
end-to-end report fixture failed. Direct assign_clone_brmas cases pin the
boundary rule (a quantile on a cumulative boundary takes the next BRMA;
zero-probability columns are never chosen), which the production
comparison's <= to < mutant previously survived.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The terminal uk_local_brma_enum_domain entry passes in this unscoped probe,
where the enum_domain binding is stubbed, so the battery now passes 21 gates.

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

Copy link
Copy Markdown
Collaborator

@MaxGhenis this PR overlaps with #1305, now open as a draft. Both write household brma on the local clones, and both add the uk_local_brma_enum_domain gate.

Which do you want for this PR?

The wider questions on the direction are on PolicyEngine/policyengine-uk-data#521 (13 and 16 October).

🤖 Generated with Claude Code

@juaristi22

Copy link
Copy Markdown
Collaborator

Closing: #1305 merged today (a90eec3) and supersedes this PR on the local line in Great Britain.

  • BRMA. Each placed clone in Great Britain now takes the BRMA of its assigned area: the LSOA in England and Wales, the output area in Scotland (uk.full.geography.brma, --brma-localisation area). That node rewrites every Great Britain clone's BRMA, so a spread of the spine's draw would not reach them.
  • The gate. Place UK private rent and BRMA at the assigned area; bind Scotland's rents at BRMA grain (#1262, #1090) #1305 carries the same uk_local_brma_enum_domain terminal gate, so that part of this PR is on main.
  • Northern Ireland keeps the spine's frs_brma draw, as does the national line. If you want the spread there, a new PR off main could add a spread mode for Northern Ireland to the same node, beside none and area, and share the gate. This branch conflicts with main.

The questions on the direction are on PolicyEngine/policyengine-uk-data#521. The comment above asked whether to close this PR or keep it for Northern Ireland.

🤖 Generated with Claude Code

@juaristi22 juaristi22 closed this Oct 20, 2026
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