Skip to content

UK dataset sizes: informed L0 and exact-count candidates (#355) - #877

Merged
juaristi22 merged 33 commits into
mainfrom
uk-dataset-sizes-355
Sep 10, 2026
Merged

UK dataset sizes: informed L0 and exact-count candidates (#355)#877
juaristi22 merged 33 commits into
mainfrom
uk-dataset-sizes-355

Conversation

@juaristi22

@juaristi22 juaristi22 commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

The UK rowwise driver currently exports every cloned household. This adds --dataset-households to request a compact candidate after the dense solve, using contribution-informed L0, exact-count sampling, and a frozen-support refit, plus the machinery a licensed 55,000-household run turned out to need and the evaluation that measures such a candidate.

Based on main (after #883). Refs #355 and #665; this does not close #355's certification and default-switch requirements.

Behavior

  • Keep pool generation separate from output size: the existing K=15, grain_equal, 1,500-epoch defaults and local gates remain; --epochs is free outside --release-candidate.
  • Initialize L0 using target-contribution shares and protect each nonzero target's largest absolute weighted carrier; refuse by name any nonzero target no pool household supports.
  • Search the L0 penalty on the gates' open-probability mass and stop only on a probe whose gate probabilities admit the exact-count draw at the requested certainty threshold (calibrate(..., budget_basis="open_probability_mass", feasible_draw_pi_hi=h); exact_k_design_feasibility is the draw's own inequality). Every probe and the stop reason ride the size receipt.
  • Draw with the shared exact-count Sampford sampler at --selection-pi-hi (default 1.0; candidate-only) and refit from the normalized Horvitz–Thompson baseline with the stretch multiplier 10 on that inclusion-adjusted reference; the manifest records the reference. A refused draw carries the measured gate mass and the feasible alternatives.
  • Checkpoint the dense solve and the search before the draw (size_selection_checkpoint.{npz,json} in --out; --no-size-checkpoint opts out); --resume-size-checkpoint DIR re-derives and verifies the pool and target surface, rebuilds both results through rebuild_calibration_result, and continues at the draw, at any threshold.
  • Freeze full-pool target contributions, preserve linked households, benefit units and people, rerun selection inside each holdout fold; --selection-seed re-draws on one pool and one dense reference.
  • Export the dense reference the selection was cut from (dense_reference_diagnostics.csv, a summary under solve.dataset_size.dense_reference) and the selection itself (dataset_size_selection.csv).
  • Report solver progress (a loss line every hundred epochs, one line per budget probe with its verdict, the search stop) through the calibrator's callback seam.
  • Evaluate a size run with one command, tools/evaluate_uk_dataset_size.py (library uk_runtime/size_evaluation.py): run acceptance, size-only deltas against the run's dense reference, comparison with a reference run, incumbent surface re-resolution with the frozen-versus-recomputed check, downstream T3/T4/T5 and the UK scorecard: populace_uk_2023 vs enhanced_frs_2024_25 vs admin benchmarks (2026) #731 scorecard, a summary with the pre-registered outcomes, every step with a receipt.
  • Two register changes met on the way: the PIPR private-rent price level is composed into a linear total (12 × mean monthly rent × the A17-uprated private-renter count, receipted per authority), and two zero-support UC payment bands are signed A16 exclusions (tracking WS-C deferred adjudications and the reconciliation layer #736).

Measured

Two licensed 55,000-household candidates from the K=15 pool on spine-p and feed 6fb700e (thresholds 0.5 and 0.95, 2,000 epochs) and the refusal that preceded them are in experiments/355-uk-dataset-size-receipts.md, with the evaluation trees under each run; the summary is in the PR comment below.

Review boundary

These are candidate artifacts. Size requests refuse --release-candidate and remain unreleasable until matched comparisons and the promotion scorecard are adjudicated. No dataset registry, production pointer or downstream default changes. Full builds still require the dense build's peak memory and add selection/refit work; savings apply to the exported dataset and downstream use. The operating instructions are in docs/uk-dataset-size-plan-355.md.

Validation

  • Calibrate suite 236; UK rowwise runtime, driver, evaluation and gate battery 100; registers and ledger 84; spec-engine identities and graph parity 58; US multispine pool tool 187; engine re-run on policyengine-uk 2.97.0 120 — all green on the rebased tree.
  • Repository-wide Ruff check, changed-file formatting and CI test inventory verification pass; the attested calibrate identities (country bundles, seed protocol and map, loader golden vector, US spec digest, coverage report, H1 calibrate parity fixture, the best-iterate oracle's gates attestation) are re-cut.

@juaristi22
juaristi22 force-pushed the uk-rowwise-candidate-762-b branch 2 times, most recently from d5f04a8 to 3516100 Compare September 7, 2026 21:33
@juaristi22
juaristi22 changed the base branch from uk-rowwise-candidate-762-b to main September 8, 2026 10:36
@juaristi22
juaristi22 force-pushed the uk-dataset-sizes-355 branch 2 times, most recently from d4043ec to 573c4b4 Compare September 8, 2026 13:15
juaristi22 added a commit that referenced this pull request Sep 8, 2026
…evaluate_uk_dataset_size steps 00-90, uk_fit_by_family lifted (#355)

Every evaluation piece for a --dataset-households candidate now has one
name and one place:

- uk_runtime/size_evaluation.py — pure measurement over a run directory:
  load_run (tolerant of pre-#877 dense runs), run_acceptance (the size-run
  checklist, not_applicable on dense runs), fit_tables (by grain and
  family), weight_tables (Kish ESS, distinct sources, stretch vs the
  Horvitz–Thompson baseline and vs pool design from the selection sidecar
  or a spine, self-checked against the manifest), area_support_tables
  (per-grain floors and breaches), gate_table (six ids with criticality),
  paired_targets (join on name; wins/ties/losses; the reference's red rows
  tracked through family/area/metric), dense_reference_deltas (size-only
  effect from the run's own dense reference), frozen_vs_recomputed
  (national rows against the incumbent-surface evaluator), footprint, and
  summarize with PRE_REGISTERED_OUTCOMES_V1 flags. No subprocess, no
  engine, no licensed data in tests.
- tools/evaluate_uk_dataset_size.py — the one command: steps
  00-run-acceptance, 10-dense-reference, 20-vs-reference/<label>,
  30-incumbent-score (score_uk_local_candidate), 40-incumbent-surface
  (evaluate_uk_incumbent_surface --engine-blocks 1 + frozen vs
  recomputed), 50-downstream (uk-candidate-eval T3/T4/T5 and the #731
  scorecard under this venv, each timed with /usr/bin/time -l), 90-summary
  (EVALUATION.md + JSON with the pre-registered table and a file index);
  every step writes receipt.json (inputs and outputs with digests, argv,
  wall, exit) and is skipped with a reason when its inputs are absent;
  re-running a step rewrites only its folder.
- _fit_by_family lifted from the driver into
  uk_runtime/diagnostics.py::uk_fit_by_family unchanged; the driver
  imports it.

Tests: test_uk_size_evaluation.py (synthetic run dirs in both manifest
shapes, table-format H5s, time -l parsing, red-row mapping, stretch
self-check, breaches, flags, refusals) and
test_uk_evaluate_dataset_size_tool.py (tree, receipts, manifest, summary;
skipped steps; a failing downstream stub recorded without aborting;
single-step re-run). Implemented by Codex from .codex-work/PLAN.md;
verified here: ruff, format, ci_test_groups --verify, 88 tests across the
four UK rowwise/evaluation files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Sep 9, 2026
…evaluate_uk_dataset_size steps 00-90, uk_fit_by_family lifted (#355)

Every evaluation piece for a --dataset-households candidate now has one
name and one place:

- uk_runtime/size_evaluation.py — pure measurement over a run directory:
  load_run (tolerant of pre-#877 dense runs), run_acceptance (the size-run
  checklist, not_applicable on dense runs), fit_tables (by grain and
  family), weight_tables (Kish ESS, distinct sources, stretch vs the
  Horvitz–Thompson baseline and vs pool design from the selection sidecar
  or a spine, self-checked against the manifest), area_support_tables
  (per-grain floors and breaches), gate_table (six ids with criticality),
  paired_targets (join on name; wins/ties/losses; the reference's red rows
  tracked through family/area/metric), dense_reference_deltas (size-only
  effect from the run's own dense reference), frozen_vs_recomputed
  (national rows against the incumbent-surface evaluator), footprint, and
  summarize with PRE_REGISTERED_OUTCOMES_V1 flags. No subprocess, no
  engine, no licensed data in tests.
- tools/evaluate_uk_dataset_size.py — the one command: steps
  00-run-acceptance, 10-dense-reference, 20-vs-reference/<label>,
  30-incumbent-score (score_uk_local_candidate), 40-incumbent-surface
  (evaluate_uk_incumbent_surface --engine-blocks 1 + frozen vs
  recomputed), 50-downstream (uk-candidate-eval T3/T4/T5 and the #731
  scorecard under this venv, each timed with /usr/bin/time -l), 90-summary
  (EVALUATION.md + JSON with the pre-registered table and a file index);
  every step writes receipt.json (inputs and outputs with digests, argv,
  wall, exit) and is skipped with a reason when its inputs are absent;
  re-running a step rewrites only its folder.
- _fit_by_family lifted from the driver into
  uk_runtime/diagnostics.py::uk_fit_by_family unchanged; the driver
  imports it.

Tests: test_uk_size_evaluation.py (synthetic run dirs in both manifest
shapes, table-format H5s, time -l parsing, red-row mapping, stretch
self-check, breaches, flags, refusals) and
test_uk_evaluate_dataset_size_tool.py (tree, receipts, manifest, summary;
skipped steps; a failing downstream stub recorded without aborting;
single-step re-run). Implemented by Codex from .codex-work/PLAN.md;
verified here: ruff, format, ci_test_groups --verify, 88 tests across the
four UK rowwise/evaluation files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22
juaristi22 marked this pull request as ready for review September 9, 2026 13:53

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review pass at 37f81933 (Claude Code, high effort; worktree checkout; without the engine: 1,048 passed, 24 skipped, 10 failed — the ten are test_us_multispine_pool_tool needing the US extra, identical on main; ruff check and ruff format clean on all 29 changed Python files; CI green apart from one wheel job unreported).

The default solver paths are byte-identical, and that was checked rather than read

This is the second UK PR in a week to touch the generic solver, so I ran the same synthetic n=300 problem against main's and the PR's microcosm.calibrate in one venv: the count-basis L0 search with target_records, the same under mass="conserve", a fixed l0_lambda, and Sampford select_exact_k plus refit_l0_selection — weights, gate probabilities, support, q, trajectories and losses all hash-identical. Reading confirms why: steer() reduces to the old bisection when the verdict is not_required, and settled() reduces to the old tolerance test without feasible_draw_pi_hi. The US exact-K ladder's tests pass with no fixture regeneration. The parity fixture's direct.csv is unchanged and only the pin moved, asserted bitwise on every platform.

The statistics are right: units with π at or above pi_hi form a certainty stratum, boundary q is m·π/Σπ with a strict cap at one, and exact_k_design_feasibility is the draw's own inequality, property-tested against the draw across all four verdicts. Protected carriers get π exactly one via torch.where on the mask, so forced inclusion lands in the certainty stratum and does not break the design, and the refit asserts none was lost. Refusals carry numbers (the full feasibility scan) rather than errors, the unsupported targets are refused by name — and found exactly the two UC bands this PR then signs — and the resume path verifies arrays, identity keys, household ids, the target surface, initial weights and both closing losses. --release-candidate refuses --dataset-households, the manifest's releasable is false whenever a size is requested, and no registry or pointer moves.

Findings

1. Should-fix — doctrine drift is undetectable on resume. _size_checkpoint_identity (build_uk_rowwise_candidate.py:489-520) carries pins, seeds, epochs and sample settings but no code pin and no doctrine constants; load_uk_size_checkpoint rebuilds with the checkpoint's own target_loss_cap and options, the refit takes max_weight_ratio and mass from the dense solve's options, and the closing-loss check uses the stored cap. If UK_LOCAL_MAX_WEIGHT_RATIO or UK_LOCAL_TARGET_LOSS_CAP change between checkpoint and resume, the refit runs under the old bound while the manifest declares the new one. Put the existing code_pin and the doctrine values into the identity, or assert the restored options against current doctrine after the load.

2. Should-fix — a stale checkpoint in --out is refused only after the dense solve. write_uk_size_checkpoint refuses to overwrite (size_checkpoint.py:135-137), correctly, but it runs after the solve and the search, and _output_paths' pre-solve existence guard does not cover the checkpoint files. A re-run into a directory left by a refused run loses the multi-hour solve at the write. Add the two files to the pre-solve guard.

3. Should-fix — no frozen control covers gates.py, and it sits outside the attested kernel sources. The pre_best_iterate oracle pins the gate module's sha to the current file and imports HardConcrete from it on both sides, so it cannot detect a gate-behaviour change; this PR re-cut that sha. My A/B run is the only evidence of byte identity. Separately, the seed-protocol implementation digest hashes calibrate.solve and calibrate.exact_k only (seeds.py:348-349), so gates.py and the new initialization.py — which now shape L0 behaviour — do not move the attested identity when they change. A frozen-gates control and adding both modules to that list close it.

4. Question — say plainly how deterministic the "draw" was on the licensed runs. The receipts show the protected carriers as certainties at pi_hi = 1 and, at the lower thresholds used for the two 55,000 candidates, a large majority of the selection landing in the certainty stratum with only the tail actually drawn. That is by design — pi_hi is doing the selection — but the body's "exact-count Sampford draw" reads as a random design when it is mostly a threshold on learned π. Reporting the certainty share per run in the body would make the claim honest at a glance. Related: the HT refit baseline is design weights over q projected to design mass, and the stretch multiplier 10 is relative to that baseline, not the dense doctrine's ten-times-design; the plan doc says so and lists it for adjudication, but realized_max_weight_ratio_vs_design is now computed against solve.initial_weights, which on a size run is the HT baseline, so the key name misleads.

5. Question — zero-valued targets in the prior. contribution_initialization uses a unit denominator when the target is zero (initialization.py:327), so raw pound contributions saturate the prior at 0.9. The local parity fixture holds thousands of zero-valued rows (Welsh and Scottish private-rent amounts and renter counts); if any survive onto the compiled surface they dominate the prior. A zero_target_rows count in the size receipt would answer it.

6. Question — two register changes ride along and move the dense surface for every joint build. The PIPR private-rent composition into an annual total (12 × mean monthly rent × the uprated private-renter count, ledger_targets.py:1496) is unconditional, and the two zero-support UC bands become A16 exclusions expiring 2026-12-08 (census counts 49 → 51, 84 → 82). Both are reasonable and both affect comparability with #870's R17 receipts; they deserve their own line in the body, or a split.

Nits

The checkpoint manifest's written_at and absolute directory ride into the candidate manifest (excluded from the identity digest, so harmless, but manifests of identical runs will not byte-match). The parity pin's authoring platform moved from py3.13 to py3.14. Every calibrate() caller's options gains four new keys, which changes the shape of any manifest that embeds them.

Needs the licensed data

Every measured claim (the two 55k candidates, their ESS, the 25% area-support breach, 211 of 650 constituencies below the ESS floor), the frozen_vs_recomputed step — which the receipts say was not measured on either candidate and is the actual test of freezing population-dependent measures from the full pool — any holdout for a size run (all used --skip-holdout), and the incumbent leg of the #731 scorecard, which did not run.


No blocking defects; the machinery is careful and the receipts document the failures honestly. 1 through 3 are the ones I would land before this is used to cut a candidate anyone compares: a resume that can silently run under the wrong bound, a checkpoint that can be lost after the expensive step, and a solver module that is neither controlled nor attested.

juaristi22 and others added 19 commits September 9, 2026 18:59
…ection-seed moves only the draw (#355)

A --dataset-households run replaces its calibration product with the
compact refit, but the dense joint solve it was cut from is byte-identical
to a standalone dense run on the same inputs, seed and epochs. Keeping it
lets a size candidate be compared with its own dense reference without a
second full-pool run:

- local_rowwise: the evidence labelling (alignment by name, exact target
  values, local/national diagnostics, past-cap censuses, initial loss)
  moves into _doctrine_solve_evidence and runs for both results; the
  solve product carries `dense_reference` (weights, initial weights,
  diagnostics, losses, censuses).
- driver: a size run writes dense_reference_diagnostics.csv (every
  target's dense estimate, local rows then national, with a `grain`
  column) and dataset_size_selection.csv (pool row index, household id,
  clone index, design weight, inclusion probability, certainty,
  Horvitz–Thompson baseline, refit weight), lists both under outputs with
  digests, and summarises the reference under solve.dataset_size
  (losses, fit by family, uk_weight_summary). Dense runs are unchanged;
  the two files are size-run-only in the publish order.
- --selection-seed (requires --dataset-households; defaults to --seed)
  seeds only the informed L0 search, the exact-count draw and the refit,
  threaded through the holdout as well, so two selections compare on one
  pool and one dense reference; recorded as parameters.selection_seed.

Tests: the doctrine solve keeps a reference equal to the standalone dense
solve and a selection seed leaves it untouched; the CLI size run writes
both sidecars, lists them, and records both seeds; --selection-seed
without a size is refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nd receipt (#355)

The first licensed size rehearsal (55,000 of 792,690 at 100 epochs) was
refused inside select_exact_k: "degenerate boundary mass … adjust pi_hi or
k". With pi_hi=1.0 every gate below one stays in the boundary and its open
probability is scaled to the remaining draw size m, so the design is
feasible only when m * max(pi_boundary) <= sum(pi_boundary); the L0 budget
search stops on the count of not-fully-closed gates, which sits above the
open-probability mass while gates are only partly polarised. The refusal
carried no numbers.

refit_uk_dataset_size now measures selection_feasibility before the draw —
certainties and boundary draw at pi_hi=1, boundary mass and largest boundary
gate, feasibility, the largest household count feasible at pi_hi=1, the
smallest feasible pi_hi on a fixed grid, gate quantiles and counts, the
budget search's n_nonzero and lambda — and attaches it to the ValueError on
refusal and to the size receipt on success. Nothing is clamped or promoted;
the ruling on pi_hi stays a reviewed decision, now made from measured mass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…evaluate_uk_dataset_size steps 00-90, uk_fit_by_family lifted (#355)

Every evaluation piece for a --dataset-households candidate now has one
name and one place:

- uk_runtime/size_evaluation.py — pure measurement over a run directory:
  load_run (tolerant of pre-#877 dense runs), run_acceptance (the size-run
  checklist, not_applicable on dense runs), fit_tables (by grain and
  family), weight_tables (Kish ESS, distinct sources, stretch vs the
  Horvitz–Thompson baseline and vs pool design from the selection sidecar
  or a spine, self-checked against the manifest), area_support_tables
  (per-grain floors and breaches), gate_table (six ids with criticality),
  paired_targets (join on name; wins/ties/losses; the reference's red rows
  tracked through family/area/metric), dense_reference_deltas (size-only
  effect from the run's own dense reference), frozen_vs_recomputed
  (national rows against the incumbent-surface evaluator), footprint, and
  summarize with PRE_REGISTERED_OUTCOMES_V1 flags. No subprocess, no
  engine, no licensed data in tests.
- tools/evaluate_uk_dataset_size.py — the one command: steps
  00-run-acceptance, 10-dense-reference, 20-vs-reference/<label>,
  30-incumbent-score (score_uk_local_candidate), 40-incumbent-surface
  (evaluate_uk_incumbent_surface --engine-blocks 1 + frozen vs
  recomputed), 50-downstream (uk-candidate-eval T3/T4/T5 and the #731
  scorecard under this venv, each timed with /usr/bin/time -l), 90-summary
  (EVALUATION.md + JSON with the pre-registered table and a file index);
  every step writes receipt.json (inputs and outputs with digests, argv,
  wall, exit) and is skipped with a reason when its inputs are absent;
  re-running a step rewrites only its folder.
- _fit_by_family lifted from the driver into
  uk_runtime/diagnostics.py::uk_fit_by_family unchanged; the driver
  imports it.

Tests: test_uk_size_evaluation.py (synthetic run dirs in both manifest
shapes, table-format H5s, time -l parsing, red-row mapping, stretch
self-check, breaches, flags, refusals) and
test_uk_evaluate_dataset_size_tool.py (tree, receipts, manifest, summary;
skipped steps; a failing downstream stub recorded without aborting;
single-step re-run). Implemented by Codex from .codex-work/PLAN.md;
verified here: ruff, format, ci_test_groups --verify, 88 tests across the
four UK rowwise/evaluation files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…/spine-p, S0, S1/S1b refusals with the feasibility numbers (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t, as the wheels venv is (#355)

The wheels lane runs the suite against installed wheels in a venv without
pytables; the size CLI test read the exported candidate through
pd.HDFStore and failed there (the only failure in 7,963 tests). The
rowwise candidate test file's convention is importorskip("tables") and
importorskip("h5py") at the top of every CLI test; the size test now
follows it, and the two evaluation test files, which write PyTables-format
H5 fixtures, carry the module-level importorskip the same lane relies on.
With pytables present nothing is skipped (41 tests run); with the import
blocked the modules skip instead of erroring.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…b: --selection-pi-hi (#355)

refit_uk_dataset_size takes pi_hi (default 1.0, the protected-carriers-only
design); the driver exposes it as --selection-pi-hi, candidate-only and
bounded to (0, 1], threaded through the holdout, recorded as
parameters.selection_pi_hi and in the size receipt (selection_pi_hi, the
draw receipt's pi_hi, and the feasibility measurement's
requested_pi_hi / feasible_at_requested_pi_hi). The refusal message names
the requested threshold. A threshold below one promotes learned
near-certain gates; the plan doc records the measured reason and the
2026-09-08 ruling (smoke at the measured feasible count; the 55,000
candidate at pi_hi 0.95, the US exact-k ladder's setting, and 2,000
epochs). Nothing changes for dense runs or for the default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…355)

The 45,800 smoke on spine-p (the tree rebased onto #879) was refused inside
contribution_initialization: "nonzero target at row 20648 has no support",
a national row whose constraint row is all zeros on the cloned pool. The
dense solve tolerates such a row as a miss; a size selection cannot carry
it, and the row index alone is not actionable. refit_uk_dataset_size now
lists every nonzero target with no supporting household by name before
initialisation and refuses with the list, so the binding defect is fixed
upstream or the row is excluded with a signed reason, never selected
around. unsupported_nonzero_targets() is the reusable check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…macOS's /usr/bin/time -l (#355)

The evaluation command timed each downstream script with `/usr/bin/time
-l`, a BSD flag GNU time rejects with exit 125, so on the linux CI runners
a failing stub could not surface its own exit code
(test_downstream_continues_after_a_failing_stub: 125 != 7). The timed
form now runs the script in a child interpreter through a shim that
executes it in-process and reports the child's own peak resident set
(bytes on every platform) on stderr at exit; wall time is measured by the
caller and the exit code is the script's own. The marker line is stripped
from the recorded stderr tail. Same receipt shape on macOS and linux.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ent bands from #879), the ruling, and the CI portability fix (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s above £27,600 (#355, ruling 2026-09-08)

Under #879's relationship-based uc_calibration_family_type no benefit
unit in the spine is a couple without children with an annual UC award
in the 27,600-28,800 or 28,800-30,000 band: both rows sit at -100% on
#879's corrected comparison and have no carrier on the K=15 rowwise pool
of spine-p, where the #355 size selection refuses them by name. The dense
seam tolerated them as capped misses. Signed into the reviewed measure
exclusion register on the zero-support-channel precedent (approved by
juaristi22, 2026-09-08, expiring 2026-12-08; catalogue and top-band
repair tracked in microcosm#736; the sibling 26,400-27,600 band was
already excluded). Register census pins move with it: 18 payment-band
exclusions, 82 active payment bands.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…aunch (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… smoke4 at pi_hi 0.7 (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t binding defect (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… never write one (#355)

score_uk_local_candidate needs the frozen compiled register R14 built
separately; --scoring-registry names it, with <run>/ and each reference's
scoring_registry_compiled.json as fallbacks, and the skip reason says so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…t; re-pin the identities it moves (#355, ruling 2026-09-08)

The exact-count draw can only draw from the gates' open-probability mass,
but the L0 budget search stopped on the count of not-fully-closed weights,
which sat 10-13% above that mass in every licensed measurement; the draw
then refused, and the "feasible count" moved with every request.
calibrate() gains budget_basis ("nonzero_count", the historical default,
or "open_probability_mass": each probe is measured by sum(pi), the gates'
expected open count) and records it in options; the size refit uses the
mass basis and records selection_budget_basis, and the feasibility
measurement names its basis. On a 60-record toy problem the mass basis
lands sum(pi) 20.2 for a budget of 20 where the count basis gives count 19
with mass 18.5.

solve.py is attested, so the identities move again: the am/be/uk
country-bundle digests, the loader golden vector, the seed-protocol and
seed-map digests with the regenerated US coverage report, the H1 calibrate
parity fixture (regenerated on the authoring platform; direct bytes
unchanged), and the US spec digest pinned in the multispine pool-tool
test. 245 tests across the pin and parity files pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…onthly rent x A17-uprated private-renter households (#355, ruling 2026-09-08)

ons.rent.private_rent (314 local-authority rows, ONS PIPR, active since
#874's re-pin) bound a mean monthly rent to rent/private_rent, a weighted
annual total, so every row sat at a relative error of 1e5-1e6 and at the
loss cap on the rowwise pool. The calibration is linear in the weights, so
the mean's linear form is a total: uk_private_rent_mean_to_total, applied
in uk_local_target_surface after the A15/A17 uprating, composes each row's
value as 12 x mean x the same authority's bound tenure/private_rent count,
keeps the mean and the count in the row's metadata, refuses by area when
the count is missing or non-positive, and records every cell under
cross_grain.private_rent_mean_to_total. The metric is unchanged. The
census rationale for the PIPR source no longer claims the feed carries
only 2026-06 (it carries the 2025 months the reference averages); the
committed census artifact is regenerated. Implemented by Codex from
.codex-work/PLAN-rent.md; verified here: ruff clean, 137 tests across the
ledger-target, local rowwise, rowwise candidate and census files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… on 2026-09-08 (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…un, the S2 launch (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juaristi22 and others added 13 commits September 9, 2026 18:59
…e ±5% band diagnosis, options (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… checkpoint the solve before the draw (#355)

S2 (55,000 at pi_hi 0.95, 2,000 epochs) was refused at the exact-count draw
after 4.8 hours: the mass-basis budget search stopped inside its +/-5% band
166 rows under the request, the draw's condition is one-sided, and nothing
was persisted, so the dense solve was lost with the refusal.

Fix A. exact_k_design_feasibility(pi, k, pi_hi) is the draw's inequality
without the draw (verdicts feasible / certainties_exceed_k /
boundary_short_of_draw / boundary_mass_short; random polarised designs
agree with select_exact_k on every one). calibrate(...,
feasible_draw_pi_hi=h) on the mass basis makes the budget search accept a
probe only when a draw of target_records at h is feasible on its gate
probabilities, steers an infeasible probe like a count miss, prefers
feasible probes for the returned run, and records every probe under
options["budget_search"]. The UK size selection passes its threshold; the
size receipt carries the search receipt and each scan threshold's verdict.

Fix C. refit_uk_dataset_size is split into select_uk_dataset_size (the
search) and the draw + refit, which accepts an existing selection.
uk_runtime/size_checkpoint.py persists the dense solve and the selection
with the identity of the pool, the target surface and the solve settings;
load rebuilds both through the new calibrate.rebuild_calibration_result on
the resumed run's compiled pool and refuses by name on identity, pool or
surface drift. The doctrine solve and the candidate driver write the
checkpoint into --out by default on size runs (--no-size-checkpoint) and
--resume-size-checkpoint DIR continues at the draw, at any threshold, with
both thresholds and Logbook phases recorded.

Attested calibrate modules moved: country-bundle digests, seed protocol and
map, loader golden vector, US spec digest (multispine pool-tool test), the
regenerated coverage report and the H1 calibrate parity fixture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…to the pre-registered table; P95b launched on the fixed code (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…et probes with their drawability verdict, the search stop (#355)

A size run was silent between "solving ... under the doctrine..." and its
manifest, hours later. The budget search now emits one budget_probe event
per finished probe (penalty, measure, certainties, boundary draw and mass,
verdict) and a budget_search_done event beside the per-epoch stream, on the
calibrator's existing progress_callback seam. The UK doctrine solve takes a
progress line sink (uk_runtime/solve_progress.py formats timestamped lines:
a loss every hundred epochs and at the last epoch of the dense solve, of each
probe and of the refit; one line per probe; one when the search stops), the
size search and refit are tagged by phase, and the candidate driver writes
the lines to stderr so they land in the run log.

solve.py is attested: the country-bundle digests, seed protocol and map,
loader golden vector, US spec digest, coverage report and H1 calibrate
parity fixture are re-cut.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…selection comparison (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ted and downstream findings, the mass-loss mechanism (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…est-iterate oracle's gates attestation for the rebased tree (#355)

Rebased onto main after #881 and #883. The country-bundle digests, the seed
protocol and map, the loader golden vector, the US spec digest in the
multispine pool-tool test, the regenerated coverage report and the H1
calibrate parity fixture are re-cut on the combined tree. Main's new
pre-best-iterate oracle attests the gates module's bytes; #355's gates
changes (per-record initial probabilities and the protected mask) move that
digest legitimately, so the attestation is re-pinned and the oracle's
byte-identity checks on the excluded paths still pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nsequence for later size runs (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… receipts: one-engine scorecard, incumbent-score finding, exclusion audit (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…chine (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…6 replays against the enhanced FRS (#355)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…oint guard, attested and frozen gates, honest draw and stretch keys (#355)

1. The size-checkpoint identity carries the solve doctrine and the doctrine
   solve asserts the restored max_weight_ratio, mass and target_loss_cap
   against today's doctrine after a resume; the writing run's code pin and
   build id ride in the checkpoint's provenance and are reported, not compared.
2. An --out that already holds a size checkpoint is refused before the solve.
3. microcosm.calibrate.gates and microcosm.calibrate.initialization join the
   seed-protocol implementation digest, and the pre-best-iterate oracle runs
   with a frozen copy of the gate module loaded as pre_best_iterate_gates, so
   a gate-behaviour change surfaces as a numeric mismatch on the gated
   control path instead of being absorbed on both sides.
4. The size receipt reports certainty_share, boundary_draws and
   zero_target_rows; the manifest distinguishes the realized stretch against
   the refit's reference from the stretch against the pool design weights.
5. The written-checkpoint receipt carries no timestamp and no absolute path.

Rebased over #891. The attestation change moved the seed protocol, the
pointer inventory, the field and mode counts and the resolved_seed_protocol
claim, the country-bundle digests, the loader golden vector and the US spec
digest; all re-cut, the coverage report regenerated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…tialization (#355)

Attesting microcosm.calibrate.gates and microcosm.calibrate.initialization
in seeds.py added two resolved-binding fields (9,770 -> 9,772; ledger
42,154 -> 42,156; compiler_semantic 27,715 -> 27,717; no_generation0_effect
3,678 -> 3,680). field_usage.py and the coverage tool test were re-pinned in
c2ee134; this sibling test only runs in the rest, engine-shared and wheels
lanes and was missed. Values recomputed from the ledger, not transcribed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Response at c2ee1342 (review items) + 1bdd6447 (a literal pin the attestation moved in test_spec_engine_field_usage.py, missed in the first push; CI run 34405953724 on the combined head).

Thank you for the A/B run on the default paths; that is the check I could not do from inside the PR. Everything below is on the branch, rebased over #891, with every identity re-cut on the combined tree.

1. Doctrine drift on resume — fixed, two locks. The checkpoint identity now carries the solve doctrine (_doctrine_bounds(): the stretch multiplier, the loss cap, the rule) beside the pins, seeds and sizes, so a resume after a doctrine change is refused by name at the identity check; and independently the doctrine solve asserts the restored options (max_weight_ratio, mass, target_loss_cap) against today's doctrine after the load and refuses if they differ. The writing run's code_pin and build_id ride in the checkpoint's provenance and are reported on resume rather than compared, so a receipts commit does not invalidate a checkpoint. Tests at the solve level (a resume under a doubled stretch bound refuses) and the driver level (identity carries the doctrine, provenance names the run).

2. Stale checkpoint refused after the solve — fixed. The driver now refuses an --out holding size_selection_checkpoint.{npz,json} beside the other pre-solve output guards, naming the files and the --resume-size-checkpoint way out; the writer's refusal stays as the last line. Driver test: a directory holding a stale checkpoint fails before any solve and leaves no manifest.

3. gates.py and initialization.py — attested and controlled. Both join the seed-protocol implementation digest (seeds.py), so the attested identity moves when gate behaviour or the contribution prior change (this moved the seed protocol digest, the pointer inventory, two field counts and the US spec digest; all re-cut in c2ee1342 except the field-usage test's own literal counts, which only run in the rest/engine-shared/wheels lanes and went red there; 1bdd6447 re-pins them from the recomputed ledger). The pre-best-iterate oracle now runs with a frozen copy of the gate module (fixtures/pre_best_iterate/gates_14608a5e.py, loaded under pre_best_iterate_gates) instead of importing the live one; the control asserts the oracle's HardConcrete is the frozen class and not the live one, so a gate-behaviour change surfaces as a numeric mismatch on the gated control path. The oracle module's only change is that import; its digest is re-pinned.

4. How deterministic the draw was — now stated in the receipt. The size receipt carries certainty_share and boundary_draws. On the licensed runs: P50 (threshold 0.5) 54,595 of 55,000 by threshold, 405 drawn (99.3 %); P95b (0.95) 54,844 by threshold, 156 drawn (99.7 %). The body's "exact-count Sampford draw" is therefore a threshold on learned π with a small tail draw, and the receipts say so. On the stretch key: the manifest now carries realized_max_weight_ratio_vs_stretch_reference (against the frame the refit started from, the HT baseline on a size run; 10.0 on both candidates) and realized_max_weight_ratio_vs_design measured against the pool design weights themselves (P50 1,383, P95b 1,679; the share of rows above 100× design is 0.07 % and 0.03 %). The inclusion-adjusted reference stays listed for adjudication in the plan.

5. Zero-valued targets — counted. zero_target_rows in the size receipt. On the compiled 20,794-row surface of the licensed runs it is zero: the zero-valued private-rent rows in the parity fixture are the deferred cells, which never reach the compiled surface.

6. The two register changes. Both are now their own paragraph in the PR body, with the effect on comparability with R17 stated; splitting them would leave this branch unable to run (the composed rent rows are what makes the 314 PIPR cells bindable and the two UC bands are what the size path refuses by name), so they stay, flagged.

Nits. The checkpoint receipt that rides into the candidate manifest no longer carries written_at or the absolute directory (both stay in the checkpoint's own JSON), so manifests of identical runs byte-match again. The parity fixture's authoring platform is py3.14 because that is the interpreter this workspace's venv pins; the linux entries are untouched. The four new options keys are additive (budget_basis, feasible_draw_pi_hi, budget_search, and the selection_receipt main added); manifests that embed options gain keys and lose none.

Measured since your pass (receipts experiments/355-uk-dataset-size-receipts.md): frozen_vs_recomputed ran on both candidates — 340 of 364 national rows matched, maximum divergence 9.3 % (P50) and 11.1 % (P95b) on the council-tax band stock and private-school students, the other 328 rows within 1 % — the pre-registered red flag fires and is reported; the incumbent leg of the #731 scorecard runs under 2.97.0 (households P50 27.2 m, P95b 27.4 m, R17 29.3 m, eFRS 31.3 m; Gini 0.350 / 0.351 / 0.376 / 0.376); the downstream instruments were re-run on one engine for all four files; the T6 replays ran against the enhanced FRS. The holdout is the one item still not run for a size candidate, by ruling; the rule-1 score against the incumbent needs it.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass at 1bdd6447 (Claude Code, high effort; worktree checkout; the six commits since the reviewed head read against the dispositions — four more than the two the reply names; the round-one battery: 901 passed, 3 skipped, 0 failed; on the old head exactly the three new tests fail; ruff check clean; CI run 34405953724 green on all 23 jobs).

1. Verified, two locks. _size_checkpoint_identity now carries "doctrine": _doctrine_bounds() (loss cap, stretch, scale rule, weight rule, epochs, clone count; build_uk_rowwise_candidate.py:555) and the load compares it by key; independently local_rowwise.py:1218-1239 asserts the restored max_weight_ratio, mass and target_loss_cap against today's doctrine. code_pin and build_id ride in provenance and are reported, not compared. Both tests fail on the old head (KeyError: 'doctrine') and pass here.

2. Verified. _refuse_stale_size_checkpoint runs right after the output-path validation and before any ladder load or solve, names the files and the --resume-size-checkpoint way out; the test asserts no manifest is written. Fails on the old head.

3. Verified, with one thing to say plainly. gates.py and initialization.py join _DIRECT_KERNEL_MODULES (seeds.py:349-350), so the attested identity now moves with them. The oracle imports a frozen gate module (gates_14608a5e.py), the control asserts the oracle's HardConcrete is the frozen class and not the live one, and the mechanism works: I mutated the live clamp from 1.0 to 0.999 and all three excluded-path controls failed on weight bytes. One observation: the frozen copy is byte-identical to this PR's gates.py (14608a5e…), not main's (379e4adb…), so the control cannot adjudicate this PR's own gate change — the evidence for that is that the control's numeric values did not move when only the module sha did. Fine going forward; worth one sentence in the receipt.

4. Implemented; the design-ratio numbers are only in the comment. certainty_share, boundary_draws (dataset_size.py:348-349) and both realized_max_weight_ratio_* keys (driver :2496-2513) exist, and the receipts carry the certainty counts (54,595 / 405 and 54,844 / 156) and the >100× design shares. The vs_design values themselves (1,383 and 1,679) and the 10.0 stretch-reference ratio appear nowhere in the tree — the receipts md names the keys at lines 724-725 but not the values, and no manifest with the new keys is committed. Write them into the receipt.

5. Verified. zero_target_rows is counted and the receipt records 0 on the licensed surface, consistent with the zero-valued parity rows being deferred cells.

6. Partial — the disposition says more than the body does. The two register changes are a bullet in the changes list, not a paragraph, and the body contains no comparability statement against R17 (no match for "R17" or "comparab"). The reasoning for keeping them in this PR is sound; the sentence just is not there. Also unnamed in the reply: ad519c26 changes the PIPR census notes from "all 361 remain deferred" to "314 bind, 47 deferred", which is a register-facing edit that deserves a mention.

Nits. written_at and the directory are out of the candidate manifest and stay in the checkpoint's own JSON; the options keys are additive. The parity pin replaces the darwin py3.13 entry with py3.14 rather than keeping both, and there are no linux entries in that file, so "the linux entries are untouched" is vacuous. test_spec_engine_field_usage.py, touched in 1bdd6447, would be reformatted by ruff format; CI does not enforce it, but "changed-file formatting passes" is not true of the head.


Approving: the three should-fixes are closed with discriminating tests and a working frozen control. The residuals are all text — the two design-ratio numbers into the receipt, the R17 sentence into the body, one line on the frozen gates being the post-PR module.

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.

UK small national default: L0-selected ~50k-household file from the 535k pool

2 participants