Port the UK stochastic layer as declarative source stages - #709
Conversation
f316312 to
ad3e538
Compare
Four stages appended after the FRS derived layer: frs_take_up (reported-anchored benunit take-up plus the clipped-normal extended-hours draw), frs_person_draws, frs_household_draws, and frs_brma (LHA_category through the rules-engine adapter, count-table BRMA sampling with an identity-keyed household collapse). Identity-keyed blake2b draws replace the incumbent's sequential streams as a standing signed-difference class; rates land in the cited build/uk take-up contract (UC 0.55 frozen per U8) and the VOA BRMA count table with its regenerator. Delivers the take_up_signal gate into the shared vocabulary plus a brma enum_domain declaration (both release-blocking), drops the dead higher_earner_tie_break from both export registers, pins the real policyengine-uk version into build records, adds declared_seeds and the contract sha to the driver sidecar, ships the household_is_ provenance-flag convention helper, the row-permutation identity and BRMA distribution acceptance instruments, and the seeded childcare fitting tool (offline, receipt-emitting). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The frs_brma stage declares its count table as a non-tab resource artifact; the driver's pin collector assumed every artifact was a raw-tab pin and failed on the first licensed run. Tab pins now skip resource entries, and resource references land in the sidecar as resource_pins carrying the country-package sha, fail-closed when the resource is undeclared. The synthetic driver spec mirrors the real manifest's resource artifact so the split stays exercised without licensed data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The tool's main() wrote a stub receipt ("requires caller-supplied
transform") without checking anything. It now recomputes every E4 column
from the pure derivations in original and permuted row order, un-permutes
by entity id, compares both against each other and against the stored
artifact columns, reports missing stored columns explicitly, and exits
nonzero on any mismatch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The distribution diagnostic compared household BRMA shares per region against the rents table's own regional margin — mixing our benunit LHA category composition with the table's lettings-weighted composition, which read as double-digit z-scores on a correct sampler. The comparison now re-derives the benunit-level assignment and checks each (region, LHA category) cell against its conditional distribution, reporting per-row z and the max |z|, with the same SDC masking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four PR-CI failures none of which the local build-shard run could see:
- test_contract.py holds hand-mirrored copies of the gate-battery pins that
microcosm-data cannot import from microcosm-build. E4 re-cut the triple in
contract.py but not the mirrors, and did not add its two entries to the
fixture's entry table, so the lockstep test and the end-to-end release
report both failed. Mirrors re-cut against the live producer
(test_gate_battery_contract_pins drives it), both entries added in
manifest order with fixture details mirroring what each gate emits.
- test_uk_brma_resource pinned the regenerator against a CSV path inside a
local session scratch directory, so it could only ever pass on the machine
that staged it. The staged input is now located via UK_LHA_RENTS_CSV and
the regeneration check skips when it is absent; the committed resource's
sha256, row count, and cell/BRMA counts stay pinned in CI by the
source-facts test. The regenerator tool takes the same override.
- The driver sidecar test called metadata.version("policyengine-uk")
directly, which raises in the engine-hermetic lane. It now resolves the
expected value the way the driver does, so it holds with or without the
engine installed and still fails on the "unknown" the U1 pin fix removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ad3e538 to
658b9f1
Compare
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Checked the same way as E2/E3 — kernel and every transform against the incumbent's code, rates against the incumbent's parameter files — and this is the cleanest of the three ports so far.
Verified faithful: assign_binary_with_anchored_residual reproduces assign_takeup_with_reported_anchors exactly (int(rate·n) full-population target, max(0, …) residual, adjusted rate over non-anchored rows, silent overshoot — all four quirks preserved and the int() floor test-pinned); the benunit anchor aggregation matches the incumbent's _reported_benunit_mask (any member with a positive reported amount); every rate in take_up_contract.json matches the incumbent YAMLs including the 2023-applicable selections (TFC at 0.586 with the 2024-04-06 0.88 correctly not selected, evasion at 0.1058, FTB at 0.384, property purchase 0.0385 with the HMRC/ONS derivation carried over); SCP banding, the clipped-normal parameters, and the BRMA count-table sampling (≡ the incumbent's uniform row-sampling over the pooled observations) all line up; the household collapse is a uniform member pick, same as the incumbent's sample(n=1) modulo the standing RNG class. The signed differences are drawn exactly where they should be — 3 and 4 in particular document incumbent quirks (evasion drawn independently despite the conditional YAML phrasing; the unweighted full-population take-up denominator) instead of silently "fixing" them, which is the right call for a parity-first port. higher_earner_tie_break's removal with an engine guard test is well-warranted. All the PR's test files pass for me with the uk extra.
The acceptance section deserves a specific word: reporting that the permutation-identity instrument's first version wrote a stub receipt without checking anything, and that an earlier BRMA margin draft produced misleading z-scores on a correct sampler — both rewritten before sign-off — is exactly the receipts culture working. An instrument that has never failed is an instrument you haven't tested; documenting the discarded drafts is what makes the surviving receipts credible.
Three observations, none blocking:
stable_identity_uniformsis a per-id Python loop over blake2b. Fine at FRS scale (~36k persons × ~17 columns), but this kernel is country-agnostic and E7's clone stack — and eventually populace-scale consumers — will multiply it into tens of millions of hash calls per build. Worth a note in the module (or an issue) that the loop is the known hot spot with a straightforward vectorization (hash into a bytes array,np.frombuffer), so the first person who hits it at scale isn't re-deriving the fix under pressure.- The
uk_take_up_signalgate's target for the three anchored columns is the raw contract rate, but the assignment targets an unweighted count with anchor overshoot accepted. If reporters are weight-skewed (plausible for pension credit), the weighted share can drift from the rate legitimately — today's margin is comfortable (max |Δ| 0.0073 against a 0.05 band), but when this gate eventually fails on an anchored column, the first diagnostic question will be "regression or weight-skew?". One sentence in the entry notes distinguishing the two would save that future debugging session. - The #706 merge-coordination recipe is clear and the entry edits are indeed disjoint — whoever lands second, the fingerprint recompute test makes the union mechanical. No concern.
…on the rebased battery Two review threads, one commit. vahid-ahmadi asked whether the shared input-mass relative_tolerance, minted at the worst surviving drift, leaves the fence too loose for the other 127 columns given that its edge column looks explained rather than noisy. The full comparison surface is now recorded in the receipts doc: 128 columns at median 0.24 / p90 0.63 against a 4.5218 fence, a smooth decay with no natural cut point, and the exclusions-versus-tolerance tradeoff (a 5-10x tightening needs 7 to 30 receipted columns, not the three-to-five the review sketched; conversely only four columns sit above 1.5, so the register would not bloat for a modest gain). Today exactly one top drift carries a documented explanation (#703's head-to-head receipt, expiring naturally at the E10 recomposition), and excluding it alone buys 1.48x. The tolerance therefore stays at the measured edge, with the limitation recorded in the entry notes rather than silent, and the tightening path tracked as follow-up work gated on evidence per column. The #709 rebase also aged the confirmation receipt: two new release-blocking entries (uk_take_up_signal, uk_brma_enum_domain) meant m5's 14-gate report no longer covered the battery. Re-run as m6 on the rebased branch: all 16 gates green (13 passed, the three parity gates evidence_absent, none failed), and m6's artifact is payload-identical to m5's — the rebase moved policy only, not one byte of built data. The build record is re-cut from m6 with the 16-gate signed report embedded. Refs #706, #709, #703, #630. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both from vahid's review of #709, neither a defect: - stable_identity_uniforms is a per-id Python loop. Measured ~1.8M ids/s: negligible at FRS spine scale (0.35s for 36k x 17 columns), ~15s for a country-agnostic caller at 1.6M x 17. The docstring now records the measurement and the vectorization path, with the warning that the key string and digest size must stay byte-identical or every committed draw moves. - The take-up gate compares a weighted share against a rate the assignment targets unweighted, so weight-skewed reporters can drift it without any regression. The triage step (check the unweighted share first) is in the gate's own docstring, where whoever reads the failure will land, rather than in the gates.json notes where it would move the battery fingerprints for a comment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Workstream E4 of the UK migration (#145, master epic #665). Closes #680. Stacked on #703 (E3); base flips to main when #703 merges.
What
Ports the incumbent stochastic layer as four declarative source stages appended after
frs_education_grant_split, replacing the incumbent's sequential RNG streams with identity-keyed draws (blake2b overseed:variable:entity_id, the US_stable_unit_drawsarithmetic) — the standing RNG signed-difference class; parity is weighted-distributional only, as UKDS disclosure rules require anyway:frs_take_up(benunit) — reported-receipt anchored take-up for child benefit / pension credit / universal credit (the incumbentassign_takeup_with_reported_anchorssemantics exactly: anchors certain,int(rate·n)unweighted full-population target, adjusted residual, overshoot accepted; anchors fail loud on a missing*_reportedsource column), plain Bernoulli take-up for opts-out/TFC/the three childcare programs, and the clipped-normal extended-childcare-hours draw (inverse-CDF).frs_person_draws(person) — marriage allowance, age-banded SCP (0.97 under 6 / 0.85 at 6+), and the persisted private-school uniform.frs_household_draws(household) — TV ownership, TV-licence evasion, first-time-buyer, property purchase.frs_brma(household) —LHA_categorymaterialized through the rules-engine adapter (consumed, never persisted), benunit-level inverse-CDF sampling from the committed VOA count table, household collapse by identity-keyed uniform member pick.Supporting: the country-agnostic kernel
microcosm/build/stochastic_assignment.py+ six generic operation kinds (assign_binary_with_anchored_residual,assign_binary_from_banded_rates,assign_uniform_draw,assign_clipped_normal,aggregate_person_to_benunit,sample_categorical_from_count_table); the cited rate contractbuild/uk/take_up_contract.json(status vocabulary incl.frozen_by_adjudicationfor UC 0.55 per U8/uk-data#452 andfitted_offlinewith receipts; loader refuses unreviewed rates; year≤build-year selection semantics);build/uk/brma_rent_counts.json(60 region×LHA-category cells, 200 BRMAs, 1,257,744 observations — count-weighted sampling ≡ the incumbent's uniform row-sampling incl. the unfiltered 2019+2020 pooling; regeneratortools/build_uk_brma_count_table.py; chronicle provenance registration pending); the delivered take-up gate (take_up_signaladded to the shared vocabulary;uk_take_up_signalrelease-blocking at ±0.05 weighted share vs contract targets — the universal-take-up constant-column landmine catcher) anduk_brma_enum_domain(release-blocking, engine enum domain); the U1 pin fix (rules_engine.versionnow real viaimportlib.metadata, was "unknown"); driver sidecardeclared_seeds+stochastic_contract_sha256; thehousehold_is_provenance-flag convention helper (flags themselves land in E7/E8); acceptance instrumentstools/verify_uk_identity_stability.py(row-permutation identity at full scale) andtools/emit_uk_brma_distribution.py; and the seeded childcare fitting tool porttools/fit_uk_childcare_takeup.py(explicitdefault_rng, local H5 input, receipt-emitting, not in the build — U4).UK-specificity note (required by #680)
Generic contributions: the stochastic-assignment kernel, the six operation kinds, and the
take_up_signalgate name live in shared modules/vocabularies, usable by any country. UK-specific: the twobuild/uk/resources (program rates and BRMA geography are UK), the fouruk_runtimetransforms, the gate declarations/bindings, and the childcare fitting tool (UK program semantics). The incumbent package name appears nowhere in the tree.Signed differences
seed:variable:entity_id) replaces the sequentialdefault_rng(100)/default_rng(0)streams. Parity is weighted-distributional only. Subsumes the extended-hours inverse-CDF and BRMA count-table variates (distributionally identical). Incumbent seeds (take-up 100, brma 0, property 0) recorded as documentation in the contract.higher_earner_tie_breakdropped (adjudicated with María): no consumer in policyengine-uk (the deterministic age tie-break replaced it), absent from both parity instruments; removed from both export registers; a guard test asserts the engine still has no such variable.household_owns_tvalthough the incumbent YAML phrases the rate conditionally; the reference share 0.105666 matches the independent draw at the 2023-applicable 0.1058 rate — ported as-is.int(rate × all units), unweighted, full entity population — not the "share of eligible" the incumbent YAML descriptions claim; overshoot when reporters exceed the target accepted silently — ported as-is, recorded in the contract policy string; theint()floor is test-pinned.Licensed-data acceptance (all data stayed local; disclosure-safe aggregates only)
compare_uk_h5_payload.pyverdict payload-identical; shares files byte-identical; both attempts landed hash-chained Logbook rows. A first attempt failed at the configured phase on the driver's pin collector (it assumed every stage artifact was a raw-tab pin and hitfrs_brma's resource artifact) — fixed with resource references split into a sidecarresource_pinskey, receipted inacceptance_receipt.json.permutation_identity_receipt.json, seed 123). The instrument's first version wrote a stub receipt without checking anything; rewritten to verify before sign-off.would_claim_targeted_childcare); exact 1.0 forbrmaandattends_private_school_random_draw.uk_take_up_signal±0.05 band.acceptance_checks.json.brmahas no weighted-register row by design; this is its acceptance instrument.rules_engine.version: 2.89.0(the U1 pin, formerly "unknown"),declared_seedsper stage/output,stochastic_contract_sha256, andresource_pins.Merge coordination with #706
#706 (the #630 close-out) and this PR both edit
uk/gates.jsonand re-cut the_UK_GATE_BATTERY_*fingerprints inmicrocosm-data/contract.py. The entry edits are disjoint (this PR addsuk_take_up_signal/uk_brma_enum_domain+ the tie-break export removal; #706 rewrites the weight-ratio/input-mass/tail-concentration parameters). Whoever lands second: union the entries, take this PR's take-up sentence + #706's weighted-integrity sentence in the policy string, and re-run the fingerprint recompute (test_gate_battery_contract_pinsdrives it). Recipe agreed between sessions on 2026-08-17.Implementation notes
Codex implemented under
/codex-implement-plan; one recorded deviation, reviewed and kept: the BRMA enum-domain evidence falls back to a frame-derived domain only when the injected engine exposes no_variable("brma")(engine-less test doubles); the production adapter exposes it, and the gate evaluator fails closed when neither source resolves. Post-review fixes on top: the plan-declarednonnegative_outputsforattends_private_school_random_drawrestored with its lockstep constant/test; take-up anchor aggregation made fail-loud on a missing*_reportedsource column (with test); the BRMA household collapse rewritten from an O(households×benunits) scan to a groupby (identical semantics). The licensed run then surfaced three defects PR CI could not see, each fixed and test-covered: the driver's pin collector crashing onfrs_brma's non-tab resource artifact (now split into sidecarresource_pins, with the synthetic driver spec mirroring the real artifact so the path stays exercised); the identity-stability instrument writing a stub receipt instead of verifying (now recomputes, permutes, and compares against stored columns, exiting nonzero on mismatch); and the BRMA distribution diagnostic comparing at a misleading region margin (now the native cell margin with per-row z).🤖 Generated with Claude Code