The scale ladder: shared frame_sampling core + the UK national build consumer (#627) - #656
Conversation
The seeded whole-household sampler, mass renormalization, and receipt shape move from us_runtime.stacked_spine into country-agnostic microcosm.build.frame_sampling, following the engine_tables promotion-over-third-copy precedent. The US call sites keep their schema pin and pre-assembly guard in a thin wrapper and delegate the draw; the default path is bit-identical (same RNG stream, same receipt fields — pinned by digests captured from the pre-promotion sampler and reproduced through a UK-shaped schema, proving schema independence). Country policy enters as parameters, not forks: an optional sampling unit key (a drawn unit brings all its household rows — the clone-family contract the UK consumer needs), per-unit strata with proportional per-group floors (monotone, so quota inequalities survive sampling), and forced retentions unioned after the draw so the RNG stream is untouched, receipted as such. Refs #627, #624, #612. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sample_uk_national_frame is the UK policy over the shared frame_sampling core, shaped by what the compact actually is — a mid-pipeline artifact with baked-in structure. The sampling unit is the canonical clone family (clone reversal always lands on a surviving clone_index=0 row); the families carrying the argmax canonical household and person ids are force-retained so the re-derived clone multiplier keeps its digit count; the draw is stratified by the family-level channel flags and the canonical row's region, whose monotone per-group floors preserve the SPI rebuild's per-cell base>=dead quota — re-asserted post-sample and failing closed so the uniform-clone-structure assumption is a runtime receipt, not a hope. Sampled mass renormalizes to the full-source total (the kernel mints the record) and the exported household_weight column is refreshed in place, so the result satisfies validate_uk_national_frame. The build seam samples after the loader and before provenance binding — the certified-candidate fence attests the frame the stages actually consume — and never invokes the sampler at fraction 1.0, so full-scale builds are structurally unchanged. The driver adds the rung-validated --sample-fraction and a separate --sample-seed (dev sweeps vary one draw at a time), pins both plus the rung token into the checkpointed run config as strings (two rungs on one checkpoint directory refuse instead of cross-resuming), carries the receipt in the build record and stdout payload, and refuses a canonical release id below 1.0 — rung artifacts are receipts, never releases, until the gate battery's release_candidate wiring takes over at the #611 consumer half. Refs #627, #624, #630, #612. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…627) The first credentialed 1% smoke run failed stage one's lineage fence: "Candidate SPI/capital-gains person IDs do not reverse to the raw FRS surface." The fence has a second reversal layer the clone-family unit did not model — SPI and CG rows reverse to raw FRS rows through max-derived offsets (max(canonical raw id)+1, max(canonical pre-CG id)+1) — so a clone-family draw could keep a derivative while dropping the raw row it reverses to, and dropping a single argmax row shifts an offset and breaks every reversal at once. The unit is now the source FRS family: the raw canonical household plus every SPI/CG derivative and every geography clone that reverses onto it through both arithmetics. Forced retention pins all six argmax ids (canonical/raw/pre-CG x household/person), so the multiplier and both offsets are reproduced exactly. Strata reduce to the raw canonical region — channel flags vary within a family now — and the per-cell SPI quota becomes structurally preserved (a dead row's in-cell source is in its own family), with the post-sample check kept as the fail-closed backstop. The smoke failure is pinned as a regression test. Refs #627, #624. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corrected 1% smoke run cleared the lineage reversal and hit the next fence: retain_uk_frs_hmrc_leaves requires every raw-survey person to exist in the candidate base — the source-faithfulness completeness proof — which a rung sample structurally cannot satisfy (99% of the raw surface is deliberately absent at 1%). The stage now accepts an explicit sampled_rung declaration, threaded from the driver's --sample-fraction: the raw surface is restricted to the surviving canonicals, the dropped count is receipted in the lineage evidence (source_people_outside_candidate, zero on every full-scale build), and the strict raise is unchanged when the flag is absent — a release build cannot relax the fence by accident, and a rung build cannot relax it silently. Refs #627, #624. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
) The third smoke attempt cleared both stage-one fences and hit the mid-stage effective-mass floor: at 1% the sparse charitable_investment_gifts column restored ~3e-7 effective mass. On a declared rung the floor defers to the terminal input-coverage gate — the same surface, evaluated with a receipted verdict — instead of aborting mid-stage; the per-column shares reach the replay report either way, and the strict raise is unchanged without the declaration. Refs #627, #624. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner adjudication on #657 (closed without pipeline changes): the singleton-class refusal inside the SPI imputation's stratified split is an accepted dev-scale statistical edge — the computation is never altered to avoid it, and the remedy is re-rolling --seed. What the rung owes is transparency: instead of a bare traceback, a sub-1.0 build that hits the named edge writes a *.rung_abort.json receipt (edge name, stage, rung token, seeds, error text, remedy) and exits with a distinct code. The match is deliberately narrow — only the named signature, only on a declared rung; unknown exceptions and full-scale builds crash exactly as before, so the receipt path can never absorb a real defect. Refs #657, #627, #624. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… sampling (#627) Three independent adversarial reviewers attacked the branch; every finding was re-verified before fixing. Landed here: - A stale *.rung_abort.json no longer survives a later successful build at the same staging path (the one confirmed real bug): the receipt is unlinked with the other sidecars, derived once, and registered in the pairwise-distinct path fence so it can never overwrite a read-only gate input. - The non-releasability fence now mirrors the microcosm-data contract it cites: year and count widths use [1-9][0-9]*, and the tier alternation is built from the ratified UK_RELEASE_TIERS, so a fat-fingered year or a future tier cannot slip a canonical-shaped id onto a rung build. - A checkpointed rung build requires the sampling identity inside run_config at the library level, not just in the driver — two rungs pointed at one checkpoint directory refuse even for programmatic callers whose only stage is fully checkpointed. - The sampler-vs-fence contract is now integration-tested against the real _resolve_candidate_lineage: the sampling fixture's person ids satisfy the fence's person//1000==household invariant, and sampled frames must resolve with the full frame's exact multiplier and person-level SPI/CG offsets — the drift class that broke the first credentialed rung run can no longer pass CI. - frame_sampling hardening: unit ids must be integer-typed (fractional ids would collide in the selection digest), a strata-only call emits the honest sampling_unit block instead of a requested_household_count that contradicts the declared exact-count rule, and the core reads the person entity and membership column from the schema instead of hardcoding the names. - Rung evidence stays a source fact: source_signal_rows (and therefore structural_zero) are computed from the unrestricted raw survey surface, so a rung can never assert a structural zero from a sampled-away population; the sampler's fail-closed docstring now states its household-level scope. Recorded as accepted, not fixed: per-stratum floors can zero small strata (receipted in the strata block); a rung cannot distinguish a compact missing raw people from sampling loss (the full-scale fence stays strict); the terminal gate report does not yet carry the rung — that field belongs to the #611 schema-4 battery report at the UK migration. Refs #627, #624, #630, #611. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Adversarial review completed — three independent skeptical reviewers (shared-core behavior preservation; UK policy vs the real fences; driver/fences/receipts), every finding re-verified before acting. Fixes landed as Confirmed and fixed:
Verified and accepted (recorded, not fixed):
Claims that survived attack (for reviewer confidence): the default draw is bit-for-bit the pre-refactor US draw (200-trial fuzz, byte-identical receipts); US manifests/identities unchanged; 900 seeded UK draws pass the real fence with exact reversal constants; the six forced argmax pins are sufficient; the per-cell SPI quota preservation is structural for pipeline-built compacts (regions are family-constant by construction); the in-place 🤖 Generated with Claude Code |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
The promotion is well done. Parameterising country policy (unit_ids, unit_strata, forced_unit_ids) instead of forking, and pinning the promoted US RNG stream with captured digests in _PINNED_DRAWS, is exactly the right way to move a sampler between pipelines — the pin is what makes "same seed, same draw" a claim rather than a hope. The two invariants in the module docstring (whole lineages, full-selection-is-a-no-op) are stated crisply and both are actually enforced in code.
uk_source_family_units is the part I'd want a second pair of eyes on, and it mostly holds up: re-deriving the fence's arithmetic and then forcing retention of the argmax-carrying families so the multiplier's digit count and both offsets stay stable is a genuinely clever solve. Refusing pre-draw on inputs the stage fence would reject is the right place for that check.
Notes, roughly in order of how much they matter.
1. sampled.table("household")["household_weight"] = ... assumes table() returns a live reference.
sampled.table("household")["household_weight"] = sampled.weights_for("household").valuesIf Frame.table() ever returns a copy (or a defensive view), this assignment succeeds and does nothing, and the exported column silently keeps its pre-normalisation values while the typed weights are correct. The comment explains why it's done in place (column position, and therefore staging payload column order), which is a good reason — but the correctness depends on an invariant of Frame that isn't visible here.
Worth either asserting it right after (assert np.allclose(sampled.table("household")["household_weight"], ...)) or exposing a Frame method that refreshes an exported column in place. This is the one place in the PR where a silent wrong answer is possible rather than a loud failure.
2. normalize_sampled_household_mass validates the divisor but not the target.
if not np.isfinite(sampled_mass) or sampled_mass <= 0.0:
raise ValueError(...)
factor = float(target_mass / sampled_mass)target_mass gets no such check, so a NaN or non-positive target produces a NaN/negative factor that propagates into the weights and the MassChange record. The caller currently passes receipt["incoming_household_mass"], which is trustworthy — but the function is exported and country-agnostic. Same guard on both sides would be cheap.
3. The floors-to-zero refusal ignores forced units.
requested_units < 1 raises before forced_unit_ids is considered, so a configuration with forced retention that would guarantee a non-empty sample still fails closed. For the UK path forced is always non-empty (six argmax pins), so at very small fractions on a small frame the error message — "sample fraction floors to zero source families" — will be true of the draw but not of the realised sample. Might be intended; if so a sentence in the docstring would save someone the confusion.
4. Dead check in _aligned_int_array.
if not np.issubdtype(array.dtype, np.integer):
raise ...
...
if pd.isna(array).any():
raise ValueError(f"{source_name} {label} must not contain missing values.")By the time the second check runs the dtype is guaranteed integer, so pd.isna is always all-False. Harmless, but it reads as protection that isn't there. (The equivalent check in _int_column in national_sampling.py runs before the int cast, so it does bite — the asymmetry is what makes this one look intentional.)
5. Smaller things
stratalabels are built asf"region={region_by_household[int(unit)]}". If a region code ever arrives as1in one build and1.0in another, those are different strata with no error._int_column-style coercion on the region column would remove the ambiguity.receipt["strata"]reportsrequested_unitsper group, but forced inclusions can pushrealized_unitsabove it. That's honest givenforced_unit_inclusionssits alongside, but a reader diffing the two blocks will trip over it once.- The comment at
frame_sampling.py:305-313explaining whyrequested_household_countis omitted under strata (per-group floors need not sum to the global floor, so emitting it would contradictEXACT_COUNT_RULE) is excellent — that's the kind of thing that gets silently "fixed" back in six months. Good that it names the adversarial-review finding.
6. Process
This is still marked draft, and #658 is stacked on top of it (uk-source-year-exclusion-630 → uk-scale-ladder-627), so #658 is blocked until this lands. If the draft status is stale it'd be worth flipping it so the stack can move.
…bels (#627) Addresses vahid-ahmadi's review of #656: - The in-place household_weight refresh now verifies it persisted and fails closed naming its cause, instead of relying on validate_uk_national_frame two calls later to catch a Frame.table that stopped returning live references (the one spot a silent wrong answer was structurally possible). - normalize_sampled_household_mass guards target_mass the same way it guards the divisor; the function is exported and country-agnostic, so both sides of the ratio fail closed. - Region strata labels are restricted to non-empty strings, fail-closed: the compact's region column is string-coded (LONDON, SCOTLAND, ...), and refusing numerics removes the 1-vs-1.0 two-strata ambiguity entirely rather than papering over it with formatting. - Per-stratum receipts gain added_beyond_draw so realized = requested + added_beyond_draw reads directly instead of needing reconciliation against the global forced_unit_inclusions block. - The dead pd.isna check in _aligned_int_array (unreachable after the integer-dtype guard) is removed; the floors-to-zero refusal documents that forced units are deliberately not counted — they are identity pins, not a sample. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks @vahid-ahmadi — all six points addressed in fe94809, in your order: 1. Live-reference assumption on the weight-column refresh — fixed, with one nuance. A silent wrong answer was actually already impossible two calls later: 2. 3. Floors-to-zero ignoring forced units — intended, now documented. Forced units are identity pins (they exist to keep the fence's reversal constants derivable), not a sample; a fraction whose proportional draw floors to zero has no representative content regardless of what forced retention would realize, so it stays fail-closed. The docstring now says exactly that. 4. Dead 5a. Region label ambiguity — fixed, but not with int coercion. I checked the real compact: 5b. 6. Process — the PR is out of draft, and #658 is rebased onto the new head. |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
fe94809 addresses all six. Verified each against the diff rather than the summary:
- Weight refresh — the local
np.array_equalcheck right at the assignment site, with an error namingFrame.tableas the cause.test_weight_column_refresh_verifies_the_live_referencemonkeypatchesFrame.tableto return copies and asserts the refusal fires; suppressing the pandasChainedAssignmentErrorwith a comment saying the warning is the failure mode is a nice touch. Your point thatvalidate_uk_national_framealready made a silent wrong answer impossible is fair — the value here is that the failure is now local and named, and survives a refactor that reorders the revalidation. target_mass— same finite-and-positive guard on both sides.- Floors-to-zero — accepted, and the docstring reason (forced units are identity pins, not sample content) is the right justification. Good that it's written down rather than left as behaviour.
- Dead
pd.isna— gone.
5a. Region labels —_str_columnenforcing non-empty strings is a better answer than the int coercion I suggested. Refusing a numeric region code outright removes the1/1.0ambiguity instead of papering over it, and checking the real compact first was the right instinct.
5b.added_beyond_drawper stratum —realized = requested + added_beyond_drawnow reads in place, with the per-group and global-sum identity both asserted. - Out of draft, #658 rebased.
No new findings. This looks ready to me.
S1 of the #627 scale-ladder lane, in two commits: the shared sampling core promoted out of the US stacked pipeline, then the UK national build consuming it. Together they deliver #627's ask — an identity-bound
sample_fractionin the UK national build with US semantics, rungs f001/f010/f100, and "boring before billed" (#624) wiring.Commit 1 — promote the sampling core into
microcosm.build.frame_samplingus_runtime.stacked_spine(theengine_tablespromotion-over-third-copy precedent). The US wrapper keeps its schema pin, pre-assembly guard, and exact error messages, and delegates the draw.test_frame_sampling.pyand reproduce exactly — same RNG stream, same receipt fields in the same order — through a UK-shaped schema, proving schema independence at the same time. The full US stacked-spine suite passes unchanged; receipt extensions (sampling_unit/strata/forced_unit_inclusions) appear only when the feature is exercised, so US checkpoint identities and manifests are untouched.Commit 2 — the UK consumer, shaped by what the compact actually is
The certified compact is a mid-pipeline artifact with baked-in structure, so the UK policy (
uk_runtime/national_sampling.py):clone_index=0row —frs_hmrc_leavesfence);#base ≥ #deadquota (spi_support._sample_replacement_household_ids) to survive deterministically; a post-sample quota check fails closed, turning the uniform-clone-structure assumption into a runtime receipt;MassChangeRecord) and refreshes the exportedhousehold_weightcolumn in place, so the result satisfiesvalidate_uk_national_frame.Build seam: sampling runs after
load_uk_national_frameand before provenance binding — the certified-candidate fence attests the frame the stages actually consume. Atfraction=1.0the sampler is never invoked: full-scale builds are structurally byte-invariant (asserted).Driver: rung-validated
--sample-fraction+ separate--sample-seed(dev sweeps vary one draw at a time;--seedkeeps the SPI replacement draw); fraction/seed/rung pinned into the checkpointedrun_configas strings (exact-equality canonical JSON — two rungs on one checkpoint dir refuse instead of cross-resuming); the receipt rides the build record and stdout payload; and a sampled build refuses a canonical release id — rung artifacts are receipts, never releases. That interim fence stands until the gate battery'srelease_candidatewiring takes over at the #611 consumer half (the durable, country-agnostic home; no production caller passes it today in either country).What this unblocks
Fences
stacked_spine.py— land this first or rebase trivially. File-disjoint from the A1 lane (PR UK consumer half of the gate battery: uk/gates.json, bindings, and the contract doc (#611 increment 1) #655).national_build.py's gate block; this PR's insertion sits upstream of it and does not touch the gate files.Refs #627, #624, #630, #612, #654.
🤖 Generated with Claude Code