diff --git a/changelog.d/uk-full-build.changed.md b/changelog.d/uk-full-build.changed.md index f4f462554..945b2331b 100644 --- a/changelog.d/uk-full-build.changed.md +++ b/changelog.d/uk-full-build.changed.md @@ -1 +1 @@ -The UK full build is one executable graph served by one driver, `microcosm-build-uk` (`tools/build_uk_full.py`), which carries the release roles of microcosm#823. `--release-role dense` builds the K-clone joint national and local surface through the graph: source spine, geographic cloning, target compilation and selection, calibration, exact-count sizing, gates, diagnostics and a checked export, with all applicable geographies calibrated by default and `--target-geographies country` as an explicit filter in the same build. `--release-role national` is parsed and validated by the same posture-aware validator and then dispatched, before any graph is prepared, to the retained calibration seam through `uk_runtime.national_role`, so the national line is built by the same engine as before. The dense role keeps the posture's solve defaults and refusal tables, the `microcosm_uk_2024_25_local.*` output names, a schema-4 `rowwise_candidate_manifest.json` projected from the stored graph artifacts (which the dense release pre-flight and assembler accept), the Logbook row, staging telemetry around each graph phase and the staged bundle, and gains `--baseline-pi-floor`, `--no-size-checkpoint`, `--candidate-clone-counts` and `--households-only`. `tools/build_uk_rowwise_candidate.py` is now a stub over the driver and `tools/build_uk_frs_spine.py` a shim over `uk_runtime.spine_build`, into which the spine tool moved with its gate batteries as graph nodes and its stage evidence read back from the content store; every runbook command keeps working through them. Bound stage, gate and diagnostic artifacts are restored on replay, exported bytes are checked against their declared artifacts, and the graph's terminal node reports unsigned certification readiness. +The UK full build is one executable graph served by one driver, `microcosm-build-uk` (`tools/build_uk_full.py`), which carries the release roles of microcosm#823. `--release-role dense` builds the K-clone joint national and local surface through the graph: source spine, geographic cloning, target compilation and selection, calibration, exact-count sizing, gates, diagnostics and a checked export, with all applicable geographies calibrated by default and `--target-geographies country` as an explicit filter in the same build. `--release-role national` is parsed and validated by the same posture-aware validator and, since the change that followed, built on the same graph (see the national graph path entry). The dense role keeps the posture's solve defaults and refusal tables, the `microcosm_uk_2024_25_local.*` output names, a schema-4 `rowwise_candidate_manifest.json` projected from the stored graph artifacts (which the dense release pre-flight and assembler accept), the Logbook row, staging telemetry around each graph phase and the staged bundle, and gains `--baseline-pi-floor`, `--no-size-checkpoint`, `--candidate-clone-counts` and `--households-only`. `tools/build_uk_rowwise_candidate.py` is now a stub over the driver and `tools/build_uk_frs_spine.py` a shim over `uk_runtime.spine_build`, into which the spine tool moved with its gate batteries as graph nodes and its stage evidence read back from the content store; every runbook command keeps working through them. Bound stage, gate and diagnostic artifacts are restored on replay, exported bytes are checked against their declared artifacts, and the graph's terminal node reports unsigned certification readiness. diff --git a/changelog.d/uk-national-graph-path.changed.md b/changelog.d/uk-national-graph-path.changed.md new file mode 100644 index 000000000..731087354 --- /dev/null +++ b/changelog.d/uk-national-graph-path.changed.md @@ -0,0 +1,3 @@ +The UK national release role (`microcosm-build-uk --release-role national`) now builds on the shared executable graph instead of dispatching to the in-process calibration seam. `uk_runtime.graph_national` composes the bound spine checkpoint, a national register node (the pinned Chronicle artifact, measure exclusions, band edges, the frozen-register check), a national problem node (engine measures resolved on the bound frame, the register materialised, one ordered problem with the national doctrine's `family_equal` loss weights, mass reason and bounds bound into it), the shared dense and calibrated nodes under the doctrine, and the calibration-seam gate scope evaluated in the graph; the driver materialises the seam-shaped evidence from the stored artifacts (the seam diagnostics with the build block, the target-support sidecars, the battery replayed from the stored phase report and signed, `microcosm_uk_2024_25.h5` with a graph readback, the schema-1 `build_record.json` the release-cut certifier reads, the frozen registries, the schema-4 national manifest, the Logbook row on `uk-frs-calibration`, staging and the incumbent evaluation). The dense solve node's admission is declared by the node (the dense line keeps its preflight battery; the national line admits the bound spine's provenance), the bound checkpoint keeps its H5's own period, and the measure receipt carries the materialisation report. `calibration_run.run_uk_calibration`, its attempt runner and battery, and `national_calibration.UKNationalCalibrationStage` are retired; the seam's contracts (gate scopes, sidecar authentication, provenance blocks, scoped signing) and the frame adapter stay. + +Review round 1: the national line refuses an occupied `--out` before any work (the seam's rule), an operator interrupt records a `discarded` Logbook row on both lines, and `build.json` is re-issued after the close step so it binds the final build record and manifest bytes. The measure receipt records a scratch-mode resolver's source path relative to the scratch root, making the measure and national problem nodes deterministic between runs; the dense solve refuses a bound `mass_rule`, `scale_rule` or `l0_lambda` it does not honour; the national gate node reads its scope from the posture. Folded from microcosm#932's round 1: a release candidate's atomic-area supports must be the `sources.yaml` rows and the dense release pre-flight checks the manifest's geography binding against that register; the OA-to-LAD lookups accept `LAD23CD`/`LAD24CD` only; the committed #931 evidence blanks z-scores beside suppressed counts. diff --git a/docs/evidence/uk-901-national-ab/compare_a_vs_b2.json b/docs/evidence/uk-901-national-ab/compare_a_vs_b2.json new file mode 100644 index 000000000..6094b3d6a --- /dev/null +++ b/docs/evidence/uk-901-national-ab/compare_a_vs_b2.json @@ -0,0 +1,187 @@ +{ + "purpose": "R5 of experiments/901-uk-national-graph-path-receipts.md: the licensed seam-vs-graph national A/B on the 10 % smoke spine of the #901 pre-merge run (53,806 households, 1,062 targets), compared field by field by data/ukds/acceptance/901-national-ab/compare_national_ab.py. Aggregates and field-level differences only; no unit records.", + "date": "2026-09-29", + "a": "a-seam (main at the #901 merge, 6a70cd4ee, run_uk_calibration)", + "b": "b-graph-2 (uk-national-graph-path at 218226898, microcosm-build-uk --release-role national)", + "solve": { + "targets": [ + 1062, + 1062 + ], + "households": [ + 53806, + 53806 + ], + "row_names_equal": true, + "target_vector_equal": true, + "matrix_identical": true, + "design_weights_equal": true, + "household_ids_equal": true, + "final_weights_identical": true, + "final_weights_max_abs_diff": 0.0, + "final_weight_totals": [ + 29024853.117736887, + 29024853.117736887 + ] + }, + "diagnostics": { + "scalars": { + "schema_version": [ + 8, + 8, + true + ], + "initial_loss": [ + 0.3107629418373108, + 0.3107629418373108, + true + ], + "final_loss": [ + 0.008806911920052014, + 0.008806911920052014, + true + ], + "n_nonzero": [ + 53806, + 53806, + true + ], + "n_records": [ + 53806, + 53806, + true + ], + "realized_max_weight_ratio": [ + 10.000000000000002, + 10.000000000000002, + true + ], + "fraction_within_10pct": [ + 0.9774011299435028, + 0.9774011299435028, + true + ] + }, + "diff_minus_build": [], + "build_diff": [ + "/measure_resolution/provider/mode: 'direct_h5' vs 'scratch_frame_export'", + "/measure_resolution/provider/source_path: the seam's input H5 path vs the graph's scratch export path (machine paths omitted here; the scratch path is recorded scratch-relative since the round-1 review)" + ], + "bytes_identical": false + }, + "gate_report": { + "blocked_at_phase": [ + "terminal", + "terminal" + ], + "statuses": { + "uk_aggregate_admin": [ + "passed", + "passed" + ], + "uk_calibration_reference_coverage": [ + "passed", + "passed" + ], + "uk_cgt_projection_entrants": [ + "failed", + "failed" + ], + "uk_target_fit": [ + "passed", + "passed" + ], + "uk_weight_ess": [ + "passed", + "passed" + ], + "uk_weight_ratio": [ + "passed", + "passed" + ], + "uk_zero_weight_strata": [ + "passed", + "passed" + ] + }, + "diff_minus_attestation": [], + "signed": [ + true, + true + ] + }, + "logbook": { + "pipeline": [ + "uk-frs-calibration", + "uk-frs-calibration", + true + ], + "identity_digest": [ + "2344c21d2bc85ed0695cf8678db1890cb03238ed9aa3612ca9a209204fd11659", + "2344c21d2bc85ed0695cf8678db1890cb03238ed9aa3612ca9a209204fd11659", + true + ], + "input_pins_digest": [ + "74a4aa2fbfb82565b98b7f7fbb1b31076d9b95ebd48ab2e125ccc5366bc3b188", + "74a4aa2fbfb82565b98b7f7fbb1b31076d9b95ebd48ab2e125ccc5366bc3b188", + true + ], + "disposition": [ + "failed", + "failed", + true + ], + "rung": [ + "f100", + "f100", + true + ], + "seed": [ + 0, + 0, + true + ], + "phases": [ + [ + "attempt_started", + "input_sha_verified", + "input_loaded", + "input_sidecar_bound", + "input_finite", + "national_calibration_solved", + "diagnostics_written", + "target_support_sidecars_written", + "error" + ], + [ + "attempt_started", + "configured", + "input_sha_verified", + "input_loaded", + "input_sidecar_bound", + "targets_bound", + "national_calibration_solved", + "diagnostics_written", + "target_support_sidecars_written", + "calibration_gates_evaluated", + "candidate_blocked" + ] + ] + }, + "h5": { + "written": [ + false, + false + ] + }, + "build_record": { + "written": [ + false, + false + ] + }, + "wall_seconds": { + "a": "637.99", + "b": "653.31" + } +} diff --git a/docs/evidence/uk-931/atomic-assignment-cells.json b/docs/evidence/uk-931/atomic-assignment-cells.json index f60e39d11..3f7854bfa 100644 --- a/docs/evidence/uk-931/atomic-assignment-cells.json +++ b/docs/evidence/uk-931/atomic-assignment-cells.json @@ -10,7 +10,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.32158776388379867 + "z": null }, { "area_code": "E14001064", @@ -30,7 +30,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4849882421940613 + "z": null }, { "area_code": "E14001066", @@ -40,7 +40,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4629811535772127 + "z": null }, { "area_code": "E14001067", @@ -60,7 +60,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.44990246350253255 + "z": null }, { "area_code": "E14001069", @@ -90,7 +90,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.36591442200259805 + "z": null }, { "area_code": "E14001072", @@ -140,7 +140,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3845972134529664 + "z": null }, { "area_code": "E14001077", @@ -150,7 +150,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2750282743313824 + "z": null }, { "area_code": "E14001078", @@ -160,7 +160,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3342498274527362 + "z": null }, { "area_code": "E14001079", @@ -180,7 +180,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3793397145454179 + "z": null }, { "area_code": "E14001081", @@ -210,7 +210,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6527404137722314 + "z": null }, { "area_code": "E14001084", @@ -230,7 +230,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.980669833429713 + "z": null }, { "area_code": "E14001086", @@ -250,7 +250,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.23874262030272575 + "z": null }, { "area_code": "E14001088", @@ -260,7 +260,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7285562203864382 + "z": null }, { "area_code": "E14001089", @@ -310,7 +310,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.3749361963031372 + "z": null }, { "area_code": "E14001094", @@ -340,7 +340,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.4678877955408116 + "z": null }, { "area_code": "E14001097", @@ -360,7 +360,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.9658982047722182 + "z": null }, { "area_code": "E14001099", @@ -400,7 +400,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.49663287171730586 + "z": null }, { "area_code": "E14001103", @@ -440,7 +440,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4597236432850999 + "z": null }, { "area_code": "E14001107", @@ -450,7 +450,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.34276750082845936 + "z": null }, { "area_code": "E14001108", @@ -460,7 +460,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.2977241251274199 + "z": null }, { "area_code": "E14001109", @@ -480,7 +480,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3331871728460651 + "z": null }, { "area_code": "E14001111", @@ -510,7 +510,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3865559234376189 + "z": null }, { "area_code": "E14001114", @@ -520,7 +520,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2376731249692524 + "z": null }, { "area_code": "E14001115", @@ -530,7 +530,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.4263360863824335 + "z": null }, { "area_code": "E14001116", @@ -570,7 +570,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1505010035482153 + "z": null }, { "area_code": "E14001120", @@ -580,7 +580,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.4029985546487236 + "z": null }, { "area_code": "E14001121", @@ -600,7 +600,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5893634719431714 + "z": null }, { "area_code": "E14001123", @@ -620,7 +620,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6324367758966978 + "z": null }, { "area_code": "E14001125", @@ -640,7 +640,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3612024160556402 + "z": null }, { "area_code": "E14001127", @@ -660,7 +660,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.24186382290079347 + "z": null }, { "area_code": "E14001129", @@ -680,7 +680,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.37437742788651746 + "z": null }, { "area_code": "E14001131", @@ -690,7 +690,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4796376802519078 + "z": null }, { "area_code": "E14001132", @@ -720,7 +720,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.5766475992036715 + "z": null }, { "area_code": "E14001135", @@ -770,7 +770,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2748417359533837 + "z": null }, { "area_code": "E14001140", @@ -780,7 +780,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.43901609786931284 + "z": null }, { "area_code": "E14001141", @@ -790,7 +790,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.42893870675195694 + "z": null }, { "area_code": "E14001142", @@ -830,7 +830,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4357658400747256 + "z": null }, { "area_code": "E14001146", @@ -850,7 +850,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1231180428776212 + "z": null }, { "area_code": "E14001148", @@ -890,7 +890,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.6433755321339925 + "z": null }, { "area_code": "E14001152", @@ -900,7 +900,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3293800655156362 + "z": null }, { "area_code": "E14001153", @@ -910,7 +910,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7943082053907812 + "z": null }, { "area_code": "E14001154", @@ -930,7 +930,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41346183321298596 + "z": null }, { "area_code": "E14001156", @@ -940,7 +940,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5860394631212886 + "z": null }, { "area_code": "E14001157", @@ -960,7 +960,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.8313706566040058 + "z": null }, { "area_code": "E14001159", @@ -980,7 +980,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.7678301273365191 + "z": null }, { "area_code": "E14001161", @@ -990,7 +990,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3148369440112527 + "z": null }, { "area_code": "E14001162", @@ -1000,7 +1000,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4875877163646158 + "z": null }, { "area_code": "E14001163", @@ -1030,7 +1030,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3895233507633208 + "z": null }, { "area_code": "E14001166", @@ -1070,7 +1070,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 2.1514742218192957 + "z": null }, { "area_code": "E14001170", @@ -1090,7 +1090,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4328967282444347 + "z": null }, { "area_code": "E14001172", @@ -1110,7 +1110,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.47585855288405676 + "z": null }, { "area_code": "E14001174", @@ -1160,7 +1160,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.8005850103409007 + "z": null }, { "area_code": "E14001179", @@ -1180,7 +1180,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.44002279028476576 + "z": null }, { "area_code": "E14001181", @@ -1190,7 +1190,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4874023087349405 + "z": null }, { "area_code": "E14001182", @@ -1220,7 +1220,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.33619397532209666 + "z": null }, { "area_code": "E14001185", @@ -1230,7 +1230,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.5103972379503492 + "z": null }, { "area_code": "E14001186", @@ -1240,7 +1240,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7421848411798616 + "z": null }, { "area_code": "E14001187", @@ -1320,7 +1320,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.5530654008514524 + "z": null }, { "area_code": "E14001195", @@ -1330,7 +1330,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4792777856612157 + "z": null }, { "area_code": "E14001196", @@ -1350,7 +1350,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.369174705695138 + "z": null }, { "area_code": "E14001198", @@ -1370,7 +1370,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.25594460478160524 + "z": null }, { "area_code": "E14001200", @@ -1390,7 +1390,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.49103746506670576 + "z": null }, { "area_code": "E14001202", @@ -1410,7 +1410,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4962670117534825 + "z": null }, { "area_code": "E14001204", @@ -1450,7 +1450,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.4945446583121496 + "z": null }, { "area_code": "E14001208", @@ -1460,7 +1460,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 2.083669546044031 + "z": null }, { "area_code": "E14001209", @@ -1470,7 +1470,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7762814170940947 + "z": null }, { "area_code": "E14001210", @@ -1490,7 +1490,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.44380761195216983 + "z": null }, { "area_code": "E14001212", @@ -1530,7 +1530,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4686463318112006 + "z": null }, { "area_code": "E14001216", @@ -1540,7 +1540,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3204649862101008 + "z": null }, { "area_code": "E14001217", @@ -1580,7 +1580,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7833443444923207 + "z": null }, { "area_code": "E14001221", @@ -1600,7 +1600,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.46336266535681103 + "z": null }, { "area_code": "E14001223", @@ -1680,7 +1680,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.44003760477810105 + "z": null }, { "area_code": "E14001231", @@ -1690,7 +1690,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4247330898615719 + "z": null }, { "area_code": "E14001232", @@ -1720,7 +1720,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4425387166226698 + "z": null }, { "area_code": "E14001235", @@ -1730,7 +1730,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7281029020421135 + "z": null }, { "area_code": "E14001236", @@ -1750,7 +1750,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.5873849249311824 + "z": null }, { "area_code": "E14001238", @@ -1760,7 +1760,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.9231930060855296 + "z": null }, { "area_code": "E14001239", @@ -1790,7 +1790,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4676654429170951 + "z": null }, { "area_code": "E14001242", @@ -1810,7 +1810,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.41469814337084093 + "z": null }, { "area_code": "E14001244", @@ -1850,7 +1850,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.678366027524772 + "z": null }, { "area_code": "E14001248", @@ -1860,7 +1860,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.2300800891804827 + "z": null }, { "area_code": "E14001249", @@ -1880,7 +1880,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.21366136754459 + "z": null }, { "area_code": "E14001251", @@ -1900,7 +1900,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.41850672852920373 + "z": null }, { "area_code": "E14001253", @@ -1920,7 +1920,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4245799682733015 + "z": null }, { "area_code": "E14001255", @@ -1930,7 +1930,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.0856530203952832 + "z": null }, { "area_code": "E14001256", @@ -1940,7 +1940,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2519175370702735 + "z": null }, { "area_code": "E14001257", @@ -1950,7 +1950,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.9246028264866595 + "z": null }, { "area_code": "E14001258", @@ -1970,7 +1970,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7059127088605026 + "z": null }, { "area_code": "E14001260", @@ -1990,7 +1990,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5985214453258789 + "z": null }, { "area_code": "E14001262", @@ -2010,7 +2010,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.41041333036795014 + "z": null }, { "area_code": "E14001264", @@ -2020,7 +2020,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5308693681713419 + "z": null }, { "area_code": "E14001265", @@ -2030,7 +2030,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.8101430604251691 + "z": null }, { "area_code": "E14001266", @@ -2050,7 +2050,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.23972233783915875 + "z": null }, { "area_code": "E14001268", @@ -2060,7 +2060,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.4004033904453059 + "z": null }, { "area_code": "E14001269", @@ -2070,7 +2070,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.1916650922042074 + "z": null }, { "area_code": "E14001270", @@ -2110,7 +2110,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.311531082513639 + "z": null }, { "area_code": "E14001274", @@ -2120,7 +2120,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.30555474858286163 + "z": null }, { "area_code": "E14001275", @@ -2130,7 +2130,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.703205575984158 + "z": null }, { "area_code": "E14001276", @@ -2140,7 +2140,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7748534598733734 + "z": null }, { "area_code": "E14001277", @@ -2160,7 +2160,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.31541457248798915 + "z": null }, { "area_code": "E14001279", @@ -2180,7 +2180,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7627534093367492 + "z": null }, { "area_code": "E14001281", @@ -2220,7 +2220,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.4897421283144736 + "z": null }, { "area_code": "E14001285", @@ -2250,7 +2250,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.45403959903953134 + "z": null }, { "area_code": "E14001288", @@ -2270,7 +2270,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.3424745738280712 + "z": null }, { "area_code": "E14001290", @@ -2290,7 +2290,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4012791352293487 + "z": null }, { "area_code": "E14001292", @@ -2320,7 +2320,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3832226258523204 + "z": null }, { "area_code": "E14001295", @@ -2330,7 +2330,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.38512896800392843 + "z": null }, { "area_code": "E14001296", @@ -2340,7 +2340,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.5045277002379462 + "z": null }, { "area_code": "E14001297", @@ -2350,7 +2350,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.09125891241801104 + "z": null }, { "area_code": "E14001298", @@ -2380,7 +2380,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 2.3466307469512855 + "z": null }, { "area_code": "E14001301", @@ -2390,7 +2390,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.8129947805259939 + "z": null }, { "area_code": "E14001302", @@ -2400,7 +2400,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.1485508154393418 + "z": null }, { "area_code": "E14001303", @@ -2420,7 +2420,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.7071130762347614 + "z": null }, { "area_code": "E14001305", @@ -2440,7 +2440,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.8311436718952614 + "z": null }, { "area_code": "E14001307", @@ -2450,7 +2450,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.43625011004596537 + "z": null }, { "area_code": "E14001308", @@ -2460,7 +2460,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1929409467273133 + "z": null }, { "area_code": "E14001309", @@ -2470,7 +2470,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.9674657996158076 + "z": null }, { "area_code": "E14001310", @@ -2480,7 +2480,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.5414488000122937 + "z": null }, { "area_code": "E14001311", @@ -2500,7 +2500,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6702791691393438 + "z": null }, { "area_code": "E14001313", @@ -2540,7 +2540,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 2.0125689899844588 + "z": null }, { "area_code": "E14001317", @@ -2550,7 +2550,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5007186806513056 + "z": null }, { "area_code": "E14001318", @@ -2560,7 +2560,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.829739188409867 + "z": null }, { "area_code": "E14001319", @@ -2570,7 +2570,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.391363374445718 + "z": null }, { "area_code": "E14001320", @@ -2590,7 +2590,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.3712196425687253 + "z": null }, { "area_code": "E14001322", @@ -2600,7 +2600,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.3693788726497518 + "z": null }, { "area_code": "E14001323", @@ -2610,7 +2610,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.928125656535068 + "z": null }, { "area_code": "E14001324", @@ -2630,7 +2630,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.2183565125413214 + "z": null }, { "area_code": "E14001326", @@ -2650,7 +2650,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4695635975456629 + "z": null }, { "area_code": "E14001328", @@ -2660,7 +2660,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.29769652335973656 + "z": null }, { "area_code": "E14001329", @@ -2720,7 +2720,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7079602911068119 + "z": null }, { "area_code": "E14001335", @@ -2750,7 +2750,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7575155963898628 + "z": null }, { "area_code": "E14001338", @@ -2770,7 +2770,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4280620884278869 + "z": null }, { "area_code": "E14001340", @@ -2780,7 +2780,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.664620252723077 + "z": null }, { "area_code": "E14001341", @@ -2800,7 +2800,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.48995997591467055 + "z": null }, { "area_code": "E14001343", @@ -2820,7 +2820,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.458008432485655 + "z": null }, { "area_code": "E14001345", @@ -2830,7 +2830,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5661148895714108 + "z": null }, { "area_code": "E14001346", @@ -2870,7 +2870,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.5739380720295577 + "z": null }, { "area_code": "E14001350", @@ -2890,7 +2890,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.495234426846877 + "z": null }, { "area_code": "E14001352", @@ -2930,7 +2930,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.33141381739506776 + "z": null }, { "area_code": "E14001356", @@ -2960,7 +2960,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.556436910949623 + "z": null }, { "area_code": "E14001359", @@ -2990,7 +2990,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7710734857093866 + "z": null }, { "area_code": "E14001362", @@ -3020,7 +3020,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.42739321798417074 + "z": null }, { "area_code": "E14001365", @@ -3050,7 +3050,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.570512978590799 + "z": null }, { "area_code": "E14001368", @@ -3080,7 +3080,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7075388854304887 + "z": null }, { "area_code": "E14001371", @@ -3090,7 +3090,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.68912726287325 + "z": null }, { "area_code": "E14001372", @@ -3110,7 +3110,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.49675372656528266 + "z": null }, { "area_code": "E14001374", @@ -3120,7 +3120,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5034942265392278 + "z": null }, { "area_code": "E14001375", @@ -3130,7 +3130,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.34126496441321036 + "z": null }, { "area_code": "E14001376", @@ -3150,7 +3150,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.24668384763741613 + "z": null }, { "area_code": "E14001378", @@ -3200,7 +3200,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.41698335625592653 + "z": null }, { "area_code": "E14001383", @@ -3210,7 +3210,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.2090869109633893 + "z": null }, { "area_code": "E14001384", @@ -3230,7 +3230,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3551695843762593 + "z": null }, { "area_code": "E14001386", @@ -3250,7 +3250,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.35386828135875736 + "z": null }, { "area_code": "E14001388", @@ -3270,7 +3270,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.406764528573411 + "z": null }, { "area_code": "E14001390", @@ -3280,7 +3280,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2626552997163804 + "z": null }, { "area_code": "E14001391", @@ -3300,7 +3300,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.44098723894278036 + "z": null }, { "area_code": "E14001393", @@ -3320,7 +3320,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4470361586141468 + "z": null }, { "area_code": "E14001395", @@ -3340,7 +3340,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.3353205966399994 + "z": null }, { "area_code": "E14001397", @@ -3360,7 +3360,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5064305775451065 + "z": null }, { "area_code": "E14001399", @@ -3380,7 +3380,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.9634199217711192 + "z": null }, { "area_code": "E14001401", @@ -3390,7 +3390,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.20218727544344936 + "z": null }, { "area_code": "E14001402", @@ -3410,7 +3410,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.35480790096879433 + "z": null }, { "area_code": "E14001404", @@ -3430,7 +3430,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.22772279087354985 + "z": null }, { "area_code": "E14001406", @@ -3470,7 +3470,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3032276566483965 + "z": null }, { "area_code": "E14001410", @@ -3480,7 +3480,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.29533869717959 + "z": null }, { "area_code": "E14001411", @@ -3490,7 +3490,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3576147478997206 + "z": null }, { "area_code": "E14001412", @@ -3500,7 +3500,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.775093453278694 + "z": null }, { "area_code": "E14001413", @@ -3560,7 +3560,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.22745832880364256 + "z": null }, { "area_code": "E14001419", @@ -3580,7 +3580,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5168797936121438 + "z": null }, { "area_code": "E14001421", @@ -3620,7 +3620,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4090980252539021 + "z": null }, { "area_code": "E14001425", @@ -3630,7 +3630,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.13975909550009658 + "z": null }, { "area_code": "E14001426", @@ -3650,7 +3650,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.26259907241829006 + "z": null }, { "area_code": "E14001428", @@ -3670,7 +3670,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.555694920224616 + "z": null }, { "area_code": "E14001430", @@ -3720,7 +3720,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6546499739753521 + "z": null }, { "area_code": "E14001435", @@ -3770,7 +3770,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5231676867771178 + "z": null }, { "area_code": "E14001440", @@ -3780,7 +3780,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.715595138225534 + "z": null }, { "area_code": "E14001441", @@ -3790,7 +3790,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8851339607545967 + "z": null }, { "area_code": "E14001442", @@ -3800,7 +3800,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.6136605727200248 + "z": null }, { "area_code": "E14001443", @@ -3820,7 +3820,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.21501674994262981 + "z": null }, { "area_code": "E14001445", @@ -3840,7 +3840,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5159863585262688 + "z": null }, { "area_code": "E14001447", @@ -3850,7 +3850,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4315092711285778 + "z": null }, { "area_code": "E14001448", @@ -3890,7 +3890,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.25620252281884265 + "z": null }, { "area_code": "E14001452", @@ -3910,7 +3910,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4673353795196942 + "z": null }, { "area_code": "E14001454", @@ -3930,7 +3930,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7593044445906483 + "z": null }, { "area_code": "E14001456", @@ -3950,7 +3950,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.42051186089930004 + "z": null }, { "area_code": "E14001458", @@ -4000,7 +4000,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.11895920031389433 + "z": null }, { "area_code": "E14001463", @@ -4040,7 +4040,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.2016360120000744 + "z": null }, { "area_code": "E14001467", @@ -4060,7 +4060,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.29459106484563485 + "z": null }, { "area_code": "E14001469", @@ -4070,7 +4070,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.125390813361269 + "z": null }, { "area_code": "E14001470", @@ -4090,7 +4090,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.590197834273257 + "z": null }, { "area_code": "E14001472", @@ -4180,7 +4180,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.34796991784098863 + "z": null }, { "area_code": "E14001481", @@ -4190,7 +4190,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.28881504521729234 + "z": null }, { "area_code": "E14001482", @@ -4220,7 +4220,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41024455243289326 + "z": null }, { "area_code": "E14001485", @@ -4240,7 +4240,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.6416753339771217 + "z": null }, { "area_code": "E14001487", @@ -4250,7 +4250,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.48748006644614 + "z": null }, { "area_code": "E14001488", @@ -4280,7 +4280,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4217570794107836 + "z": null }, { "area_code": "E14001491", @@ -4300,7 +4300,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.46469762883118776 + "z": null }, { "area_code": "E14001493", @@ -4320,7 +4320,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.557948561396511 + "z": null }, { "area_code": "E14001495", @@ -4330,7 +4330,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3954561287226731 + "z": null }, { "area_code": "E14001496", @@ -4340,7 +4340,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.6071879574253543 + "z": null }, { "area_code": "E14001497", @@ -4350,7 +4350,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2593916101032326 + "z": null }, { "area_code": "E14001498", @@ -4360,7 +4360,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3857729822945046 + "z": null }, { "area_code": "E14001499", @@ -4380,7 +4380,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.521378648457463 + "z": null }, { "area_code": "E14001501", @@ -4400,7 +4400,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.26620729548201666 + "z": null }, { "area_code": "E14001503", @@ -4420,7 +4420,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.41523594238541134 + "z": null }, { "area_code": "E14001505", @@ -4440,7 +4440,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.21570929994891677 + "z": null }, { "area_code": "E14001507", @@ -4450,7 +4450,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.3637276115502675 + "z": null }, { "area_code": "E14001508", @@ -4470,7 +4470,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4082577667806476 + "z": null }, { "area_code": "E14001510", @@ -4480,7 +4480,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.71069958831694 + "z": null }, { "area_code": "E14001511", @@ -4520,7 +4520,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 2.0410283614693205 + "z": null }, { "area_code": "E14001515", @@ -4530,7 +4530,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4523679227618345 + "z": null }, { "area_code": "E14001516", @@ -4550,7 +4550,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.35124562206592874 + "z": null }, { "area_code": "E14001518", @@ -4560,7 +4560,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.43496808700484496 + "z": null }, { "area_code": "E14001519", @@ -4570,7 +4570,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.55500246984011 + "z": null }, { "area_code": "E14001520", @@ -4600,7 +4600,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5936946588577345 + "z": null }, { "area_code": "E14001523", @@ -4660,7 +4660,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.718131341339497 + "z": null }, { "area_code": "E14001529", @@ -4680,7 +4680,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.29196643568833125 + "z": null }, { "area_code": "E14001531", @@ -4690,7 +4690,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4175291969058184 + "z": null }, { "area_code": "E14001532", @@ -4720,7 +4720,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.766023806914981 + "z": null }, { "area_code": "E14001535", @@ -4750,7 +4750,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.27358381740829496 + "z": null }, { "area_code": "E14001538", @@ -4760,7 +4760,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8759480843946659 + "z": null }, { "area_code": "E14001539", @@ -4810,7 +4810,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5021052601823875 + "z": null }, { "area_code": "E14001544", @@ -4820,7 +4820,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1515667397870977 + "z": null }, { "area_code": "E14001545", @@ -4830,7 +4830,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.6801702827517055 + "z": null }, { "area_code": "E14001546", @@ -4850,7 +4850,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4883831407309799 + "z": null }, { "area_code": "E14001548", @@ -4890,7 +4890,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.4041537510629771 + "z": null }, { "area_code": "E14001552", @@ -4920,7 +4920,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41064611329339074 + "z": null }, { "area_code": "E14001555", @@ -4930,7 +4930,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.5643387263910384 + "z": null }, { "area_code": "E14001556", @@ -4960,7 +4960,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 2.160745604436413 + "z": null }, { "area_code": "E14001559", @@ -4980,7 +4980,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.1837023809542946 + "z": null }, { "area_code": "E14001561", @@ -4990,7 +4990,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4249459583052577 + "z": null }, { "area_code": "E14001562", @@ -5000,7 +5000,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4926687980102825 + "z": null }, { "area_code": "E14001563", @@ -5040,7 +5040,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.47170928725815137 + "z": null }, { "area_code": "E14001567", @@ -5050,7 +5050,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4560100568828488 + "z": null }, { "area_code": "E14001568", @@ -5060,7 +5060,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.4593835193283042 + "z": null }, { "area_code": "E14001569", @@ -5070,7 +5070,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5913357750521941 + "z": null }, { "area_code": "E14001570", @@ -5080,7 +5080,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5131166343804763 + "z": null }, { "area_code": "E14001571", @@ -5110,7 +5110,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.26824288708579963 + "z": null }, { "area_code": "E14001574", @@ -5140,7 +5140,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5914860168640855 + "z": null }, { "area_code": "E14001577", @@ -5170,7 +5170,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.508290431409214 + "z": null }, { "area_code": "E14001580", @@ -5200,7 +5200,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.4139689826450081 + "z": null }, { "area_code": "E14001583", @@ -5280,7 +5280,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.29581898562110054 + "z": null }, { "area_code": "E14001591", @@ -5300,7 +5300,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.432078716043809 + "z": null }, { "area_code": "E14001593", @@ -5320,7 +5320,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8468925347336995 + "z": null }, { "area_code": "E14001595", @@ -5340,7 +5340,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.46658173820862037 + "z": null }, { "area_code": "E14001597", @@ -5350,7 +5350,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4772805359778839 + "z": null }, { "area_code": "E14001598", @@ -5370,7 +5370,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.340633490819473 + "z": null }, { "area_code": "E14001600", @@ -5380,7 +5380,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7112354885254668 + "z": null }, { "area_code": "E14001601", @@ -5410,7 +5410,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.4372638046504163 + "z": null }, { "area_code": "E14001604", @@ -5430,7 +5430,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.23540415564794806 + "z": null }, { "area_code": "N05000001", @@ -5500,7 +5500,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.7214627704887812 + "z": null }, { "area_code": "N05000008", @@ -5530,7 +5530,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.145240088197603 + "z": null }, { "area_code": "N05000011", @@ -5550,7 +5550,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.7044614834659986 + "z": null }, { "area_code": "N05000013", @@ -5570,7 +5570,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.709427166240236 + "z": null }, { "area_code": "N05000015", @@ -5610,7 +5610,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.170623346112067 + "z": null }, { "area_code": "S14000021", @@ -5640,7 +5640,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24509068046274338 + "z": null }, { "area_code": "S14000048", @@ -5700,7 +5700,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.2512150337392181 + "z": null }, { "area_code": "S14000064", @@ -5710,7 +5710,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.18230273032997046 + "z": null }, { "area_code": "S14000065", @@ -5720,7 +5720,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.11966858683050367 + "z": null }, { "area_code": "S14000066", @@ -5730,7 +5730,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.13058440677690267 + "z": null }, { "area_code": "S14000067", @@ -5810,7 +5810,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24773898779591202 + "z": null }, { "area_code": "S14000075", @@ -5820,7 +5820,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.03648593640719042 + "z": null }, { "area_code": "S14000076", @@ -5880,7 +5880,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.06169903648439683 + "z": null }, { "area_code": "S14000082", @@ -5890,7 +5890,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.17133210531946164 + "z": null }, { "area_code": "S14000083", @@ -5910,7 +5910,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.07859810770070573 + "z": null }, { "area_code": "S14000085", @@ -5940,7 +5940,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.14144335273594033 + "z": null }, { "area_code": "S14000088", @@ -5950,7 +5950,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.19367851664842178 + "z": null }, { "area_code": "S14000089", @@ -5960,7 +5960,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.1124288862243548 + "z": null }, { "area_code": "S14000090", @@ -6000,7 +6000,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.331071216210559 + "z": null }, { "area_code": "S14000094", @@ -6020,7 +6020,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.17149255556728069 + "z": null }, { "area_code": "S14000096", @@ -6030,7 +6030,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3527723556005102 + "z": null }, { "area_code": "S14000097", @@ -6040,7 +6040,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.4108193151776318 + "z": null }, { "area_code": "S14000098", @@ -6060,7 +6060,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.16959175344063188 + "z": null }, { "area_code": "S14000100", @@ -6070,7 +6070,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24541205143468758 + "z": null }, { "area_code": "S14000101", @@ -6100,7 +6100,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.10348326197072943 + "z": null }, { "area_code": "S14000104", @@ -6110,7 +6110,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3247849755429084 + "z": null }, { "area_code": "S14000105", @@ -6120,7 +6120,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.25743420351537 + "z": null }, { "area_code": "S14000106", @@ -6130,7 +6130,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.2778586076938372 + "z": null }, { "area_code": "S14000107", @@ -6150,7 +6150,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.1200820383390516 + "z": null }, { "area_code": "S14000109", @@ -6160,7 +6160,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3775892756644503 + "z": null }, { "area_code": "S14000110", @@ -6170,7 +6170,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.1697290379737109 + "z": null }, { "area_code": "S14000111", @@ -6190,7 +6190,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.03151905600890873 + "z": null }, { "area_code": "W07000082", @@ -6230,7 +6230,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.0321981499825186 + "z": null }, { "area_code": "W07000086", @@ -6250,7 +6250,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.028166486292114375 + "z": null }, { "area_code": "W07000088", @@ -6260,7 +6260,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.0431570567372324 + "z": null }, { "area_code": "W07000089", @@ -6300,7 +6300,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.03562895178764713 + "z": null }, { "area_code": "W07000093", @@ -6320,7 +6320,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.00885613296933634 + "z": null }, { "area_code": "W07000095", @@ -6330,7 +6330,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.9295677867785186 + "z": null }, { "area_code": "W07000096", @@ -6340,7 +6340,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.001771247064405 + "z": null }, { "area_code": "W07000097", @@ -6350,7 +6350,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.0031688977450496218 + "z": null }, { "area_code": "W07000098", @@ -6410,7 +6410,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.029708988420561095 + "z": null }, { "area_code": "W07000104", @@ -6430,7 +6430,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.024604935459826355 + "z": null }, { "area_code": "W07000106", @@ -6450,7 +6450,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.04471363989224115 + "z": null }, { "area_code": "W07000108", @@ -6460,7 +6460,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.8797410391969579 + "z": null }, { "area_code": "W07000109", @@ -6470,7 +6470,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.04574454319613207 + "z": null }, { "area_code": "W07000110", @@ -6480,7 +6480,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.025739521264306193 + "z": null }, { "area_code": "W07000111", @@ -6490,7 +6490,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.0082704021049473 + "z": null }, { "area_code": "W07000112", @@ -6520,7 +6520,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.130990971476548 + "z": null }, { "area_code": "E06000003", @@ -6530,7 +6530,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.1233467785391877 + "z": null }, { "area_code": "E06000004", @@ -6540,7 +6540,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.6648235512243621 + "z": null }, { "area_code": "E06000005", @@ -6560,7 +6560,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.2589971357974303 + "z": null }, { "area_code": "E06000007", @@ -6580,7 +6580,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.1135859684420129 + "z": null }, { "area_code": "E06000009", @@ -6630,7 +6630,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.477596225879552 + "z": null }, { "area_code": "E06000014", @@ -6640,7 +6640,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.5386764826116722 + "z": null }, { "area_code": "E06000015", @@ -6660,7 +6660,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -0.00036987125822950486 + "z": null }, { "area_code": "E06000017", @@ -6700,7 +6700,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.06059738026725428 + "z": null }, { "area_code": "E06000021", @@ -6760,7 +6760,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.09672453590603645 + "z": null }, { "area_code": "E06000027", @@ -6770,7 +6770,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.9645767861386046 + "z": null }, { "area_code": "E06000030", @@ -6780,7 +6780,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -0.4575912651657026 + "z": null }, { "area_code": "E06000031", @@ -6790,7 +6790,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.40617367577100816 + "z": null }, { "area_code": "E06000032", @@ -6800,7 +6800,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.5174548404171305 + "z": null }, { "area_code": "E06000033", @@ -6810,7 +6810,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.3161308241489168 + "z": null }, { "area_code": "E06000034", @@ -6830,7 +6830,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.583679749940298 + "z": null }, { "area_code": "E06000036", @@ -6860,7 +6860,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.07910742927577243 + "z": null }, { "area_code": "E06000039", @@ -6900,7 +6900,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.15807751432654354 + "z": null }, { "area_code": "E06000043", @@ -6940,7 +6940,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.020608594678270798 + "z": null }, { "area_code": "E06000047", @@ -6970,7 +6970,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.2405479375185253 + "z": null }, { "area_code": "E06000051", @@ -6980,7 +6980,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.707707956877534 + "z": null }, { "area_code": "E06000052", @@ -7040,7 +7040,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.8892723849865242 + "z": null }, { "area_code": "E06000058", @@ -7060,7 +7060,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -1.0973772979313916 + "z": null }, { "area_code": "E06000060", @@ -7090,7 +7090,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -1.089462436046842 + "z": null }, { "area_code": "E06000063", @@ -7100,7 +7100,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.06587315308211047 + "z": null }, { "area_code": "E06000064", @@ -7110,7 +7110,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.4666109347701939 + "z": null }, { "area_code": "E06000065", @@ -7160,7 +7160,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2626552997163804 + "z": null }, { "area_code": "E07000011", @@ -7170,7 +7170,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.28444157237852297 + "z": null }, { "area_code": "E07000012", @@ -7180,7 +7180,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.15812680265576556 + "z": null }, { "area_code": "E07000032", @@ -7210,7 +7210,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2853365706894731 + "z": null }, { "area_code": "E07000035", @@ -7220,7 +7220,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.7117577025563167 + "z": null }, { "area_code": "E07000036", @@ -7240,7 +7240,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.45403959903953134 + "z": null }, { "area_code": "E07000038", @@ -7280,7 +7280,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.15131387447055292 + "z": null }, { "area_code": "E07000042", @@ -7290,7 +7290,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.5209475865159013 + "z": null }, { "area_code": "E07000043", @@ -7300,7 +7300,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.35386828135875736 + "z": null }, { "area_code": "E07000044", @@ -7310,7 +7310,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4535358467577024 + "z": null }, { "area_code": "E07000045", @@ -7320,7 +7320,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.027325966899906472 + "z": null }, { "area_code": "E07000046", @@ -7360,7 +7360,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4499684054422251 + "z": null }, { "area_code": "E07000063", @@ -7380,7 +7380,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.42048506573527417 + "z": null }, { "area_code": "E07000065", @@ -7390,7 +7390,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.0711133078709747 + "z": null }, { "area_code": "E07000066", @@ -7400,7 +7400,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.5941808108428601 + "z": null }, { "area_code": "E07000067", @@ -7440,7 +7440,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.28811747505336877 + "z": null }, { "area_code": "E07000071", @@ -7450,7 +7450,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.3494048649454273 + "z": null }, { "area_code": "E07000072", @@ -7470,7 +7470,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.41751933636408917 + "z": null }, { "area_code": "E07000074", @@ -7480,7 +7480,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.7455017193495017 + "z": null }, { "area_code": "E07000075", @@ -7500,7 +7500,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.1590390291573623 + "z": null }, { "area_code": "E07000077", @@ -7520,7 +7520,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.14799277794263882 + "z": null }, { "area_code": "E07000079", @@ -7550,7 +7550,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.09641609653420453 + "z": null }, { "area_code": "E07000082", @@ -7580,7 +7580,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.731009006859017 + "z": null }, { "area_code": "E07000085", @@ -7600,7 +7600,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.1832962399006122 + "z": null }, { "area_code": "E07000087", @@ -7610,7 +7610,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.26646059238550623 + "z": null }, { "area_code": "E07000088", @@ -7620,7 +7620,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.584904575007004 + "z": null }, { "area_code": "E07000089", @@ -7630,7 +7630,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.46309348668390066 + "z": null }, { "area_code": "E07000090", @@ -7640,7 +7640,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.2714132138089886 + "z": null }, { "area_code": "E07000091", @@ -7650,7 +7650,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.7109403525525836 + "z": null }, { "area_code": "E07000092", @@ -7660,7 +7660,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.48851544062700314 + "z": null }, { "area_code": "E07000093", @@ -7690,7 +7690,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.367065739389646 + "z": null }, { "area_code": "E07000096", @@ -7700,7 +7700,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.8713645673543226 + "z": null }, { "area_code": "E07000098", @@ -7710,7 +7710,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.4636427821273117 + "z": null }, { "area_code": "E07000099", @@ -7720,7 +7720,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.02923946777942345 + "z": null }, { "area_code": "E07000102", @@ -7730,7 +7730,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.696893062233561 + "z": null }, { "area_code": "E07000103", @@ -7740,7 +7740,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5802034446547915 + "z": null }, { "area_code": "E07000105", @@ -7760,7 +7760,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.9903881200615484 + "z": null }, { "area_code": "E07000107", @@ -7790,7 +7790,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4245799682733015 + "z": null }, { "area_code": "E07000110", @@ -7830,7 +7830,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.05790086110898173 + "z": null }, { "area_code": "E07000114", @@ -7840,7 +7840,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.027541227447640197 + "z": null }, { "area_code": "E07000115", @@ -7850,7 +7850,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.1701536834214769 + "z": null }, { "area_code": "E07000116", @@ -7860,7 +7860,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.423927657687799 + "z": null }, { "area_code": "E07000117", @@ -7910,7 +7910,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.07206007055794207 + "z": null }, { "area_code": "E07000122", @@ -7930,7 +7930,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.08612968438426084 + "z": null }, { "area_code": "E07000124", @@ -7940,7 +7940,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.9733209403654685 + "z": null }, { "area_code": "E07000125", @@ -7980,7 +7980,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.27925846079169553 + "z": null }, { "area_code": "E07000129", @@ -7990,7 +7990,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.40555237856446674 + "z": null }, { "area_code": "E07000130", @@ -8000,7 +8000,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -0.15689508271601793 + "z": null }, { "area_code": "E07000131", @@ -8060,7 +8060,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.7560160180304334 + "z": null }, { "area_code": "E07000137", @@ -8100,7 +8100,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.7154480294236794 + "z": null }, { "area_code": "E07000141", @@ -8120,7 +8120,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.41469814337084093 + "z": null }, { "area_code": "E07000143", @@ -8150,7 +8150,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2519175370702735 + "z": null }, { "area_code": "E07000146", @@ -8160,7 +8160,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.7634111727840195 + "z": null }, { "area_code": "E07000147", @@ -8170,7 +8170,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.17542909856107242 + "z": null }, { "area_code": "E07000148", @@ -8180,7 +8180,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.8297484661514863 + "z": null }, { "area_code": "E07000149", @@ -8190,7 +8190,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.07227568695370964 + "z": null }, { "area_code": "E07000170", @@ -8200,7 +8200,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.2590578772171588 + "z": null }, { "area_code": "E07000171", @@ -8220,7 +8220,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.4484663142324123 + "z": null }, { "area_code": "E07000173", @@ -8240,7 +8240,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2832984722074241 + "z": null }, { "area_code": "E07000175", @@ -8250,7 +8250,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.2968572790208523 + "z": null }, { "area_code": "E07000176", @@ -8260,7 +8260,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2396809179035498 + "z": null }, { "area_code": "E07000177", @@ -8290,7 +8290,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.051903570193828 + "z": null }, { "area_code": "E07000180", @@ -8300,7 +8300,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.2499959486055532 + "z": null }, { "area_code": "E07000181", @@ -8360,7 +8360,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.43087789821744255 + "z": null }, { "area_code": "E07000197", @@ -8370,7 +8370,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.19024731903911418 + "z": null }, { "area_code": "E07000198", @@ -8380,7 +8380,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8487896188733013 + "z": null }, { "area_code": "E07000199", @@ -8390,7 +8390,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 2.303136535885502 + "z": null }, { "area_code": "E07000200", @@ -8400,7 +8400,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.557948561396511 + "z": null }, { "area_code": "E07000202", @@ -8410,7 +8410,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.9442884252856386 + "z": null }, { "area_code": "E07000203", @@ -8420,7 +8420,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2408163104761246 + "z": null }, { "area_code": "E07000207", @@ -8430,7 +8430,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.12976829043728363 + "z": null }, { "area_code": "E07000208", @@ -8460,7 +8460,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5814849033030273 + "z": null }, { "area_code": "E07000211", @@ -8470,7 +8470,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.0799380715511866 + "z": null }, { "area_code": "E07000212", @@ -8510,7 +8510,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5939900420152026 + "z": null }, { "area_code": "E07000216", @@ -8520,7 +8520,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.18445679604453516 + "z": null }, { "area_code": "E07000217", @@ -8530,7 +8530,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.432078716043809 + "z": null }, { "area_code": "E07000218", @@ -8540,7 +8540,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.0008529753213318 + "z": null }, { "area_code": "E07000219", @@ -8550,7 +8550,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.23854163889722768 + "z": null }, { "area_code": "E07000220", @@ -8560,7 +8560,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4198516573375252 + "z": null }, { "area_code": "E07000221", @@ -8570,7 +8570,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.19343487822854089 + "z": null }, { "area_code": "E07000222", @@ -8580,7 +8580,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.2056266956789221 + "z": null }, { "area_code": "E07000223", @@ -8600,7 +8600,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.8020744486840312 + "z": null }, { "area_code": "E07000225", @@ -8620,7 +8620,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.33619397532209666 + "z": null }, { "area_code": "E07000227", @@ -8630,7 +8630,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.021881830161520767 + "z": null }, { "area_code": "E07000228", @@ -8670,7 +8670,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.7304282269848646 + "z": null }, { "area_code": "E07000236", @@ -8680,7 +8680,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.6922885953465575 + "z": null }, { "area_code": "E07000237", @@ -8690,7 +8690,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4772805359778839 + "z": null }, { "area_code": "E07000238", @@ -8700,7 +8700,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.3591735223233599 + "z": null }, { "area_code": "E07000239", @@ -8720,7 +8720,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.04246375867109319 + "z": null }, { "area_code": "E07000241", @@ -8730,7 +8730,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.21397102628049783 + "z": null }, { "area_code": "E07000242", @@ -8790,7 +8790,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.19429907306376437 + "z": null }, { "area_code": "E08000003", @@ -8820,7 +8820,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.31419116742331493 + "z": null }, { "area_code": "E08000006", @@ -8850,7 +8850,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.40117548110251866 + "z": null }, { "area_code": "E08000009", @@ -8880,7 +8880,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.013548892332739936 + "z": null }, { "area_code": "E08000012", @@ -8910,7 +8910,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.09839994477645013 + "z": null }, { "area_code": "E08000015", @@ -8920,7 +8920,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.12399221300275691 + "z": null }, { "area_code": "E08000016", @@ -8950,7 +8950,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.8546222558501688 + "z": null }, { "area_code": "E08000019", @@ -8970,7 +8970,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.6972999401841465 + "z": null }, { "area_code": "E08000022", @@ -8990,7 +8990,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.04734895138734082 + "z": null }, { "area_code": "E08000024", @@ -9020,7 +9020,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.09233754545541237 + "z": null }, { "area_code": "E08000027", @@ -9030,7 +9030,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.06648666147512776 + "z": null }, { "area_code": "E08000028", @@ -9040,7 +9040,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.631123304220133 + "z": null }, { "area_code": "E08000029", @@ -9050,7 +9050,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.21295086936470148 + "z": null }, { "area_code": "E08000030", @@ -9060,7 +9060,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.4654496569540146 + "z": null }, { "area_code": "E08000031", @@ -9090,7 +9090,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.5997081920374492 + "z": null }, { "area_code": "E08000034", @@ -9130,7 +9130,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.5711574826001733 + "z": null }, { "area_code": "E09000001", @@ -9180,7 +9180,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.30832272431292346 + "z": null }, { "area_code": "E09000006", @@ -9190,7 +9190,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.4469128414655834 + "z": null }, { "area_code": "E09000007", @@ -9200,7 +9200,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.9310258253064386 + "z": null }, { "area_code": "E09000008", @@ -9210,7 +9210,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.5780573338462608 + "z": null }, { "area_code": "E09000009", @@ -9240,7 +9240,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.616206708255202 + "z": null }, { "area_code": "E09000012", @@ -9250,7 +9250,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.1837480178571041 + "z": null }, { "area_code": "E09000013", @@ -9260,7 +9260,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.06913097510601028 + "z": null }, { "area_code": "E09000014", @@ -9310,7 +9310,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7662081875799396 + "z": null }, { "area_code": "E09000019", @@ -9320,7 +9320,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.8730406481713173 + "z": null }, { "area_code": "E09000020", @@ -9340,7 +9340,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.27219538795607895 + "z": null }, { "area_code": "E09000022", @@ -9370,7 +9370,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.08006947430595814 + "z": null }, { "area_code": "E09000025", @@ -9380,7 +9380,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.2940081072381655 + "z": null }, { "area_code": "E09000026", @@ -9410,7 +9410,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.41956155468736805 + "z": null }, { "area_code": "E09000029", @@ -9420,7 +9420,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.1099427360431473 + "z": null }, { "area_code": "E09000030", @@ -9440,7 +9440,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.17652391454729638 + "z": null }, { "area_code": "E09000032", @@ -9450,7 +9450,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.4609040342915792 + "z": null }, { "area_code": "E09000033", @@ -9520,7 +9520,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.3555665402689974 + "z": null }, { "area_code": "N09000007", @@ -9550,7 +9550,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.1228612492321715 + "z": null }, { "area_code": "N09000010", @@ -9560,7 +9560,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.960185329533569 + "z": null }, { "area_code": "N09000011", @@ -9580,7 +9580,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.807511605214467 + "z": null }, { "area_code": "S12000006", @@ -9600,7 +9600,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.06369896847580099 + "z": null }, { "area_code": "S12000010", @@ -9660,7 +9660,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.5500251400772276 + "z": null }, { "area_code": "S12000019", @@ -9670,7 +9670,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24509068046274338 + "z": null }, { "area_code": "S12000020", @@ -9690,7 +9690,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.20880505911201006 + "z": null }, { "area_code": "S12000023", @@ -9710,7 +9710,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.06708736144866549 + "z": null }, { "area_code": "S12000027", @@ -9730,7 +9730,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.0007536196609207166 + "z": null }, { "area_code": "S12000029", @@ -9750,7 +9750,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.4085863662580551 + "z": null }, { "area_code": "S12000033", @@ -9810,7 +9810,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.330814816547259 + "z": null }, { "area_code": "S12000040", @@ -9820,7 +9820,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.43557405568150964 + "z": null }, { "area_code": "S12000041", @@ -9830,7 +9830,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.9732008080201083 + "z": null }, { "area_code": "S12000042", @@ -9840,7 +9840,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.2997738928489757 + "z": null }, { "area_code": "S12000045", @@ -9850,7 +9850,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.1959408674976184 + "z": null }, { "area_code": "S12000047", @@ -9860,7 +9860,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -1.2910340970309047 + "z": null }, { "area_code": "S12000048", @@ -9870,7 +9870,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.3016585489161866 + "z": null }, { "area_code": "S12000049", @@ -9890,7 +9890,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -1.1493236832583618 + "z": null }, { "area_code": "W06000001", @@ -9910,7 +9910,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.6896832442368885 + "z": null }, { "area_code": "W06000003", @@ -9920,7 +9920,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.7443665717310806 + "z": null }, { "area_code": "W06000004", @@ -9940,7 +9940,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.48344119155916293 + "z": null }, { "area_code": "W06000006", @@ -9950,7 +9950,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.5364324792076375 + "z": null }, { "area_code": "W06000008", @@ -10000,7 +10000,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.4293291631864791 + "z": null }, { "area_code": "W06000013", @@ -10020,7 +10020,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.2961128495386596 + "z": null }, { "area_code": "W06000015", @@ -10050,7 +10050,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.15222313404156732 + "z": null }, { "area_code": "W06000019", @@ -10070,7 +10070,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.04574454319613207 + "z": null }, { "area_code": "W06000021", @@ -10090,7 +10090,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.4757646896711776 + "z": null }, { "area_code": "W06000023", @@ -10100,7 +10100,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.5038131748794512 + "z": null }, { "area_code": "W06000024", @@ -11258,7 +11258,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4629811535772127 + "z": null }, { "area_code": "E14001067", @@ -11278,7 +11278,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.44990246350253255 + "z": null }, { "area_code": "E14001069", @@ -11288,7 +11288,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.40959089245014424 + "z": null }, { "area_code": "E14001070", @@ -11298,7 +11298,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4489264222605665 + "z": null }, { "area_code": "E14001071", @@ -11358,7 +11358,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.6022924147600126 + "z": null }, { "area_code": "E14001077", @@ -11388,7 +11388,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3376120583190917 + "z": null }, { "area_code": "E14001080", @@ -11418,7 +11418,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4863006361232289 + "z": null }, { "area_code": "E14001083", @@ -11468,7 +11468,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.23874262030272575 + "z": null }, { "area_code": "E14001088", @@ -11518,7 +11518,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5160318036913791 + "z": null }, { "area_code": "E14001093", @@ -11548,7 +11548,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5247848970919522 + "z": null }, { "area_code": "E14001096", @@ -11568,7 +11568,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.46012072599761 + "z": null }, { "area_code": "E14001098", @@ -11578,7 +11578,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.608517044291883 + "z": null }, { "area_code": "E14001099", @@ -11588,7 +11588,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5579713533258863 + "z": null }, { "area_code": "E14001100", @@ -11638,7 +11638,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.40741825755600247 + "z": null }, { "area_code": "E14001105", @@ -11658,7 +11658,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4597236432850999 + "z": null }, { "area_code": "E14001107", @@ -11678,7 +11678,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.2977241251274199 + "z": null }, { "area_code": "E14001109", @@ -11698,7 +11698,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3331871728460651 + "z": null }, { "area_code": "E14001111", @@ -11738,7 +11738,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2376731249692524 + "z": null }, { "area_code": "E14001115", @@ -11748,7 +11748,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.2716937060337006 + "z": null }, { "area_code": "E14001116", @@ -11758,7 +11758,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.4887037079104486 + "z": null }, { "area_code": "E14001117", @@ -11768,7 +11768,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4478885094648818 + "z": null }, { "area_code": "E14001118", @@ -11788,7 +11788,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1505010035482153 + "z": null }, { "area_code": "E14001120", @@ -11798,7 +11798,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.25751897508300897 + "z": null }, { "area_code": "E14001121", @@ -11848,7 +11848,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5309594067562815 + "z": null }, { "area_code": "E14001126", @@ -11858,7 +11858,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.5667124892209705 + "z": null }, { "area_code": "E14001127", @@ -11888,7 +11888,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.43128653915634174 + "z": null }, { "area_code": "E14001130", @@ -11908,7 +11908,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4796376802519078 + "z": null }, { "area_code": "E14001132", @@ -11958,7 +11958,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5338301894534232 + "z": null }, { "area_code": "E14001137", @@ -11968,7 +11968,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.830774001429061 + "z": null }, { "area_code": "E14001138", @@ -11978,7 +11978,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5607700542508336 + "z": null }, { "area_code": "E14001139", @@ -11988,7 +11988,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2748417359533837 + "z": null }, { "area_code": "E14001140", @@ -11998,7 +11998,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.692404633441529 + "z": null }, { "area_code": "E14001141", @@ -12038,7 +12038,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.6656428777692758 + "z": null }, { "area_code": "E14001145", @@ -12048,7 +12048,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4357658400747256 + "z": null }, { "area_code": "E14001146", @@ -12088,7 +12088,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2513712471807038 + "z": null }, { "area_code": "E14001150", @@ -12098,7 +12098,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5066449872108447 + "z": null }, { "area_code": "E14001151", @@ -12118,7 +12118,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.3293800655156362 + "z": null }, { "area_code": "E14001153", @@ -12148,7 +12148,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41346183321298596 + "z": null }, { "area_code": "E14001156", @@ -12158,7 +12158,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.5860394631212886 + "z": null }, { "area_code": "E14001157", @@ -12178,7 +12178,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5274185427869791 + "z": null }, { "area_code": "E14001159", @@ -12188,7 +12188,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.24014962819539287 + "z": null }, { "area_code": "E14001160", @@ -12198,7 +12198,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.48718432644433524 + "z": null }, { "area_code": "E14001161", @@ -12228,7 +12228,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4640309367562743 + "z": null }, { "area_code": "E14001164", @@ -12248,7 +12248,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3895233507633208 + "z": null }, { "area_code": "E14001166", @@ -12258,7 +12258,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.33556783793954365 + "z": null }, { "area_code": "E14001167", @@ -12268,7 +12268,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7455957280024599 + "z": null }, { "area_code": "E14001168", @@ -12278,7 +12278,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.40713823315346953 + "z": null }, { "area_code": "E14001169", @@ -12358,7 +12358,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3016787075037808 + "z": null }, { "area_code": "E14001177", @@ -12368,7 +12368,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.3408499088728592 + "z": null }, { "area_code": "E14001178", @@ -12378,7 +12378,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5083990706158836 + "z": null }, { "area_code": "E14001179", @@ -12428,7 +12428,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4040217784284127 + "z": null }, { "area_code": "E14001184", @@ -12488,7 +12488,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7443653415443071 + "z": null }, { "area_code": "E14001190", @@ -12498,7 +12498,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.3954685761964229 + "z": null }, { "area_code": "E14001191", @@ -12518,7 +12518,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.382839873124869 + "z": null }, { "area_code": "E14001193", @@ -12528,7 +12528,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.39259841886785524 + "z": null }, { "area_code": "E14001194", @@ -12538,7 +12538,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.35112752196504643 + "z": null }, { "area_code": "E14001195", @@ -12558,7 +12558,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.29889559365839535 + "z": null }, { "area_code": "E14001197", @@ -12588,7 +12588,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.4005509261697693 + "z": null }, { "area_code": "E14001200", @@ -12608,7 +12608,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.49103746506670576 + "z": null }, { "area_code": "E14001202", @@ -12638,7 +12638,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5517486925525881 + "z": null }, { "area_code": "E14001205", @@ -12648,7 +12648,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7096016536744152 + "z": null }, { "area_code": "E14001206", @@ -12668,7 +12668,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.4945446583121496 + "z": null }, { "area_code": "E14001208", @@ -12678,7 +12678,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 2.083669546044031 + "z": null }, { "area_code": "E14001209", @@ -12698,7 +12698,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4961964322302507 + "z": null }, { "area_code": "E14001211", @@ -12728,7 +12728,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.8894842948123709 + "z": null }, { "area_code": "E14001214", @@ -12738,7 +12738,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5140898001261861 + "z": null }, { "area_code": "E14001215", @@ -12748,7 +12748,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4686463318112006 + "z": null }, { "area_code": "E14001216", @@ -12778,7 +12778,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4049600128070073 + "z": null }, { "area_code": "E14001219", @@ -12838,7 +12838,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3841461664767194 + "z": null }, { "area_code": "E14001225", @@ -12878,7 +12878,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.408197052969491 + "z": null }, { "area_code": "E14001229", @@ -12898,7 +12898,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.44003760477810105 + "z": null }, { "area_code": "E14001231", @@ -12908,7 +12908,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4247330898615719 + "z": null }, { "area_code": "E14001232", @@ -12918,7 +12918,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3634925364722211 + "z": null }, { "area_code": "E14001233", @@ -12928,7 +12928,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.6218811028825175 + "z": null }, { "area_code": "E14001234", @@ -12938,7 +12938,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.6932968342691244 + "z": null }, { "area_code": "E14001235", @@ -12948,7 +12948,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.464226899598547 + "z": null }, { "area_code": "E14001236", @@ -12968,7 +12968,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.37427747457359234 + "z": null }, { "area_code": "E14001238", @@ -12998,7 +12998,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.7177978541194343 + "z": null }, { "area_code": "E14001241", @@ -13008,7 +13008,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4676654429170951 + "z": null }, { "area_code": "E14001242", @@ -13018,7 +13018,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.39247948968605795 + "z": null }, { "area_code": "E14001243", @@ -13038,7 +13038,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.3980940770134267 + "z": null }, { "area_code": "E14001245", @@ -13048,7 +13048,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3625838019701458 + "z": null }, { "area_code": "E14001246", @@ -13068,7 +13068,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4315098080022334 + "z": null }, { "area_code": "E14001248", @@ -13078,7 +13078,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.2300800891804827 + "z": null }, { "area_code": "E14001249", @@ -13088,7 +13088,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7328498663445133 + "z": null }, { "area_code": "E14001250", @@ -13098,7 +13098,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.13451402840770524 + "z": null }, { "area_code": "E14001251", @@ -13128,7 +13128,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3971107120555062 + "z": null }, { "area_code": "E14001254", @@ -13138,7 +13138,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4245799682733015 + "z": null }, { "area_code": "E14001255", @@ -13148,7 +13148,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.049890526602419424 + "z": null }, { "area_code": "E14001256", @@ -13198,7 +13198,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5986536851083301 + "z": null }, { "area_code": "E14001261", @@ -13208,7 +13208,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5985214453258789 + "z": null }, { "area_code": "E14001262", @@ -13228,7 +13228,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.41041333036795014 + "z": null }, { "area_code": "E14001264", @@ -13238,7 +13238,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5308693681713419 + "z": null }, { "area_code": "E14001265", @@ -13248,7 +13248,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5134184042016835 + "z": null }, { "area_code": "E14001266", @@ -13258,7 +13258,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.44774022732361674 + "z": null }, { "area_code": "E14001267", @@ -13268,7 +13268,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3753670454525537 + "z": null }, { "area_code": "E14001268", @@ -13308,7 +13308,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7557274016742614 + "z": null }, { "area_code": "E14001272", @@ -13348,7 +13348,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4487200991426378 + "z": null }, { "area_code": "E14001276", @@ -13408,7 +13408,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5217531265051647 + "z": null }, { "area_code": "E14001282", @@ -13428,7 +13428,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.4057322756897388 + "z": null }, { "area_code": "E14001284", @@ -13458,7 +13458,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7250105467889005 + "z": null }, { "area_code": "E14001287", @@ -13478,7 +13478,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4030568186780761 + "z": null }, { "area_code": "E14001289", @@ -13498,7 +13498,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.673943615297441 + "z": null }, { "area_code": "E14001291", @@ -13508,7 +13508,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4012791352293487 + "z": null }, { "area_code": "E14001292", @@ -13518,7 +13518,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.759852497047189 + "z": null }, { "area_code": "E14001293", @@ -13538,7 +13538,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3832226258523204 + "z": null }, { "area_code": "E14001295", @@ -13558,7 +13558,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.32366067522321407 + "z": null }, { "area_code": "E14001297", @@ -13568,7 +13568,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.1479997845531178 + "z": null }, { "area_code": "E14001298", @@ -13588,7 +13588,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5110514859115843 + "z": null }, { "area_code": "E14001300", @@ -13608,7 +13608,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.8129947805259939 + "z": null }, { "area_code": "E14001302", @@ -13618,7 +13618,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.1485508154393418 + "z": null }, { "area_code": "E14001303", @@ -13648,7 +13648,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6132886175659076 + "z": null }, { "area_code": "E14001306", @@ -13668,7 +13668,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.43625011004596537 + "z": null }, { "area_code": "E14001308", @@ -13688,7 +13688,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.6094746642133052 + "z": null }, { "area_code": "E14001310", @@ -13698,7 +13698,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.5414488000122937 + "z": null }, { "area_code": "E14001311", @@ -13728,7 +13728,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1421079182775638 + "z": null }, { "area_code": "E14001314", @@ -13748,7 +13748,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.062326166784915794 + "z": null }, { "area_code": "E14001316", @@ -13768,7 +13768,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5007186806513056 + "z": null }, { "area_code": "E14001318", @@ -13788,7 +13788,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.25003137283716365 + "z": null }, { "area_code": "E14001320", @@ -13798,7 +13798,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.1609026879541708 + "z": null }, { "area_code": "E14001321", @@ -13838,7 +13838,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.18480400485675583 + "z": null }, { "area_code": "E14001325", @@ -13848,7 +13848,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.2183565125413214 + "z": null }, { "area_code": "E14001326", @@ -13868,7 +13868,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4695635975456629 + "z": null }, { "area_code": "E14001328", @@ -13878,7 +13878,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.29769652335973656 + "z": null }, { "area_code": "E14001329", @@ -13888,7 +13888,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.32117245545165923 + "z": null }, { "area_code": "E14001330", @@ -13898,7 +13898,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.41779911474708986 + "z": null }, { "area_code": "E14001331", @@ -13908,7 +13908,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6610210387112493 + "z": null }, { "area_code": "E14001332", @@ -13918,7 +13918,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.9441139563560454 + "z": null }, { "area_code": "E14001333", @@ -13938,7 +13938,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7079602911068119 + "z": null }, { "area_code": "E14001335", @@ -13958,7 +13958,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.28270459884878607 + "z": null }, { "area_code": "E14001337", @@ -13978,7 +13978,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.28075022576602066 + "z": null }, { "area_code": "E14001339", @@ -14008,7 +14008,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.44970636276099607 + "z": null }, { "area_code": "E14001342", @@ -14038,7 +14038,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2927974693069026 + "z": null }, { "area_code": "E14001345", @@ -14068,7 +14068,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4288963959231652 + "z": null }, { "area_code": "E14001348", @@ -14078,7 +14078,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4260552346551035 + "z": null }, { "area_code": "E14001349", @@ -14118,7 +14118,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.3609825957662787 + "z": null }, { "area_code": "E14001353", @@ -14138,7 +14138,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5650396890058378 + "z": null }, { "area_code": "E14001355", @@ -14148,7 +14148,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.33141381739506776 + "z": null }, { "area_code": "E14001356", @@ -14198,7 +14198,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.49103746506670576 + "z": null }, { "area_code": "E14001361", @@ -14208,7 +14208,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4901173764359475 + "z": null }, { "area_code": "E14001362", @@ -14228,7 +14228,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4190426255071823 + "z": null }, { "area_code": "E14001364", @@ -14248,7 +14248,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.34315036968545987 + "z": null }, { "area_code": "E14001366", @@ -14258,7 +14258,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4453981744801181 + "z": null }, { "area_code": "E14001367", @@ -14288,7 +14288,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.2653225159980578 + "z": null }, { "area_code": "E14001370", @@ -14298,7 +14298,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.45142157166802804 + "z": null }, { "area_code": "E14001371", @@ -14308,7 +14308,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.68912726287325 + "z": null }, { "area_code": "E14001372", @@ -14338,7 +14338,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5034942265392278 + "z": null }, { "area_code": "E14001375", @@ -14348,7 +14348,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.34126496441321036 + "z": null }, { "area_code": "E14001376", @@ -14358,7 +14358,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4402374475938249 + "z": null }, { "area_code": "E14001377", @@ -14368,7 +14368,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.4000163293625925 + "z": null }, { "area_code": "E14001378", @@ -14388,7 +14388,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.3752164410398793 + "z": null }, { "area_code": "E14001380", @@ -14398,7 +14398,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5908471369811176 + "z": null }, { "area_code": "E14001381", @@ -14408,7 +14408,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.39553023953524996 + "z": null }, { "area_code": "E14001382", @@ -14418,7 +14418,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 1.6656160129759892 + "z": null }, { "area_code": "E14001383", @@ -14448,7 +14448,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3551695843762593 + "z": null }, { "area_code": "E14001386", @@ -14508,7 +14508,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.43054445959058757 + "z": null }, { "area_code": "E14001392", @@ -14528,7 +14528,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.2820966382913558 + "z": null }, { "area_code": "E14001394", @@ -14538,7 +14538,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4470361586141468 + "z": null }, { "area_code": "E14001395", @@ -14548,7 +14548,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.593851657756935 + "z": null }, { "area_code": "E14001396", @@ -14588,7 +14588,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4122540672773596 + "z": null }, { "area_code": "E14001400", @@ -14598,7 +14598,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.6070028380430507 + "z": null }, { "area_code": "E14001401", @@ -14608,7 +14608,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3173519620255145 + "z": null }, { "area_code": "E14001402", @@ -14618,7 +14618,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.4666172934050195 + "z": null }, { "area_code": "E14001403", @@ -14628,7 +14628,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.553654714802867 + "z": null }, { "area_code": "E14001404", @@ -14638,7 +14638,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.40449519944369594 + "z": null }, { "area_code": "E14001405", @@ -14668,7 +14668,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.6195524300806565 + "z": null }, { "area_code": "E14001408", @@ -14688,7 +14688,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.3032276566483965 + "z": null }, { "area_code": "E14001410", @@ -14718,7 +14718,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.49061688477774346 + "z": null }, { "area_code": "E14001413", @@ -14738,7 +14738,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.8189373150611736 + "z": null }, { "area_code": "E14001415", @@ -14778,7 +14778,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.22745832880364256 + "z": null }, { "area_code": "E14001419", @@ -14788,7 +14788,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.408938293015476 + "z": null }, { "area_code": "E14001420", @@ -14808,7 +14808,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6976723739722624 + "z": null }, { "area_code": "E14001422", @@ -14838,7 +14838,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4090980252539021 + "z": null }, { "area_code": "E14001425", @@ -14848,7 +14848,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.13975909550009658 + "z": null }, { "area_code": "E14001426", @@ -14878,7 +14878,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.13963124705817936 + "z": null }, { "area_code": "E14001429", @@ -14928,7 +14928,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.34546446547987464 + "z": null }, { "area_code": "E14001434", @@ -14938,7 +14938,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6546499739753521 + "z": null }, { "area_code": "E14001435", @@ -14958,7 +14958,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.329527928190117 + "z": null }, { "area_code": "E14001437", @@ -14968,7 +14968,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.363062597434465 + "z": null }, { "area_code": "E14001438", @@ -14978,7 +14978,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3168634365293665 + "z": null }, { "area_code": "E14001439", @@ -14988,7 +14988,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5231676867771178 + "z": null }, { "area_code": "E14001440", @@ -15018,7 +15018,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.6136605727200248 + "z": null }, { "area_code": "E14001443", @@ -15028,7 +15028,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7240199426540532 + "z": null }, { "area_code": "E14001444", @@ -15058,7 +15058,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.8128559509884643 + "z": null }, { "area_code": "E14001447", @@ -15068,7 +15068,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4315092711285778 + "z": null }, { "area_code": "E14001448", @@ -15168,7 +15168,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.42051186089930004 + "z": null }, { "area_code": "E14001458", @@ -15178,7 +15178,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4759595354528152 + "z": null }, { "area_code": "E14001459", @@ -15188,7 +15188,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.22792789060596447 + "z": null }, { "area_code": "E14001460", @@ -15248,7 +15248,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.46072170520946476 + "z": null }, { "area_code": "E14001466", @@ -15278,7 +15278,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 1.460813527399946 + "z": null }, { "area_code": "E14001469", @@ -15298,7 +15298,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.14339455256823228 + "z": null }, { "area_code": "E14001471", @@ -15318,7 +15318,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.15302819697006115 + "z": null }, { "area_code": "E14001473", @@ -15338,7 +15338,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3647148827447857 + "z": null }, { "area_code": "E14001475", @@ -15348,7 +15348,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.14881207435167818 + "z": null }, { "area_code": "E14001476", @@ -15358,7 +15358,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4340774568647665 + "z": null }, { "area_code": "E14001477", @@ -15368,7 +15368,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3465401282742667 + "z": null }, { "area_code": "E14001478", @@ -15388,7 +15388,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.6455124263740848 + "z": null }, { "area_code": "E14001480", @@ -15408,7 +15408,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.28881504521729234 + "z": null }, { "area_code": "E14001482", @@ -15418,7 +15418,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41972671137250606 + "z": null }, { "area_code": "E14001483", @@ -15448,7 +15448,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.5150241285825352 + "z": null }, { "area_code": "E14001486", @@ -15478,7 +15478,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.413995134298806 + "z": null }, { "area_code": "E14001489", @@ -15588,7 +15588,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.30924418973024187 + "z": null }, { "area_code": "E14001500", @@ -15598,7 +15598,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.521378648457463 + "z": null }, { "area_code": "E14001501", @@ -15668,7 +15668,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.3637276115502675 + "z": null }, { "area_code": "E14001508", @@ -15698,7 +15698,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.71069958831694 + "z": null }, { "area_code": "E14001511", @@ -15708,7 +15708,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.6653587118557664 + "z": null }, { "area_code": "E14001512", @@ -15728,7 +15728,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5490268563561415 + "z": null }, { "area_code": "E14001514", @@ -15778,7 +15778,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.43496808700484496 + "z": null }, { "area_code": "E14001519", @@ -15798,7 +15798,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.3887524584425091 + "z": null }, { "area_code": "E14001521", @@ -15808,7 +15808,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5134362078475421 + "z": null }, { "area_code": "E14001522", @@ -15818,7 +15818,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.9416611317413803 + "z": null }, { "area_code": "E14001523", @@ -15888,7 +15888,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3844779479738116 + "z": null }, { "area_code": "E14001530", @@ -15898,7 +15898,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.29196643568833125 + "z": null }, { "area_code": "E14001531", @@ -15908,7 +15908,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.4175291969058184 + "z": null }, { "area_code": "E14001532", @@ -15918,7 +15918,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5089382454573571 + "z": null }, { "area_code": "E14001533", @@ -15928,7 +15928,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5149827673248899 + "z": null }, { "area_code": "E14001534", @@ -15938,7 +15938,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.766023806914981 + "z": null }, { "area_code": "E14001535", @@ -15948,7 +15948,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5837016877706127 + "z": null }, { "area_code": "E14001536", @@ -15978,7 +15978,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5533505560490792 + "z": null }, { "area_code": "E14001539", @@ -15988,7 +15988,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7109740917117182 + "z": null }, { "area_code": "E14001540", @@ -16028,7 +16028,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5021052601823875 + "z": null }, { "area_code": "E14001544", @@ -16038,7 +16038,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.1515667397870977 + "z": null }, { "area_code": "E14001545", @@ -16048,7 +16048,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.4326397368808608 + "z": null }, { "area_code": "E14001546", @@ -16088,7 +16088,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7468180314848685 + "z": null }, { "area_code": "E14001550", @@ -16118,7 +16118,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.3744465870104003 + "z": null }, { "area_code": "E14001553", @@ -16128,7 +16128,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5386647986208839 + "z": null }, { "area_code": "E14001554", @@ -16138,7 +16138,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41064611329339074 + "z": null }, { "area_code": "E14001555", @@ -16148,7 +16148,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.5643387263910384 + "z": null }, { "area_code": "E14001556", @@ -16178,7 +16178,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.727138793786034 + "z": null }, { "area_code": "E14001559", @@ -16188,7 +16188,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6677365710178355 + "z": null }, { "area_code": "E14001560", @@ -16198,7 +16198,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.11481759090954657 + "z": null }, { "area_code": "E14001561", @@ -16208,7 +16208,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.4249459583052577 + "z": null }, { "area_code": "E14001562", @@ -16228,7 +16228,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.678533139474155 + "z": null }, { "area_code": "E14001564", @@ -16238,7 +16238,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7214083208280286 + "z": null }, { "area_code": "E14001565", @@ -16298,7 +16298,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.8069701545871848 + "z": null }, { "area_code": "E14001571", @@ -16318,7 +16318,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.7630262535076524 + "z": null }, { "area_code": "E14001573", @@ -16348,7 +16348,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.35292299432827595 + "z": null }, { "area_code": "E14001576", @@ -16358,7 +16358,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.5914860168640855 + "z": null }, { "area_code": "E14001577", @@ -16368,7 +16368,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.7439748744216905 + "z": null }, { "area_code": "E14001578", @@ -16388,7 +16388,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8030026895332252 + "z": null }, { "area_code": "E14001580", @@ -16428,7 +16428,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.6751496394355603 + "z": null }, { "area_code": "E14001584", @@ -16448,7 +16448,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.36198111289080465 + "z": null }, { "area_code": "E14001586", @@ -16468,7 +16468,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.40884165019699703 + "z": null }, { "area_code": "E14001588", @@ -16478,7 +16478,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4206817123017746 + "z": null }, { "area_code": "E14001589", @@ -16498,7 +16498,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.29581898562110054 + "z": null }, { "area_code": "E14001591", @@ -16508,7 +16508,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.7227572881233995 + "z": null }, { "area_code": "E14001592", @@ -16538,7 +16538,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.8468925347336995 + "z": null }, { "area_code": "E14001595", @@ -16548,7 +16548,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.6838378143384578 + "z": null }, { "area_code": "E14001596", @@ -16558,7 +16558,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.46658173820862037 + "z": null }, { "area_code": "E14001597", @@ -16568,7 +16568,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4772805359778839 + "z": null }, { "area_code": "E14001598", @@ -16578,7 +16578,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.36022775568330284 + "z": null }, { "area_code": "E14001599", @@ -16588,7 +16588,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.531268571594409 + "z": null }, { "area_code": "E14001600", @@ -16638,7 +16638,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": 0.11280893677379909 + "z": null }, { "area_code": "E14001605", @@ -16698,7 +16698,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.2383861590126324 + "z": null }, { "area_code": "N05000006", @@ -16748,7 +16748,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.145240088197603 + "z": null }, { "area_code": "N05000011", @@ -16778,7 +16778,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.2889836452437586 + "z": null }, { "area_code": "N05000014", @@ -16788,7 +16788,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.2807524533172594 + "z": null }, { "area_code": "N05000015", @@ -16818,7 +16818,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.9245393913753347 + "z": null }, { "area_code": "N05000018", @@ -16828,7 +16828,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.5746276541507723 + "z": null }, { "area_code": "S14000021", @@ -16868,7 +16868,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.16177525226132017 + "z": null }, { "area_code": "S14000051", @@ -16878,7 +16878,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.940712485086397 + "z": null }, { "area_code": "S14000060", @@ -16888,7 +16888,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.028287091165720687 + "z": null }, { "area_code": "S14000061", @@ -16908,7 +16908,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.14795798147003966 + "z": null }, { "area_code": "S14000063", @@ -16928,7 +16928,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.18230273032997046 + "z": null }, { "area_code": "S14000065", @@ -16958,7 +16958,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.164662195380953 + "z": null }, { "area_code": "S14000068", @@ -16968,7 +16968,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3156489875444004 + "z": null }, { "area_code": "S14000069", @@ -16978,7 +16978,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.14188819802180888 + "z": null }, { "area_code": "S14000070", @@ -16998,7 +16998,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.15738150076888416 + "z": null }, { "area_code": "S14000072", @@ -17008,7 +17008,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.25775394288914616 + "z": null }, { "area_code": "S14000073", @@ -17038,7 +17038,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.0650947888661417 + "z": null }, { "area_code": "S14000076", @@ -17058,7 +17058,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3150300682493308 + "z": null }, { "area_code": "S14000078", @@ -17068,7 +17068,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.017050217546049153 + "z": null }, { "area_code": "S14000079", @@ -17088,7 +17088,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3192591554187767 + "z": null }, { "area_code": "S14000081", @@ -17118,7 +17118,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.17124043119233054 + "z": null }, { "area_code": "S14000084", @@ -17138,7 +17138,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.054271087152526334 + "z": null }, { "area_code": "S14000086", @@ -17188,7 +17188,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.19927478412089034 + "z": null }, { "area_code": "S14000091", @@ -17198,7 +17198,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.26944399427287563 + "z": null }, { "area_code": "S14000092", @@ -17208,7 +17208,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.16776311729588445 + "z": null }, { "area_code": "S14000093", @@ -17228,7 +17228,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.043381231082581045 + "z": null }, { "area_code": "S14000095", @@ -17258,7 +17258,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.262775413402293 + "z": null }, { "area_code": "S14000098", @@ -17288,7 +17288,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24541205143468758 + "z": null }, { "area_code": "S14000101", @@ -17298,7 +17298,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.19075205011419813 + "z": null }, { "area_code": "S14000102", @@ -17318,7 +17318,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.1660869152662638 + "z": null }, { "area_code": "S14000104", @@ -17328,7 +17328,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.3247849755429084 + "z": null }, { "area_code": "S14000105", @@ -17348,7 +17348,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.17675603713550514 + "z": null }, { "area_code": "S14000107", @@ -17358,7 +17358,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.2156346405984812 + "z": null }, { "area_code": "S14000108", @@ -17378,7 +17378,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.24139023982907212 + "z": null }, { "area_code": "S14000110", @@ -17388,7 +17388,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.1697290379737109 + "z": null }, { "area_code": "S14000111", @@ -17398,7 +17398,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.22646119151713381 + "z": null }, { "area_code": "W07000081", @@ -17408,7 +17408,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.06290145813486 + "z": null }, { "area_code": "W07000082", @@ -17418,7 +17418,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.03998347675001903 + "z": null }, { "area_code": "W07000083", @@ -17428,7 +17428,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.047309219516953945 + "z": null }, { "area_code": "W07000084", @@ -17438,7 +17438,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.010147671210554593 + "z": null }, { "area_code": "W07000085", @@ -17448,7 +17448,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.010904953778577656 + "z": null }, { "area_code": "W07000086", @@ -17458,7 +17458,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.0713164574316931 + "z": null }, { "area_code": "W07000087", @@ -17468,7 +17468,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.028166486292114375 + "z": null }, { "area_code": "W07000088", @@ -17488,7 +17488,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.026701277586126417 + "z": null }, { "area_code": "W07000090", @@ -17518,7 +17518,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.03562895178764713 + "z": null }, { "area_code": "W07000093", @@ -17528,7 +17528,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.060009659429086 + "z": null }, { "area_code": "W07000094", @@ -17538,7 +17538,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.002869939423167 + "z": null }, { "area_code": "W07000095", @@ -17548,7 +17548,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.058551292682852744 + "z": null }, { "area_code": "W07000096", @@ -17558,7 +17558,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.009597765197425147 + "z": null }, { "area_code": "W07000097", @@ -17568,7 +17568,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.0031688977450496218 + "z": null }, { "area_code": "W07000098", @@ -17598,7 +17598,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.07893382729441682 + "z": null }, { "area_code": "W07000101", @@ -17608,7 +17608,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.028612871479656944 + "z": null }, { "area_code": "W07000102", @@ -17668,7 +17668,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.9499138134042555 + "z": null }, { "area_code": "W07000108", @@ -17698,7 +17698,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.025739521264306193 + "z": null }, { "area_code": "W07000111", @@ -17708,7 +17708,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.0082704021049473 + "z": null }, { "area_code": "W07000112", @@ -17718,7 +17718,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.3167997584204193 + "z": null }, { "area_code": "E06000001", @@ -17758,7 +17758,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.2587113752869769 + "z": null }, { "area_code": "E06000005", @@ -17768,7 +17768,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": 0.2960431676186742 + "z": null }, { "area_code": "E06000006", @@ -17788,7 +17788,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.5374141881019865 + "z": null }, { "area_code": "E06000008", @@ -17818,7 +17818,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.1895563413603205 + "z": null }, { "area_code": "E06000011", @@ -17828,7 +17828,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.597623832967746 + "z": null }, { "area_code": "E06000012", @@ -17838,7 +17838,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.3270249996342258 + "z": null }, { "area_code": "E06000013", @@ -17858,7 +17858,7 @@ "region_code": "E12000003", "rows": "<3", "sources": "<3", - "z": -0.5386764826116722 + "z": null }, { "area_code": "E06000015", @@ -17868,7 +17868,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.27122476951627894 + "z": null }, { "area_code": "E06000016", @@ -17878,7 +17878,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -0.00036987125822950486 + "z": null }, { "area_code": "E06000017", @@ -17888,7 +17888,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.4465046891495148 + "z": null }, { "area_code": "E06000018", @@ -17898,7 +17898,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -0.7073381518946915 + "z": null }, { "area_code": "E06000019", @@ -17908,7 +17908,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.7947839953231738 + "z": null }, { "area_code": "E06000020", @@ -17918,7 +17918,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": -0.06957792448514123 + "z": null }, { "area_code": "E06000021", @@ -17938,7 +17938,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -0.26339368027645693 + "z": null }, { "area_code": "E06000023", @@ -17948,7 +17948,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -1.2541650206717159 + "z": null }, { "area_code": "E06000024", @@ -18008,7 +18008,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.4351623880761947 + "z": null }, { "area_code": "E06000032", @@ -18048,7 +18048,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.5792282635449675 + "z": null }, { "area_code": "E06000036", @@ -18058,7 +18058,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.23548655035289 + "z": null }, { "area_code": "E06000037", @@ -18068,7 +18068,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.9351276793809815 + "z": null }, { "area_code": "E06000038", @@ -18078,7 +18078,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.06487862850832889 + "z": null }, { "area_code": "E06000039", @@ -18088,7 +18088,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.19280323351545164 + "z": null }, { "area_code": "E06000040", @@ -18098,7 +18098,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.070570715066159 + "z": null }, { "area_code": "E06000041", @@ -18108,7 +18108,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.08689917272326558 + "z": null }, { "area_code": "E06000042", @@ -18118,7 +18118,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.16562816157650273 + "z": null }, { "area_code": "E06000043", @@ -18128,7 +18128,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.06413511500128496 + "z": null }, { "area_code": "E06000044", @@ -18228,7 +18228,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -1.4038448735946838 + "z": null }, { "area_code": "E06000055", @@ -18288,7 +18288,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.8138529564102794 + "z": null }, { "area_code": "E06000061", @@ -18328,7 +18328,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.34353519779274866 + "z": null }, { "area_code": "E06000065", @@ -18358,7 +18358,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.1405344129060704 + "z": null }, { "area_code": "E07000009", @@ -18368,7 +18368,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.6837956347428933 + "z": null }, { "area_code": "E07000010", @@ -18388,7 +18388,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.301891837901738 + "z": null }, { "area_code": "E07000012", @@ -18408,7 +18408,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.1253019761555584 + "z": null }, { "area_code": "E07000033", @@ -18428,7 +18428,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.28549799251224034 + "z": null }, { "area_code": "E07000035", @@ -18448,7 +18448,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.23805659945276494 + "z": null }, { "area_code": "E07000037", @@ -18468,7 +18468,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.3307251168591156 + "z": null }, { "area_code": "E07000039", @@ -18488,7 +18488,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -0.0882762881468754 + "z": null }, { "area_code": "E07000041", @@ -18498,7 +18498,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.2454290199098559 + "z": null }, { "area_code": "E07000042", @@ -18538,7 +18538,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.0475261556680642 + "z": null }, { "area_code": "E07000046", @@ -18548,7 +18548,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.7166939808646855 + "z": null }, { "area_code": "E07000047", @@ -18588,7 +18588,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.3777372493003328 + "z": null }, { "area_code": "E07000064", @@ -18608,7 +18608,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.07351440844189075 + "z": null }, { "area_code": "E07000066", @@ -18618,7 +18618,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.29532953264846734 + "z": null }, { "area_code": "E07000067", @@ -18638,7 +18638,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.5801343259422829 + "z": null }, { "area_code": "E07000069", @@ -18658,7 +18658,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.2822416486845125 + "z": null }, { "area_code": "E07000071", @@ -18678,7 +18678,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.0469545231529668 + "z": null }, { "area_code": "E07000073", @@ -18688,7 +18688,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.654891976513351 + "z": null }, { "area_code": "E07000074", @@ -18728,7 +18728,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 1.695501833520265 + "z": null }, { "area_code": "E07000078", @@ -18738,7 +18738,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.14780272552579252 + "z": null }, { "area_code": "E07000079", @@ -18748,7 +18748,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.41739778510110487 + "z": null }, { "area_code": "E07000080", @@ -18758,7 +18758,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.5069687737245485 + "z": null }, { "area_code": "E07000081", @@ -18768,7 +18768,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.1646751651649654 + "z": null }, { "area_code": "E07000082", @@ -18778,7 +18778,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 0.1578969130031499 + "z": null }, { "area_code": "E07000083", @@ -18788,7 +18788,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": 1.6418347395904764 + "z": null }, { "area_code": "E07000084", @@ -18798,7 +18798,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.19093451058658872 + "z": null }, { "area_code": "E07000085", @@ -18808,7 +18808,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.2920049194247605 + "z": null }, { "area_code": "E07000086", @@ -18828,7 +18828,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.26885984921265227 + "z": null }, { "area_code": "E07000088", @@ -18858,7 +18858,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.2664533505046436 + "z": null }, { "area_code": "E07000091", @@ -18868,7 +18868,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.2029915467427796 + "z": null }, { "area_code": "E07000092", @@ -18888,7 +18888,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.14945348375397205 + "z": null }, { "area_code": "E07000094", @@ -18898,7 +18898,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.3220182654791484 + "z": null }, { "area_code": "E07000095", @@ -18908,7 +18908,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.3649053182072056 + "z": null }, { "area_code": "E07000096", @@ -18938,7 +18938,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.007108364679011891 + "z": null }, { "area_code": "E07000102", @@ -19008,7 +19008,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.4245799682733015 + "z": null }, { "area_code": "E07000110", @@ -19018,7 +19018,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.11607448063488067 + "z": null }, { "area_code": "E07000111", @@ -19028,7 +19028,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.4065054414885365 + "z": null }, { "area_code": "E07000112", @@ -19048,7 +19048,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.0483833746066423 + "z": null }, { "area_code": "E07000114", @@ -19068,7 +19068,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.17083234560304744 + "z": null }, { "area_code": "E07000116", @@ -19078,7 +19078,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.4316809532692054 + "z": null }, { "area_code": "E07000117", @@ -19108,7 +19108,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.568302645326094 + "z": null }, { "area_code": "E07000120", @@ -19118,7 +19118,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.6499283955377357 + "z": null }, { "area_code": "E07000121", @@ -19148,7 +19148,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.09791885843792754 + "z": null }, { "area_code": "E07000124", @@ -19158,7 +19158,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.9332333306526785 + "z": null }, { "area_code": "E07000125", @@ -19178,7 +19178,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.30479839473351117 + "z": null }, { "area_code": "E07000127", @@ -19188,7 +19188,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 1.4793079020414086 + "z": null }, { "area_code": "E07000128", @@ -19198,7 +19198,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.2605197798530708 + "z": null }, { "area_code": "E07000129", @@ -19228,7 +19228,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.4629811535772127 + "z": null }, { "area_code": "E07000132", @@ -19238,7 +19238,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.25697140244033506 + "z": null }, { "area_code": "E07000133", @@ -19258,7 +19258,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.35317137554944417 + "z": null }, { "area_code": "E07000135", @@ -19268,7 +19268,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.087565660129638 + "z": null }, { "area_code": "E07000136", @@ -19288,7 +19288,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -0.01739475654464597 + "z": null }, { "area_code": "E07000138", @@ -19298,7 +19298,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.41155078593575206 + "z": null }, { "area_code": "E07000139", @@ -19308,7 +19308,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.22526113780380147 + "z": null }, { "area_code": "E07000140", @@ -19328,7 +19328,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.013036277452271205 + "z": null }, { "area_code": "E07000142", @@ -19348,7 +19348,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.055085697972957176 + "z": null }, { "area_code": "E07000144", @@ -19358,7 +19358,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.010728812175718394 + "z": null }, { "area_code": "E07000145", @@ -19388,7 +19388,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.1681465126338288 + "z": null }, { "area_code": "E07000148", @@ -19398,7 +19398,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.8312775600888931 + "z": null }, { "area_code": "E07000149", @@ -19418,7 +19418,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.15766467786576654 + "z": null }, { "area_code": "E07000171", @@ -19428,7 +19428,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.21617723316685633 + "z": null }, { "area_code": "E07000172", @@ -19438,7 +19438,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.4406348784023377 + "z": null }, { "area_code": "E07000173", @@ -19448,7 +19448,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.21416552899863006 + "z": null }, { "area_code": "E07000174", @@ -19458,7 +19458,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 0.2859219359999393 + "z": null }, { "area_code": "E07000175", @@ -19478,7 +19478,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": 1.3917456015878065 + "z": null }, { "area_code": "E07000177", @@ -19498,7 +19498,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.14020426187398932 + "z": null }, { "area_code": "E07000179", @@ -19528,7 +19528,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.4395874475145327 + "z": null }, { "area_code": "E07000192", @@ -19538,7 +19538,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5066449872108447 + "z": null }, { "area_code": "E07000193", @@ -19568,7 +19568,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.29491690235095475 + "z": null }, { "area_code": "E07000196", @@ -19588,7 +19588,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 1.2809959869206549 + "z": null }, { "area_code": "E07000198", @@ -19608,7 +19608,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.8012888720042168 + "z": null }, { "area_code": "E07000200", @@ -19628,7 +19628,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.04108085553836719 + "z": null }, { "area_code": "E07000203", @@ -19638,7 +19638,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.25666500281471766 + "z": null }, { "area_code": "E07000207", @@ -19648,7 +19648,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.13334188261403737 + "z": null }, { "area_code": "E07000208", @@ -19668,7 +19668,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.13055231977946477 + "z": null }, { "area_code": "E07000210", @@ -19678,7 +19678,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5448886229830096 + "z": null }, { "area_code": "E07000211", @@ -19688,7 +19688,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.097168827971233 + "z": null }, { "area_code": "E07000212", @@ -19718,7 +19718,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5772706725731958 + "z": null }, { "area_code": "E07000215", @@ -19728,7 +19728,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.5882247576922585 + "z": null }, { "area_code": "E07000216", @@ -19738,7 +19738,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.3000515550466065 + "z": null }, { "area_code": "E07000217", @@ -19768,7 +19768,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.23634950187116924 + "z": null }, { "area_code": "E07000220", @@ -19808,7 +19808,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.8565155399956511 + "z": null }, { "area_code": "E07000224", @@ -19818,7 +19818,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -0.13643844151046525 + "z": null }, { "area_code": "E07000225", @@ -19828,7 +19828,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.1606161412364054 + "z": null }, { "area_code": "E07000226", @@ -19848,7 +19848,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.017563817068075024 + "z": null }, { "area_code": "E07000228", @@ -19858,7 +19858,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 0.0017801313909453643 + "z": null }, { "area_code": "E07000229", @@ -19868,7 +19868,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": 1.389925270929713 + "z": null }, { "area_code": "E07000234", @@ -19878,7 +19878,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.5607700542508336 + "z": null }, { "area_code": "E07000235", @@ -19888,7 +19888,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 2.1786678882417103 + "z": null }, { "area_code": "E07000236", @@ -19908,7 +19908,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.4772805359778839 + "z": null }, { "area_code": "E07000238", @@ -19938,7 +19938,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -0.03197668748766979 + "z": null }, { "area_code": "E07000241", @@ -19958,7 +19958,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": 0.8882557726812685 + "z": null }, { "area_code": "E07000243", @@ -19998,7 +19998,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.6127250826258522 + "z": null }, { "area_code": "E08000002", @@ -20048,7 +20048,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": 0.19060360440213572 + "z": null }, { "area_code": "E08000007", @@ -20058,7 +20058,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.6818203768266813 + "z": null }, { "area_code": "E08000008", @@ -20068,7 +20068,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.4251597522425939 + "z": null }, { "area_code": "E08000009", @@ -20088,7 +20088,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.1235354367110957 + "z": null }, { "area_code": "E08000011", @@ -20098,7 +20098,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.005550786227072699 + "z": null }, { "area_code": "E08000012", @@ -20108,7 +20108,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.6743535210302175 + "z": null }, { "area_code": "E08000013", @@ -20138,7 +20138,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -0.8182579303288108 + "z": null }, { "area_code": "E08000016", @@ -20198,7 +20198,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.40694624986696926 + "z": null }, { "area_code": "E08000023", @@ -20208,7 +20208,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.045715148956189194 + "z": null }, { "area_code": "E08000024", @@ -20218,7 +20218,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.6722037583472782 + "z": null }, { "area_code": "E08000025", @@ -20248,7 +20248,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.060913264927404794 + "z": null }, { "area_code": "E08000028", @@ -20268,7 +20268,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.6806581223669111 + "z": null }, { "area_code": "E08000030", @@ -20278,7 +20278,7 @@ "region_code": "E12000005", "rows": "<3", "sources": "<3", - "z": 0.35085103483733376 + "z": null }, { "area_code": "E08000031", @@ -20348,7 +20348,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -0.3232274225988881 + "z": null }, { "area_code": "E09000001", @@ -20368,7 +20368,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.1745663799483743 + "z": null }, { "area_code": "E09000003", @@ -20388,7 +20388,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.08321899415398554 + "z": null }, { "area_code": "E09000005", @@ -20418,7 +20418,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.9307145840582124 + "z": null }, { "area_code": "E09000008", @@ -20468,7 +20468,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.19268670867813054 + "z": null }, { "area_code": "E09000013", @@ -20478,7 +20478,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.07846761427725489 + "z": null }, { "area_code": "E09000014", @@ -20488,7 +20488,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.18308549262968385 + "z": null }, { "area_code": "E09000015", @@ -20498,7 +20498,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.021043736842263256 + "z": null }, { "area_code": "E09000016", @@ -20508,7 +20508,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.14520574754469798 + "z": null }, { "area_code": "E09000017", @@ -20518,7 +20518,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.22228378712877647 + "z": null }, { "area_code": "E09000018", @@ -20528,7 +20528,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.16192550484936744 + "z": null }, { "area_code": "E09000019", @@ -20538,7 +20538,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.09588744094471774 + "z": null }, { "area_code": "E09000020", @@ -20548,7 +20548,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 1.4323363324297376 + "z": null }, { "area_code": "E09000021", @@ -20568,7 +20568,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.38368634144108754 + "z": null }, { "area_code": "E09000023", @@ -20588,7 +20588,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.0721229987771787 + "z": null }, { "area_code": "E09000025", @@ -20598,7 +20598,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.6067871305156933 + "z": null }, { "area_code": "E09000026", @@ -20608,7 +20608,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7649265266228599 + "z": null }, { "area_code": "E09000027", @@ -20628,7 +20628,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.4093921309803837 + "z": null }, { "area_code": "E09000029", @@ -20638,7 +20638,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.06478492334286251 + "z": null }, { "area_code": "E09000030", @@ -20658,7 +20658,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.7762614681223793 + "z": null }, { "area_code": "E09000032", @@ -20668,7 +20668,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -0.4609040342915793 + "z": null }, { "area_code": "E09000033", @@ -20738,7 +20738,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -0.8305772909242418 + "z": null }, { "area_code": "N09000007", @@ -20758,7 +20758,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.2166651245022744 + "z": null }, { "area_code": "N09000009", @@ -20778,7 +20778,7 @@ "region_code": "N99999999", "rows": "<3", "sources": "<3", - "z": -1.9429417808796687 + "z": null }, { "area_code": "N09000011", @@ -20798,7 +20798,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.8000674599660462 + "z": null }, { "area_code": "S12000006", @@ -20818,7 +20818,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.06052385144411682 + "z": null }, { "area_code": "S12000010", @@ -20858,7 +20858,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.3329378575417563 + "z": null }, { "area_code": "S12000017", @@ -20868,7 +20868,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.09432896915209961 + "z": null }, { "area_code": "S12000018", @@ -20908,7 +20908,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.20966712562420795 + "z": null }, { "area_code": "S12000023", @@ -20938,7 +20938,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.780985828871549 + "z": null }, { "area_code": "S12000028", @@ -20948,7 +20948,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 1.0082487587835283 + "z": null }, { "area_code": "S12000029", @@ -20978,7 +20978,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.7727006745734952 + "z": null }, { "area_code": "S12000034", @@ -20998,7 +20998,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.22196164336891017 + "z": null }, { "area_code": "S12000036", @@ -21018,7 +21018,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.5224561308890212 + "z": null }, { "area_code": "S12000039", @@ -21028,7 +21028,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.20682620036657123 + "z": null }, { "area_code": "S12000040", @@ -21038,7 +21038,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.389689581491292 + "z": null }, { "area_code": "S12000041", @@ -21058,7 +21058,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.5816301304412752 + "z": null }, { "area_code": "S12000045", @@ -21068,7 +21068,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.11923355965218933 + "z": null }, { "area_code": "S12000047", @@ -21088,7 +21088,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": 0.5796115358293416 + "z": null }, { "area_code": "S12000049", @@ -21118,7 +21118,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.3167997584204193 + "z": null }, { "area_code": "W06000002", @@ -21128,7 +21128,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.19815062534670294 + "z": null }, { "area_code": "W06000003", @@ -21138,7 +21138,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.22050043818709605 + "z": null }, { "area_code": "W06000004", @@ -21158,7 +21158,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.47976931846228443 + "z": null }, { "area_code": "W06000006", @@ -21168,7 +21168,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.5440562817764399 + "z": null }, { "area_code": "W06000008", @@ -21178,7 +21178,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 1.495591976652753 + "z": null }, { "area_code": "W06000009", @@ -21188,7 +21188,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.28307636216004556 + "z": null }, { "area_code": "W06000010", @@ -21198,7 +21198,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.6978255111617081 + "z": null }, { "area_code": "W06000011", @@ -21208,7 +21208,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.32513138280258425 + "z": null }, { "area_code": "W06000012", @@ -21238,7 +21238,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.3188767999100951 + "z": null }, { "area_code": "W06000015", @@ -21248,7 +21248,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.8496536102304101 + "z": null }, { "area_code": "W06000016", @@ -21268,7 +21268,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": -0.6205643761864704 + "z": null }, { "area_code": "W06000019", @@ -21298,7 +21298,7 @@ "region_code": "W99999999", "rows": "<3", "sources": "<3", - "z": 0.028612871479656944 + "z": null }, { "area_code": "W06000022", @@ -22713,7 +22713,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -1.769868962446892 + "z": null }, { "area_code": "E14001090", @@ -24453,7 +24453,7 @@ "region_code": "E12000008", "rows": "<3", "sources": "<3", - "z": -2.5442240897678445 + "z": null }, { "area_code": "E14001264", @@ -27403,7 +27403,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -1.995827490255994 + "z": null }, { "area_code": "E14001559", @@ -28073,7 +28073,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.8624826911428566 + "z": null }, { "area_code": "S14000045", @@ -28523,7 +28523,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -2.982372940904726 + "z": null }, { "area_code": "S14000102", @@ -29113,7 +29113,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -1.0400491352821655 + "z": null }, { "area_code": "E06000018", @@ -29923,7 +29923,7 @@ "region_code": "E12000006", "rows": "<3", "sources": "<3", - "z": -1.9627311195481059 + "z": null }, { "area_code": "E07000075", @@ -30393,7 +30393,7 @@ "region_code": "E12000002", "rows": "<3", "sources": "<3", - "z": -2.2796011856369005 + "z": null }, { "area_code": "E07000126", @@ -31583,7 +31583,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": 0.3246737422552903 + "z": null }, { "area_code": "E09000002", @@ -32073,7 +32073,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.8624826911428566 + "z": null }, { "area_code": "S12000014", @@ -34791,7 +34791,7 @@ "region_code": "E12000007", "rows": "<3", "sources": "<3", - "z": -2.053963646386373 + "z": null }, { "area_code": "E14001176", @@ -37991,7 +37991,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -2.5819098716401827 + "z": null }, { "area_code": "E14001496", @@ -38221,7 +38221,7 @@ "region_code": "E12000001", "rows": "<3", "sources": "<3", - "z": -2.5417681312553575 + "z": null }, { "area_code": "E14001519", @@ -41001,7 +41001,7 @@ "region_code": "E12000009", "rows": "<3", "sources": "<3", - "z": -1.6623517968557193 + "z": null }, { "area_code": "E07000061", @@ -41651,7 +41651,7 @@ "region_code": "E12000004", "rows": "<3", "sources": "<3", - "z": -2.5625016727136045 + "z": null }, { "area_code": "E07000130", @@ -43361,7 +43361,7 @@ "region_code": "S99999999", "rows": "<3", "sources": "<3", - "z": -0.5972853042836894 + "z": null }, { "area_code": "S12000026", @@ -44909,7 +44909,7 @@ "spine": "spine-r.h5", "spine_sha256": "921612e88eefcf21511ac6ad5faf60a1514f577490188c6e39aec49cb4224f27" }, - "purpose": "Assignment-level before/after evidence for the UK geography-first reordering (step 2): legacy sequential ladder draw versus the identity-keyed single-stage draw, design weights, no solve; per-area counts below the minimum are suppressed and no unit records appear.", + "purpose": "Assignment-level before/after evidence for the UK geography-first reordering (step 2): legacy sequential ladder draw versus the identity-keyed single-stage draw, design weights, no solve; per-area counts below the minimum are suppressed together with their z-scores, and no unit records appear.", "supports": { "uk_ew_output_area_2021": "1b0dec268be0141f03ec05dc3b55c13733cc2bb20be5e403d76d73c8e27dc797", "uk_ni_data_zone_2021": "facb9923f589cb68eefa8fad37911e618b6137964ff58a231976ef62584b669f", diff --git a/docs/uk-full-build-graph.md b/docs/uk-full-build-graph.md index eec66fdeb..0786f8f39 100644 --- a/docs/uk-full-build-graph.md +++ b/docs/uk-full-build-graph.md @@ -55,7 +55,7 @@ For smaller outputs, add `--dataset-households 100000` to use the common informe ## The national role -`--release-role national` builds the certified national line. The driver parses and validates it with the same posture-aware validator (the national role refuses the dense role's arguments, `--release-candidate` among them, and requires a bound `--input-h5` with `--input-sha256`; a `--spine-request` is refused because the seam reads a pinned checkpoint) and then, before any graph is prepared, dispatches to `uk_runtime.national_role.run_national_role`. That module is the rowwise tool's national branch moved unchanged: the calibration seam of microcosm#823 (`uk_runtime.calibration_run.run_uk_calibration`, the national calibration stage, the release-cut battery and the certifier stay exactly as they were). Its outputs (`microcosm_uk_2024_25.h5`, `build_record.json`, `microcosm_uk_2024_25.terminal_gates.json`, `calibration_diagnostics.json`, the registries and `rowwise_candidate_manifest.json`), its Logbook row, staging telemetry, staged bundle and incumbent evaluation are the seam's own; the [national calibration runbook](uk-national-calibration-runbook-623.md) and the [national release assembly runbook](uk-national-release-assembly-runbook-806.md) describe them. Parity with the previous national line is by identity: the same engine runs the same code. A posture-driven national path through this graph is planned as the next change on this line and is not part of this registration. +`--release-role national` builds the certified national line on the same graph. The driver parses and validates it with the same posture-aware validator (the national role refuses the dense role's arguments, `--release-candidate` among them, and requires a bound `--input-h5` with `--input-sha256`; a `--spine-request` is refused because the national line solves a pinned checkpoint as bound) and composes the national graph (`uk_runtime.graph_national`): `uk.full.spine_checkpoint` admits the bound checkpoint, `uk.full.national_targets` compiles the national register from the pinned Chronicle artifact (measure exclusions, band edges, the frozen-register check, the feed pin with `--allow-unpinned-feed`; no ladder, no local surface), `uk.full.national_problem` refuses non-finite inputs, resolves the engine measures on the bound frame, materialises the register and encodes one ordered problem with the national doctrine's `family_equal` loss weights, mass reason and bounds in its bindings, `uk.full.dense` and `uk.full.calibrated` are the shared calibration nodes under the doctrine's epochs, learning rate and seed (the dense node's admission is the bound spine's provenance artifact rather than a preflight battery, because the national line runs no pre-solve battery: its source and reference gates are the release-cut certifier's), and `uk.full.gates.calibrated` evaluates the seven `UK_CALIBRATION_GATE_SCOPE` gates on the calibrated population. The driver then materialises the seam-shaped evidence from the stored artifacts: the diagnostics through the seam writer with the build block (the digest measured from the file), the target-support sidecars, the calibration-seam battery replayed from the stored phase report and signed (`GateBatteryRun.record_phase`), `microcosm_uk_2024_25.h5`, a graph readback of the written H5 (`uk.full.national.readback`), the schema-1 `build_record.json` that `tools/certify_uk_release_cut.py` certifies, the frozen `national_target_registry.json` and `national_contract_registry.json`, and the schema-4 `rowwise_candidate_manifest.json` (`uk_runtime.national_role`). The Logbook row is on the seam's `uk-frs-calibration` pipeline with the seam's attempt id; staging telemetry, the staged bundle and the end-of-build incumbent evaluation follow the dense role's switches. A blocked battery leaves the report on disk (`blocked_at_phase: "terminal"`), writes no H5 and records a failed row. The [national calibration runbook](uk-national-calibration-runbook-623.md) and the [national release assembly runbook](uk-national-release-assembly-runbook-806.md) describe the outputs. The in-process calibration seam (`run_uk_calibration`, `UKNationalCalibrationStage`) is retired; its contracts (gate scopes, sidecar authentication, provenance blocks, scoped signing) stay in `uk_runtime.calibration_run`. Parity with the seam is a solved question of construction (the same register, the same matrix row for row, the same solver under the same doctrine and seed, the same mass record) checked on the licensed spine. ## Graph owners and shared contracts @@ -83,6 +83,8 @@ The spine graph is the 33-stage source roster declared by the UK spec, built by | Rotated local holdout and final gates/diagnostics | `uk.full.holdout`, `uk.full.gates.calibrated` | | Export contract, H5 readback and package inventory | `uk.full.export.prepare`, `uk.full.export.readback`, `uk.full.package` | | Gate, comparison and export evidence for certification | `uk.full.certification` | +| National register, national problem and seam gates (`--release-role national`) | `uk.full.national_targets`, `uk.full.national_problem`, `uk.full.gates.calibrated` | +| Readback of the written national H5 (`--release-role national`) | `uk.full.national.readback` | The modules under `uk_runtime` divide the roster: `graph_build` composes the graph over the bound spine, `graph_population` owns sample through the geography gate, `graph_targets` owns target compilation through the problem, `graph_calibration` owns the dense solve, the size nodes and the calibrated population, `graph_terminal` owns the gate nodes, the holdout, the export and the package, `full_certification` owns the terminal certification node, and `full_build_cli` resolves the request, executes graph endpoints and atomically materialises their stored artifacts. @@ -118,4 +120,12 @@ Recorded for review in `experiments/901-uk-main-rebase-receipts.md` (R3): - The HMRC family names `spi_income_band_donors` (microcosm#1006) as a predecessor and admits its two operation kinds; the contract otherwise refused the drifted operation order. - `tools/build_uk_rowwise_dataset.py` stays as it was on main (its tests load it by path; it still serves `--candidate-clone-counts`). -The dense and national parity builds through this driver are owed once a spine can be built at main's head (receipts R4); the national role is parity by identity regardless. +Recorded at the review of the national line's move onto the graph (`experiments/901-uk-national-graph-path-receipts.md`): + +- The measure-exclusion windows and the target-fit deferral register are evaluated at `--review-date` (default today) on the national line, as the dense graph already did; the retired seam evaluated both at the run clock. A back-dated review date therefore keeps expired exclusions and deferrals in force, which the seam never allowed; the date is a node parameter (`uk.full.national_targets`, `uk.full.gates.calibrated`) and is recorded in the request bindings. +- An occupied `--out` is refused before any work on the national line (the seam's `FileExistsError`), so a repeated run never replaces a candidate under the Logbook row that names it. A `KeyboardInterrupt` records a `discarded` row on both lines and re-raises, as the seam did. +- `build.json` is re-issued after the close step on both lines: the staging receipts are appended to the rowwise manifest and, on the national line, the delivery summary to the build record (the release assembler reads it there), and the completion marker then carries those files' final digests rather than the note that receipts follow. +- The measure receipt records a scratch-mode resolver's `source_path` relative to the per-run scratch root (`simulation-input.h5`), so the measure and national problem nodes are deterministic between identical runs; the receipt no longer carries a temporary directory. +- Under `--release-candidate` the three atomic-area supports must be the `sources.yaml` rows (sha256 and byte size), whatever digest was pinned on the command line, and `tools/preflight_uk_local_release_candidate.py` refuses a manifest whose `geography.assignment` is not the atomic law on those same pins (microcosm#932, round 1). + +The dense parity build through this driver was measured at the 10 % rung before microcosm#901 merged (receipts R7); the national line's parity with the retired seam is measured on the licensed spine in the receipts of the change that moved it onto the graph. diff --git a/docs/uk-national-calibration-runbook-623.md b/docs/uk-national-calibration-runbook-623.md index 786da9b91..3c0938642 100644 --- a/docs/uk-national-calibration-runbook-623.md +++ b/docs/uk-national-calibration-runbook-623.md @@ -21,19 +21,23 @@ Calibration runs through the UK build driver's national release role, `microcosm-build-uk --release-role national` (`tools/build_uk_full.py`; `tools/build_uk_rowwise_candidate.py` is a stub over the same driver). The role arrived with microcosm#823, which retired -`tools/calibrate_uk_national_dataset.py`, and moved into the graph driver with -microcosm#901. The driver validates the request with its posture-aware -validator and, before any graph is prepared, dispatches to -`uk_runtime.national_role`, which delegates the build to the calibration seam -library (`uk_runtime.calibration_run.run_uk_calibration`): it is the only path -that builds the measure resolver from the input file and applies the committed -measure-exclusion register, and 187 of the activated references bind model -outputs that no frame carries — so it is the only path on which this target -surface materializes. No cloning, no ladder, national targets only; the role -builds from a bound `--input-h5` and refuses a `--spine-request`. The retired -June builder constructed the calibration stage without either and aborted on -the first unmaterializable reference; it also rebuilt SPI income onto its -input, which a spine artifact already carries. +`tools/calibrate_uk_national_dataset.py`, moved into the graph driver with +microcosm#901 and onto the graph itself with the change that followed it. +The driver validates the request with its posture-aware validator and +composes the national graph (`uk_runtime.graph_national`): the bound +checkpoint, the national register compiled from the pinned Chronicle +artifact with the committed measure-exclusion register applied, the engine +measures resolved on the bound frame (187 of the activated references bind +model outputs that no frame carries, so the resolver is built from the input +frame, never skipped), the one ordered national problem, the shared solve +nodes under the national doctrine and the calibration-seam gate scope. The +seam-shaped evidence (the build record, the signed gate report, the seam +diagnostics, the registries, the manifest) is materialised from the stored +graph artifacts. No cloning, no ladder, national targets only; the role +builds from a bound `--input-h5` and refuses a `--spine-request`. The +retired June builder constructed the calibration stage without either and +aborted on the first unmaterializable reference; it also rebuilt SPI income +onto its input, which a spine artifact already carries. ```bash uv run --no-sync python tools/build_uk_full.py --release-role national \ @@ -45,6 +49,18 @@ uv run --no-sync python tools/build_uk_full.py --release-role national \ --out data/ukds/acceptance/623-first-calibrated-candidate ``` +`--out` must not already hold a candidate: the role refuses an occupied +output directory before any work (`failure.json` names the +`FileExistsError`, the Logbook row is `failed`), so a rebuild goes into a +fresh directory rather than over the bytes an earlier row names. An +operator interrupt (Ctrl-C) records a `discarded` row and closes the staging +run as failed. `--review-date` (default today) dates the measure-exclusion +windows and the target-fit deferral register on this line, as it does on +the dense line; the retired seam used the run clock, so a back-dated value +keeps expired exclusions and deferrals in force and is only for replaying a +dated review. The date is a parameter of the `uk.full.national_targets` and +`uk.full.gates.calibrated` nodes (`build.graph.json`). + The role names every output after the pinned FRS release vintage: `microcosm_uk_2024_25.h5`, `calibration_diagnostics.json`, `build_record.json`, `microcosm_uk_2024_25.terminal_gates.json`, the frozen @@ -54,9 +70,9 @@ prints the plan without solving. Staging telemetry and the staged bundle follow the dense role's switches (`--staging-local-only`, `--no-staging`, `--staging-read-back`, `--no-staged-dataset`); the run id is the attempt id. -The diagnostics digest is measured, not declared: the seam writes the -diagnostics file, hashes its actual bytes, and only then constructs and signs -the terminal gate evidence. There is no `--calibration-diagnostics-sha256` to +The diagnostics digest is measured, not declared: the driver writes the +diagnostics file through the seam writer, hashes its actual bytes, and only +then replays and signs the terminal gate evidence. There is no `--calibration-diagnostics-sha256` to supply, and no way for the receipt to claim an identity the file does not have. Solve parameters are per-run overrides of the declared doctrine. Since the diff --git a/experiments/901-uk-national-graph-path-receipts.md b/experiments/901-uk-national-graph-path-receipts.md new file mode 100644 index 000000000..bdfc18919 --- /dev/null +++ b/experiments/901-uk-national-graph-path-receipts.md @@ -0,0 +1,159 @@ +# Receipts: the UK national release role on the graph (PR-2 of the #901 line) + +Branch `uk-national-graph-path` off main 6a70cd4ee (the #901 merge, 2026-09-29). Plan: +the "PR-2" paragraph of the #901 re-base plan (her ruling 2026-09-25); execution record in the +working notes. Rule: nothing here is a release; the parity run is a sampled rung and is never +exported. + +## R1. Composition and the seam fixture (hermetic) + +`packages/microcosm-build/tests/engine_free/uk/test_uk_national_graph.py` (8) runs the composed +national graph on the calibration seam suite's synthetic frame, bound as a checkpoint: + +- The ordered problem is the seam's matrix row for row (`build_constraint_matrix` on the adapter's + prepared frame with the register's target set), with the doctrine bound: `family_equal` loss + weights, the national mass reason, the bounds, the register digests. +- The graph's calibrated weights equal an in-process `calibrate(...)` call under the national + doctrine on the same frame and register; the calibration mass record is the seam's and is appended + once. +- The gate battery evaluates the seven `UK_CALIBRATION_GATE_SCOPE` gates; the evidence block carries + the seam manifest's fields. + +Found while composing: the bound checkpoint's CREATE normaliser defaulted the frame's period to the +FRS vintage (2024), so the projection fence declared 2024 against the fixture's 2023 and blocked. The +checkpoint node now carries its H5's own `time_period` (a node parameter on `uk.full.spine_checkpoint`; +its key moves, no pin). + +## R2. The driver (hermetic) + +`test_uk_rowwise_national_role.py` (23 at the round-1 head; 21 when first written) runs `microcosm-build-uk --release-role national` end to end +on the same fixture: the build record's bindings and provenance, the signed report (posture, +exclusions, release evidence, HMAC), the diagnostics build block, the frozen registries, the +manifest, local and remote staging (fake Hub), the Logbook row on `uk-frs-calibration` with the seam's +phases, the doctrine overrides, the unpinned-feed refusal and override, the incumbent evaluation after +staging, a blocked battery (report on disk, no H5, failed row), and the H5's weights equal to an +in-process calibrate call under the doctrine. + +## R3. Retirement + +`run_uk_calibration`, its attempt runner and battery runner, `UKNationalCalibrationStage`: gone. +The seam suites re-anchor on the graph's route as a library (`encode_uk_national_problem`, +`materialize_uk_national_rows`, `national_calibration_manifest`): `test_uk_national_calibration.py` +26, `test_uk_calibration_run.py` 18 (the checkpoint, scope, admin-anchor, provenance and Logbook-scope +contracts, plus the band-edge reconciliation and attempt-id tests the runner tests carried), +`test_uk_cgt_observation_period.py` 2 (counts as collected at the round-1 head; the body's first +figures, 12 and 1, were miscounted). + +## R4. Sweeps (2026-09-29, tree 218226898 with the PLAN_5 relaxation reverted) + +- The touched suites during the work: 72 driver-side (the driver, the national graph, the + preparation, the national role), 125 retirement-side (the seam suite, the national-calibration + suite, the CGT observation period, the preparation, the rowwise candidate, the national graph and + role), 451 regression (the certifier, the scoring route, the gate-register pins, the data contract, + the battery bindings, the spine graph and evidence, the posture, the full gates and targets, the + diagnostics), all green. +- The final tree: the whole engine-free suite including the uk directory in one process, 11,980 + passed, 119 skipped, one failure, `test_uk_cgt_projection::test_engine_is_reported_unavailable_without_the_uk_extra`, + which asserts the uk extra is absent: it fails identically on main 6a70cd4ee in a venv that has the + extra, and passes in the engine-free CI job, which installs none. The shared-spec lane + (`test_country_spec`, `test_spec_engine_country_bundles`, `test_gate_battery_contract_pins`, the data + contract, `test_stage_evidence`, `test_gate_battery_replay`): 432 passed. `tools/ci_test_groups.py + --verify` ok, `ruff check` clean, every edited file formatted. +- Gate-register digests unchanged (`gates.json` untouched by this change); no pinned node key + moved (the bound checkpoint's new `time_period` parameter moves that node's key, which nothing + pins). + +## R5. Licensed parity, seam vs graph (2026-09-29) + +Harness `data/ukds/acceptance/901-national-ab/` (`run_national_ab.sh`, `compare_national_ab.py`, +`compare_a_vs_b2.json`). Input: the licensed 10 % smoke spine of the #901 pre-merge run +(`901-dense-10pct/spine-a/spine-a.h5`, 53,806 households, sha `a699e756…`), the pinned Chronicle +artifact `chronicle-uk-artifact-5324aa2`, the doctrine (no solve flags: 1,500 epochs, `family_equal`, +learning rate 0.02, seed 0), `--staging-local-only`. Both trees carried the PLAN_5 measurement +relaxation uncommitted (as the smoke run did), so the spine's gate-report digests match the declared +spec; reverted after the runs, never committed. A sampled rung, never exported. + +- A = the seam: main at the #901 merge (6a70cd4ee, `repos/populace-main-901`), + `--release-role national` dispatching to `run_uk_calibration`. 638 s wall. +- B = the graph: this branch at 218226898 (`repos/populace-901r`). 653 s wall (a first run at + 7f875a6f2, before the two evidence-shape fixes, took 627 s). + +Both sides blocked at the terminal battery on the same gate, `uk_cgt_projection_entrants` (129,863 +weighted sub-exempt gainers cross the frozen exempt amount by 2030 against the 73,000 bound): a +property of this 10 % spine under the engine's uprating, identical on both sides, not of either path. +So neither side wrote the H5 or the build record; the comparison stands on what both write before the +battery and on the battery itself. + +Identical, both sides: +- The solve: the same 1,062 × 53,806 constraint matrix (CSR identical), the same row names and target + vector, the same design weights and household axis, and final weights identical to the bit + (max abs diff 0.0; total 29,024,853.117736887). +- The diagnostics file minus the build block: no differing field (initial loss 0.3107629418373108, + final loss 0.008806911920052014, 53,806 non-zero, 97.74 % within 10 %, realized max ratio 10.0, + every one of the 1,062 target rows incl. entity and measure descriptors, the UK block). +- The gate report minus attestation and release evidence: all seven outcomes and their details + (six passed, the projection fence failed with the same numbers); both reports signed. +- The Logbook row: the same pipeline (`uk-frs-calibration`), the same identity digest + (`2344c21d…`, so the graph's `run_config` is the seam's byte for byte) and the same input-pins + digest, both `failed`, rung f100, seed 0. + +The one remaining difference, deliberate: `build.measure_resolution.provider.mode` reads +`direct_h5` on the seam (the resolver simulated the input file) and `scratch_frame_export` on the +graph (the problem node exports the bound frame it holds to a scratch H5 for the engine), with the +matching `source_path`. Both statements are true of what each did; the measures they produced are the +same matrix. The comparison is committed as `docs/evidence/uk-901-national-ab/compare_a_vs_b2.json` +(aggregates and field-level differences only, machine paths omitted). Since the round-1 fix the +graph records that `source_path` relative to its scratch root, so a re-run would show the mode +difference alone. The two shape differences the first run showed (target descriptors read as compiled-row +callables on the household entity; `measure_resolution` carried the engine receipt instead of the +resolution loop's) were fixed in 218226898 and are gone in the re-run. + +Not measured here: the H5 bytes and the build record (blocked before both). The hermetic driver test +checks the H5 (calibrated weights equal to an in-process solve, the seam's single mass record) and the +build record's every field; the readback node checks the written H5's content identity against the +calibrated population. An f100 national A/B on a spine that passes the projection fence is owed when +one exists (the fence is microcosm#970's; the sub-exempt tail of this smoke spine is the #1049 line's +concern, not this PR's). + +## R6. Review round 1 (2026-09-30) + +Rebased over #932's head 94f843f21 (main 7683c0978 plus the atomic assignment; two import hunks in +`full_build_cli.py`, nothing else); the #932 round-1 nits are folded here on María's call, one commit +per finding. + +- C2 (confirmed): `prepare_national_build` runs the seam's `validate_output_paths` on the role's + output paths, so a second run into an occupied `--out` refuses with `FileExistsError` before the + graph is compiled; test = two runs into one directory, the first candidate's H5, build record and + marker byte-identical, a failed second row chained on the first. +- C1 (confirmed for the build record; the manifest half was the dense line's declared convention): + `build.json` is re-issued after the close step on both lines with the final digests of the + manifest and (national) the build record, note dropped; tests on both lines compare the marker with + the files. +- A1 (confirmed): `KeyboardInterrupt` records a `discarded` row on both lines and re-raises (the seam's + contract); tests raise it from the execute step. +- Should-fix 1: scratch-mode `source_path` is recorded relative to the scratch root in the measure + receipt (resolution and resolver receipts), so `uk.full.measures` and `uk.full.national_problem` + are deterministic between runs; test = two resolutions in different scratch directories yield equal + receipts. +- Should-fix 2: the review-date clock is recorded as a deviation (PR body, `docs/uk-full-build-graph.md`, + the #623 runbook); no code change, the dense convention stands. +- Nit 3: the body no longer says "node keys unchanged" for the dense line: `uk.full.dense` (the + `_admit` source) and `uk.full.spine_checkpoint` (`calibration_run` in the hash, the `time_period` + parameter) move; nothing pins them. +- Nit 4: `UKDenseSolveKernel` refuses a bound `mass_rule`, `scale_rule` or `l0_lambda` it does not + honour; the national problem's bindings pass the check in the existing problem test. +- Nit 5: the `if …: pass` is gone; the gate node reads `gate_scope` from the posture (node parameter, + manifest digest still pinned); `national_run_config` refuses an empty register. +- Nit 6: counts corrected above (R2, R3). +- Nit 7: R5's comparison committed under `docs/evidence/uk-901-national-ab/`. +- #932 folds: the registered-support check under `--release-candidate` and in the dense pre-flight + (`country_adapter.uk_atomic_support_register`); z-scores blanked beside suppressed counts in the + #931 evidence (sha 7d120d52…); `LAD23CD`/`LAD24CD` only; the pool docstring, the alias-name comment, + the CLI test comment and the sample-admission sibling under the atomic law. + +Verification at the round-1 head: the targeted UK suites (21 files, the ones the fixes and folds +touch) 345 passed; the whole `tests/engine_free/uk` directory plus the shared-spec lane +(`test_country_spec`, the gate-register pins, the data contract) as recorded in the progress log; +`ruff check .` clean; `tools/ci_test_plan.py verify` ok; gate-register digests unchanged +(`gates.json` untouched; the alias names stay in the allowlist). + diff --git a/experiments/931-uk-atomic-assignment-receipts.md b/experiments/931-uk-atomic-assignment-receipts.md index e1dc06140..ec51a89f7 100644 --- a/experiments/931-uk-atomic-assignment-receipts.md +++ b/experiments/931-uk-atomic-assignment-receipts.md @@ -17,7 +17,7 @@ Disclosure: the committed 1 % evidence and this file report population aggregate ## R1 — f001 (1 %, sample seed 578; committed evidence) -Evidence: `docs/evidence/uk-931/atomic-assignment-cells.json`, sha256 `32f494dff98e81c58fbaa8c03d1a425c1684277e36d4b62757a50f66f44a2a49` (4 cells × 1,011 areas; 985 area counts suppressed as `<3`). +Evidence: `docs/evidence/uk-931/atomic-assignment-cells.json`, sha256 `7d120d529da618edebd5b96b9bbddfcca28ed2e06e6c9bbbebacab5b1a0322e8` (4 cells × 1,011 areas; 985 area counts suppressed as `<3`, their z-scores blanked with them since the round-1 review of #932 found the counts recoverable from `expected_rows` and `z`; the file was re-suppressed in place under the harness's rule, no cell regenerated, from sha256 `32f494df…`). | law | K | rows | source hh | gate | shared gate | London share | const. min rows / ESS / sources | const. max abs z / share abs z>3 (n) | const. breaches | LA min rows / ESS / sources | LA max abs z / share>3 (n) | LA breaches | identity stable | K-growth nested | |---|---:|---:|---:|---|---|---:|---|---|---:|---|---|---:|---|---| diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/__init__.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/__init__.py index cd939f4bf..96e923319 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/__init__.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/__init__.py @@ -400,7 +400,6 @@ validate_uk_national_frame, write_uk_national_frame, ) -from microcosm.build.uk_runtime.national_role import run_national_role from microcosm.build.uk_runtime.national_sampling import ( sample_uk_spine_frame, uk_spine_source_family_units, @@ -785,7 +784,6 @@ "UK_NATIONAL_TARGET_WEIGHT_RULE", "uk_doctrine_with_overrides", "uk_national_target_loss_weights", - "run_national_role", "UKNationalStage", "UKRowwiseDatasetResult", "UKReleaseInputColumn", diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py index 902715b9d..1175fad0c 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/calibration_run.py @@ -1,4 +1,16 @@ -"""Production UK national calibration seam orchestration.""" +"""The UK national calibration seam's contracts: scopes, sidecars, signing. + +The seam's in-process runner (``run_uk_calibration``, the attempt runner and +its gate battery) retired when the national release role moved onto the +shared executable graph (:mod:`.graph_national`, the driver's national path +in :mod:`.full_build_cli`). What stays here is what both lines and the +release-cut certifier still consume: the Logbook pipeline and attempt-id +prefix, the four gate scopes and their closed-world exclusions, the bound +spine sidecar and checkpoint authentication, the spine provenance blocks, +the scoped gate manifest, the CGT projection and admin-anchor artifacts, +the Ledger and runtime provenance blocks, the register census and doctrine +payloads, and the scoped-report signing. +""" from __future__ import annotations @@ -9,70 +21,36 @@ import json import os import platform -import time import uuid -from collections.abc import Callable, Mapping -from dataclasses import dataclass +from collections.abc import Mapping from datetime import UTC, datetime from importlib import metadata from pathlib import Path -from types import SimpleNamespace from typing import Any import numpy as np from microcosm.build.country_spec import GatesManifest, load_country_spec -from microcosm.build.gate_battery import ( - BlockingMode, - EvidenceContext, - GateBatteryRun, - gate_signing_key_env, -) from microcosm.build.gate_battery import ( _canonical_json_bytes as gate_battery_canonical_json_bytes, ) -from microcosm.build.logbook import canonical_json_bytes -from microcosm.build.logbook_adoption import ( - AttemptState, - append_phase, - apply_error_verdict, - error_receipt_path, - git_code_pin, - local_artifact_reference, - record_terminal_attempt, - resolve_predecessor, - role_pins_digest, - write_error_receipt, +from microcosm.build.gate_battery import ( + gate_signing_key_env, ) -from microcosm.build.staging_v2 import validate_staging_delivery -from microcosm.build.target_materialization import assert_calibration_input_finite -from microcosm.build.uk_runtime.battery_bindings import UK_GATE_REGISTRY from microcosm.build.uk_runtime.cgt_projection import ( - UK_CGT_PROJECTION_ARTIFACT_KEY, UKCGTProjection, uk_cgt_projection, uk_cgt_projection_from_pins, uk_engine_installed, ) -from microcosm.build.uk_runtime.diagnostics import ( - uk_target_geography_levels, - write_uk_calibration_diagnostics, -) from microcosm.build.uk_runtime.etb_services import ( UK_NHS_SPENDING_COMPONENT_COLUMNS, ) -from microcosm.build.uk_runtime.national_calibration import UKNationalCalibrationStage from microcosm.build.uk_runtime.national_frame import ( - load_uk_national_frame, uk_household_weight_kind, uk_time_period, - write_uk_national_frame, -) -from microcosm.build.uk_runtime.target_support import ( - write_uk_target_support_sidecars, ) from microcosm.calibrate import TargetRegistry -from microcosm.diagnostics import DiagnosticsWriteFailure from microcosm.frame import Frame _REPOSITORY = Path(__file__).resolve().parents[6] @@ -87,27 +65,6 @@ _PIPELINE = UK_CALIBRATION_PIPELINE -@dataclass(frozen=True) -class UKCalibrationRunPaths: - input_h5: Path - staging_h5: Path - diagnostics_json: Path - build_record_json: Path - terminal_gate_json: Path - - -@dataclass(frozen=True) -class UKCalibrationRunResult: - frame: Frame - diagnostics_sha256: str - staging_sha256: str - build_record_sha256: str - terminal_gate_sha256: str - logbook_spool: Path - gate_report: Mapping[str, object] - build_record: Mapping[str, object] - - UK_CALIBRATION_GATE_SCOPE = ( "uk_target_fit", # The projection fence over sub-exempt gainers is a property of the @@ -283,138 +240,6 @@ def uk_local_gate_scope_exclusions() -> dict[str, str]: return exclusions -def run_uk_calibration( - *, - paths: UKCalibrationRunPaths, - input_sha256: str, - ledger_artifact: Any, - register_registry: TargetRegistry, - band_edge_registry: TargetRegistry, - calibration_year: int, - exclusion_receipt: Mapping[str, Mapping[str, str]], - doctrine: Any, - doctrine_overrides: Mapping[str, Mapping[str, object]], - measure_resolver: object | None, - source_pins: Mapping[str, Mapping[str, object]], - run_config_extra: Mapping[str, object], - release_id: str, - logbook_prev_row_digest: str | None = None, - progress_callback: Callable[[dict[str, object]], None] | None = None, - event_callback: (Callable[[str, str, Mapping[str, object]], None] | None) = None, - staging_delivery: Mapping[str, object] | None = None, - staging_finalizer: Callable[[], None] | None = None, - staging_delivery_provider: Callable[[], Mapping[str, object]] | None = None, - build_id: str | None = None, -) -> UKCalibrationRunResult: - """Run the UK national calibration seam and write its sidecars. - - ``build_id`` lets a caller that opened staging telemetry before the run - (the rowwise driver's national role) mint the attempt id first, so the - telemetry run id and the Logbook row agree; it must carry the seam's - attempt prefix. Omitted, the seam mints one. - """ - - started_at = time.perf_counter() - started_ts = datetime.now(UTC) - if build_id is None: - build_id = new_uk_calibration_attempt_id(timestamp=started_ts) - elif not ( - isinstance(build_id, str) - and build_id.startswith(UK_CALIBRATION_ATTEMPT_ID_PREFIX) - and len(build_id) > len(UK_CALIBRATION_ATTEMPT_ID_PREFIX) - ): - raise ValueError( - "a caller-minted calibration attempt id must start with " - f"{UK_CALIBRATION_ATTEMPT_ID_PREFIX!r}, got {build_id!r}." - ) - # Pure-argument validation precedes every environment probe: an - # incoherent register/receipt/band-edge triple must refuse identically - # whether or not a git checkout or Logbook chain is reachable. - _validate_band_edge_registry( - register_registry=register_registry, - band_edge_registry=band_edge_registry, - exclusion_receipt=exclusion_receipt, - ) - if (staging_finalizer is None) != (staging_delivery_provider is None): - raise ValueError( - "staging_finalizer and staging_delivery_provider must be supplied together." - ) - edge_registry = band_edge_registry - code_pin = git_code_pin(_REPOSITORY) - # Predecessor configuration is validated before anything is written: a - # disagreeing chain must refuse with no artifact on disk, not after a - # staged H5, diagnostics and a signed gate report already exist. - predecessor = resolve_predecessor(logbook_prev_row_digest) - run_config = { - "pipeline": _PIPELINE, - "release_id": release_id, - "register_sha256": register_registry.version, - "calibration_year": int(calibration_year), - "doctrine": _doctrine_payload(doctrine), - "doctrine_overrides": dict(doctrine_overrides), - # The caller verifies the feed's facts and manifest digests; sealing - # the verified identity into run_config carries it through the - # identity digest, the build record and the Logbook row, so the run - # says which Ledger artifact it was measured against. - "ledger": _ledger_provenance(ledger_artifact), - **dict(run_config_extra), - } - run_config["band_edge_register_sha256"] = edge_registry.version - state = AttemptState( - # Attempts are distinct rows even when they re-run one release: both - # the local chain and the store refuse a repeated build id. - build_id=build_id, - identity_digest=hashlib.sha256(canonical_json_bytes(run_config)).hexdigest(), - input_pins_digest=role_pins_digest(source_pins), - phases_reached=["attempt_started"], - gate_verdicts={}, - ) - spool_dir = paths.staging_h5.parent / "logbook-spool" - try: - return _run_uk_calibration_attempt( - paths=paths, - input_sha256=input_sha256, - ledger_artifact=ledger_artifact, - register_registry=register_registry, - band_edge_registry=edge_registry, - calibration_year=calibration_year, - exclusion_receipt=exclusion_receipt, - doctrine=doctrine, - doctrine_overrides=doctrine_overrides, - measure_resolver=measure_resolver, - source_pins=source_pins, - release_id=release_id, - state=state, - run_config=run_config, - code_pin=code_pin, - started_at=started_at, - started_ts=started_ts, - predecessor=predecessor, - spool_dir=spool_dir, - progress_callback=progress_callback, - event_callback=event_callback, - staging_delivery=staging_delivery, - staging_finalizer=staging_finalizer, - staging_delivery_provider=staging_delivery_provider, - ) - except BaseException as error: - # Every terminal disposition records a row — successful, failed, or - # refused (logbook/README.md). A refusal that left no row would be a - # silent gap in the chain the run is supposed to evidence. - _record_failed_attempt( - error=error, - state=state, - started_at=started_at, - started_ts=started_ts, - seed=getattr(doctrine, "seed", None), - code_pin=code_pin, - predecessor=predecessor, - receipt_base_dir=paths.staging_h5.parent, - spool_dir=spool_dir, - ) - raise - - def new_uk_calibration_attempt_id(*, timestamp: datetime) -> str: """Mint a calibration attempt id: the seam prefix, the instant, eight hex.""" @@ -467,369 +292,6 @@ def _registry_spec_names(registry: TargetRegistry) -> set[str]: return {str(spec.name) for spec in registry.specs} -def _record_failed_attempt( - *, - error: BaseException, - state: AttemptState, - started_at: float, - started_ts: datetime, - seed: int | None, - code_pin: str, - predecessor: str | None, - receipt_base_dir: Path, - spool_dir: Path, -) -> None: - if state.spool_path is not None: - return - error_path = write_error_receipt( - error_receipt_path(receipt_base_dir, build_id=state.build_id), - state=state, - pipeline=_PIPELINE, - error=error, - ) - apply_error_verdict( - state, - f"{local_artifact_reference(error_path, repository_hint=_REPOSITORY)}" - "#/error_type", - ) - record_terminal_attempt( - state=state, - started_at=started_at, - started_ts=started_ts, - pipeline=_PIPELINE, - rung="f100", - seed=seed, - code_pin=code_pin, - # An operator interrupt is a discarded attempt, not a failed one; the - # row says which so the chain reads honestly. - disposition=("discarded" if isinstance(error, KeyboardInterrupt) else "failed"), - predecessor=predecessor, - spool_dir=spool_dir, - ) - - -def _run_uk_calibration_attempt( - *, - paths: UKCalibrationRunPaths, - input_sha256: str, - ledger_artifact: Any, - register_registry: TargetRegistry, - band_edge_registry: TargetRegistry, - calibration_year: int, - exclusion_receipt: Mapping[str, Mapping[str, str]], - doctrine: Any, - doctrine_overrides: Mapping[str, Mapping[str, object]], - measure_resolver: object | None, - source_pins: Mapping[str, Mapping[str, object]], - release_id: str, - state: AttemptState, - run_config: Mapping[str, object], - code_pin: str, - started_at: float, - started_ts: datetime, - predecessor: str | None, - spool_dir: Path, - progress_callback: Callable[[dict[str, object]], None] | None, - event_callback: Callable[[str, str, Mapping[str, object]], None] | None, - staging_delivery: Mapping[str, object] | None, - staging_finalizer: Callable[[], None] | None, - staging_delivery_provider: Callable[[], Mapping[str, object]] | None, -) -> UKCalibrationRunResult: - _notify_run_event(event_callback, "input_loading", "started") - measured_input_sha = _sha256_file(paths.input_h5) - if measured_input_sha != input_sha256: - raise ValueError( - "input H5 sha mismatch: " - f"measured {measured_input_sha}, pinned {input_sha256}" - ) - append_phase(state, "input_sha_verified") - frame, _provenance = load_uk_national_frame(paths.input_h5) - append_phase(state, "input_loaded") - spine_sidecar_path = paths.input_h5.with_suffix(".build.json") - spine_sidecar = load_bound_spine_sidecar(spine_sidecar_path, frame) - append_phase(state, "input_sidecar_bound") - assert_calibration_input_finite(frame) - append_phase(state, "input_finite") - _notify_run_event( - event_callback, - "input_loading", - "completed", - entity_row_counts={ - entity: int(len(frame.table(entity))) for entity in frame.entities - }, - ) - - stage = UKNationalCalibrationStage( - register_registry, - # The declared calibration year the register was compiled at — the - # stage validates it; there is deliberately no fallback to the input - # frame's base-year time_period or any ambient default. - period=calibration_year, - doctrine=doctrine, - measure_resolver=measure_resolver, - band_edge_registry=band_edge_registry, - progress_callback=progress_callback, - stage_callback=event_callback, - ) - _notify_run_event(event_callback, "calibration", "started") - calibrated = stage(frame) - append_phase(state, "national_calibration_solved") - _notify_run_event( - event_callback, - "calibration", - "completed", - target_count=len(stage.registry.specs), - entity_row_counts={ - entity: int(len(calibrated.table(entity))) for entity in calibrated.entities - }, - ) - - build_block = { - "build_id": state.build_id, - "code_pin": code_pin, - # Captured at solve time and signed with the diagnostics bytes, so a - # release assembler can only ever pin the environment that actually - # calibrated the candidate — never an invented one. - "runtime": _runtime_provenance(), - "source_pins": dict(source_pins), - "ledger": run_config["ledger"], - "input_posture": { - "tier": "staging_candidate", - "sha256": measured_input_sha, - "size_bytes": paths.input_h5.stat().st_size, - }, - "doctrine": _doctrine_payload(doctrine), - "doctrine_overrides": dict(doctrine_overrides), - "measure_exclusions": dict(exclusion_receipt), - "measure_resolution": ( - stage.manifest.get("measure_resolution") - if isinstance(stage.manifest, Mapping) - else None - ), - "register": _register_census(register_registry, exclusion_receipt), - "spine_provenance": spine_provenance_from_sidecar( - spine_sidecar_path, - spine_sidecar, - ), - "score_vs_enhanced_frs": None, - } - _notify_run_event(event_callback, "diagnostics", "started") - diagnostics_outcome = write_uk_calibration_diagnostics( - stage.solve_result, - paths.diagnostics_json, - calibrated, - target_geography_levels=uk_target_geography_levels(stage.registry), - target_registry=stage.registry, - build=build_block, - ) - if isinstance(diagnostics_outcome, DiagnosticsWriteFailure): - raise RuntimeError( - "UK release validation requires calibration diagnostics, but their " - f"canonical write failed [{diagnostics_outcome.error_code}]: " - f"{diagnostics_outcome.message}" - ) - diagnostics_sha = diagnostics_outcome.sha256 - append_phase(state, "diagnostics_written") - _notify_run_event( - event_callback, - "diagnostics", - "completed", - target_count=len(stage.diagnostics), - ) - - # The solve's compiled system and both weight vectors are written beside - # the diagnostics before the terminal battery runs, so an attempt the - # battery blocks still leaves the per-target weight-stretch anatomy - # readable (tools/diagnose_uk_target_support.py; microcosm#930, the - # #890 acceptance line). Non-release sidecars: the staging H5 posture is - # unchanged, nothing here is a shippable artifact. - _notify_run_event(event_callback, "target_support_sidecars", "started") - sidecars = write_uk_target_support_sidecars( - stage.solve_result, paths.diagnostics_json.parent - ) - append_phase(state, "target_support_sidecars_written") - _notify_run_event( - event_callback, - "target_support_sidecars", - "completed", - files=sorted(str(path.name) for path in sidecars.values()), - ) - - _notify_run_event(event_callback, "release_check_evaluation", "started") - gate_report = _run_calibration_gate_battery( - calibrated, - stage, - paths.terminal_gate_json, - release_id=release_id, - diagnostics_sha256=diagnostics_sha, - ) - append_phase(state, "calibration_gates_evaluated") - _notify_run_event( - event_callback, - "release_check_evaluation", - "completed", - check_count=len(gate_report["gates"]), - ) - for gate_id, payload in gate_report["gates"].items(): - state.gate_verdicts[gate_id] = { - "verdict": payload["status"], - "receipt": f"local://{paths.terminal_gate_json.name}#/gates/{gate_id}", - } - - _notify_run_event(event_callback, "candidate_h5_creation", "started") - write_uk_national_frame(calibrated, paths.staging_h5) - staging_sha = _sha256_file(paths.staging_h5) - append_phase(state, "staging_h5_written") - _notify_run_event( - event_callback, - "candidate_h5_creation", - "completed", - size_bytes=paths.staging_h5.stat().st_size, - ) - - _notify_run_event(event_callback, "build_record_creation", "started") - record = { - "schema_version": 1, - "pipeline": _PIPELINE, - "build_id": state.build_id, - "run_config": run_config, - "source_pins": dict(source_pins), - "role_pins_digest": role_pins_digest(source_pins), - "input_posture": build_block["input_posture"], - "spine_provenance": build_block["spine_provenance"], - "register": build_block["register"], - "calibration": stage.manifest, - "gate_summary": _gate_summary(gate_report), - # No shippability claim lives here: the calibration-scoped battery - # covers 6 of the declared gate entries. The release verdict is the - # release-cut certification's, produced over this record. - "certification": { - "expected_artifact": str( - paths.staging_h5.with_suffix(".release_certification.json") - ), - "producer": "tools/certify_uk_release_cut.py", - }, - "artifacts": { - "staging_h5": {"path": str(paths.staging_h5), "sha256": staging_sha}, - "diagnostics_json": { - "path": str(paths.diagnostics_json), - "sha256": diagnostics_sha, - }, - "terminal_gate_json": { - "path": str(paths.terminal_gate_json), - "sha256": _sha256_file(paths.terminal_gate_json), - }, - }, - } - if staging_delivery is not None: - record["staging_delivery"] = validate_staging_delivery(staging_delivery) - _write_json(paths.build_record_json, record) - build_record_sha = _sha256_file(paths.build_record_json) - append_phase(state, "build_record_written") - _notify_run_event( - event_callback, - "build_record_creation", - "completed", - size_bytes=paths.build_record_json.stat().st_size, - ) - state.artifact_location = local_artifact_reference( - paths.staging_h5, repository_hint=_REPOSITORY - ) - if staging_finalizer is not None: - assert staging_delivery_provider is not None - try: - staging_finalizer() - finally: - record["staging_delivery"] = validate_staging_delivery( - staging_delivery_provider() - ) - _write_json(paths.build_record_json, record) - build_record_sha = _sha256_file(paths.build_record_json) - spool = record_terminal_attempt( - state=state, - started_at=started_at, - started_ts=started_ts, - pipeline=_PIPELINE, - rung="f100", - seed=getattr(doctrine, "seed", None), - code_pin=code_pin, - disposition="iterating", - predecessor=predecessor, - spool_dir=spool_dir, - ) - return UKCalibrationRunResult( - frame=calibrated, - diagnostics_sha256=diagnostics_sha, - staging_sha256=staging_sha, - build_record_sha256=build_record_sha, - terminal_gate_sha256=_sha256_file(paths.terminal_gate_json), - logbook_spool=spool, - gate_report=gate_report, - build_record=record, - ) - - -def _notify_run_event( - callback: Callable[[str, str, Mapping[str, object]], None] | None, - stage_id: str, - status: str, - **details: object, -) -> None: - if callback is not None: - callback(stage_id, status, details) - - -def _run_calibration_gate_battery( - frame: Frame, - stage: UKNationalCalibrationStage, - path: Path, - *, - release_id: str, - diagnostics_sha256: str, -) -> dict[str, object]: - manifest = _calibration_gate_manifest() - admin_totals, admin_receipt = uk_aggregate_admin_totals(frame, manifest) - projection = uk_cgt_projection_artifact(frame, manifest) - artifacts = { - "national_calibration": stage.manifest, - "parity_evidence": SimpleNamespace( - target_relative_errors={ - str(row["name"]): float(row["relative_error"]) - for row in stage.diagnostics - } - ), - "aggregate_admin": admin_totals, - UK_CGT_PROJECTION_ARTIFACT_KEY: projection, - # The target-fit deferral register is evaluated against the run - # clock (schema-2 approval windows); the seam supplies today's date - # exactly as the rowwise candidate build supplies its start date. - "exclusions_evaluated_on": datetime.now(UTC).date(), - } - battery = GateBatteryRun( - manifest, - release_id=release_id, - # The seam never runs release-candidate posture: its scoped battery - # covers 6 of the declared entries and must never sign a - # shippability claim (the #757 release-cut audit). Shippability - # comes only from the release-cut certification. - report_path=path, - release_candidate=False, - registry=UK_GATE_REGISTRY, - release_evidence={"calibration_diagnostics_sha256": diagnostics_sha256}, - ) - battery.run_phase("terminal", EvidenceContext(frame=frame, artifacts=artifacts)) - battery.enforce("terminal", mode=BlockingMode.BLOCKS_ARTIFACT) - payload = battery.report_payload() - finalize_uk_scoped_gate_report( - payload, - posture="calibration_seam", - scope_exclusions=dict(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS), - aggregate_admin_measurement=admin_receipt, - ) - _write_json(path, payload) - return payload - - def load_bound_spine_sidecar(path: Path, frame: Frame) -> dict[str, object]: if not path.is_file(): raise ValueError(f"input H5 build sidecar absent: {path}") diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/country_adapter.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/country_adapter.py index c993fd105..af75ac971 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/country_adapter.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/country_adapter.py @@ -89,6 +89,38 @@ def validate_uk_country_source_projection(spec: CountrySpec) -> None: ) +def uk_atomic_support_register( + spec: CountrySpec | None = None, +) -> dict[str, dict[str, object]]: + """The committed identity of each atomic-area support, by system. + + ``sources.yaml`` pins every support's ``sha256`` and ``byte_size`` (the + provenance register carries the same values). A release candidate must + build on exactly these bytes, whatever digest the operator pinned on + the command line; the dense release pre-flight reads the same register + against the manifest's ``geography.assignment.support_pins``. + """ + from .atomic_area_support import SOURCES + + spec = load_country_spec("uk") if spec is None else spec + if spec.resolved_spec is None: + raise ValueError("The atomic-area support register needs a resolved UK spec.") + rows = spec.resolved_spec.resource("sources").domain.to_wire()["sources"] + by_id = {row["id"]: row for row in rows if row["role"] == "uk_atomic_area_support"} + register = {} + for system, source in SOURCES.items(): + row = by_id.get(source) + if row is None: + raise ValueError( + f"sources.yaml declares no atomic-area support {source!r}." + ) + register[system] = { + "sha256": str(row["sha256"]), + "size_bytes": int(row["byte_size"]), + } + return register + + def build_uk_country_graph( config: UKFullBuildConfig | None = None, *, diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_build_cli.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_build_cli.py index 9eeaad9f9..1b4209464 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_build_cli.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_build_cli.py @@ -6,15 +6,17 @@ ``--release-role`` declares which UK dataset line the run builds and is required (microcosm#823): ``dense`` is the K-clone joint national + local -surface under the local doctrine, built here through the graph; ``national`` -is parsed and validated by the same posture-aware validator and then -dispatched, before any graph preparation, to the retained calibration seam -through :mod:`microcosm.build.uk_runtime.national_role` (its Logbook row, -staging telemetry, manifest and staged bundle live there). The role supplies -every unset solve default and refuses the other role's flags through -:mod:`microcosm.build.uk_runtime.rowwise_cli`. ``tools/build_uk_rowwise_candidate.py`` -and ``tools/build_uk_full.py`` are stubs over :func:`main`, so every UK line -is built by this one command. +surface under the local doctrine; ``national`` is the certified national +line, the bound spine checkpoint solved as bound under the national doctrine +(:mod:`microcosm.build.uk_runtime.graph_national`), with the seam-shaped +evidence (``build_record.json``, the signed calibration-seam gate report, +the diagnostics, the frozen registries, the schema-4 national manifest; +:mod:`microcosm.build.uk_runtime.national_role`) materialised from the +graph's stored artifacts. Both lines are built here through the graph. The +role supplies every unset solve default and refuses the other role's flags +through :mod:`microcosm.build.uk_runtime.rowwise_cli`. +``tools/build_uk_rowwise_candidate.py`` and ``tools/build_uk_full.py`` are +stubs over :func:`main`, so every UK line is built by this one command. A non-dry dense run is wrapped in the rowwise tool's operational envelope: the Logbook attempt (a spooled row under ``/logbook-spool`` on every @@ -39,6 +41,7 @@ import tempfile import time import uuid +from collections.abc import Mapping from dataclasses import asdict, dataclass, replace from datetime import UTC, date, datetime from pathlib import Path @@ -76,7 +79,7 @@ validate_staging_arguments, ) from ..staging_dataset import SHA256SUMS_FILENAME, refresh_sha256sums_entry -from . import national_role +from . import country_adapter, national_role from .atomic_area_support import ( IDENTITY_COLUMN, uk_atomic_assignment_definition, @@ -87,7 +90,13 @@ from .atomic_area_support import ( SYSTEMS as ATOMIC_SUPPORT_SYSTEMS, ) -from .calibration_run import runtime_provenance +from .calibration_run import ( + _write_json, + load_bound_spine_checkpoint, + new_uk_calibration_attempt_id, + runtime_provenance, + spine_provenance_from_sidecar, +) from .chronicle_feed import load_uk_chronicle_feed from .frs_release import load_uk_frs_release from .full_certification import ( @@ -103,8 +112,23 @@ uk_full_graph, ) from .graph_calibration import UKGraphCalibrationConfig +from .graph_national import ( + NATIONAL_GATES_NODE, + NATIONAL_READBACK_NODE, + NATIONAL_TARGETS_NODE, + UKNationalBuildConfig, + UKNationalGraph, + add_uk_national_readback, + graph_payload, + national_build_record, + national_result_from_manifest, + national_run_config, + register_uk_national_kernels, + replay_uk_national_gate_battery, + uk_national_graph, +) from .graph_population import GEOGRAPHY_ASSIGNMENTS -from .graph_targets import TARGET_SELECTION_TYPE +from .graph_targets import TARGET_SELECTION_TYPE, registry_from_payload from .graph_terminal import ( FULL_DIAGNOSTICS_CSV_TYPE, FULL_DIAGNOSTICS_TYPE, @@ -121,7 +145,8 @@ register_uk_terminal_kernels, rowwise_candidate_manifest_from_graph, ) -from .national_frame import load_uk_national_frame +from .national_doctrine import uk_doctrine_with_overrides +from .national_frame import load_uk_national_frame, write_uk_national_frame from .national_sampling import UK_SAMPLE_RUNG_TOKENS from .rowwise_cli import ( MANIFEST_FILENAME, @@ -132,11 +157,13 @@ git_dirty, json_text, new_candidate_build_id, + output_paths, posture_of, record_candidate_attempt, record_candidate_error, refuse_national_role_arguments, resolve_role_arguments, + rowwise_parameters, validate_cli_args, ) from .rowwise_posture import ( @@ -169,10 +196,13 @@ __all__ = [ "UK_ROWWISE_DENSE_POSTURE", "PreparedUKFullBuild", + "PreparedUKNationalBuild", "execute_full_build", + "execute_national_build", "main", "parse_args", "prepare_full_build", + "prepare_national_build", "uk_rowwise_posture", ] @@ -475,6 +505,14 @@ def _prepare_geography( """ if args.geography_assignment != "atomic": return None, {"assignment": "legacy"} + # A release candidate builds on the registered supports (sources.yaml, + # the provenance register): the operator's pin proves the bytes are the + # ones requested, the register proves they are the ones reviewed. + register = ( + country_adapter.uk_atomic_support_register() + if getattr(args, "release_candidate", False) + else None + ) support_pins = {} payloads = {} for system, label in _SUPPORT_ARGUMENTS.items(): @@ -485,6 +523,13 @@ def _prepare_geography( "--atomic-support-scotland and --atomic-support-ni." ) pin = _pin(path, getattr(args, f"atomic_support_sha256_{label}")) + if register is not None and pin != register[system]: + raise ValueError( + f"--release-candidate requires the registered atomic-area support " + f"for --atomic-support-{label}: sha256 {pin['sha256']} " + f"({pin['size_bytes']} bytes) is not the sources.yaml row " + f"{register[system]['sha256']} ({register[system]['size_bytes']} bytes)." + ) pins[ATOMIC_SUPPORT_SOURCES[system]] = pin support_pins[system] = pin payloads[system] = load_raw_bytes(path) @@ -1032,6 +1077,25 @@ def execute_full_build( return status +def _refresh_completion(output: Path, roles: Mapping[str, Path]) -> None: + """Re-issue the completion marker over the files the close step rewrote. + + ``publish_staged_bundle`` places ``build.json`` last, binding the staged + bytes. The close step then appends the staging receipts to the rowwise + manifest (both lines) and the delivery summary to the build record (the + national line, whose release assembler reads it there), so the marker is + re-issued with those files' final digests: it stays the bundle's binding + of what is on disk rather than of what was published a moment earlier. + """ + marker = output / "build.json" + completion = json.loads(marker.read_text()) + for role, path in roles.items(): + entry = dict(completion.get(role) or {}) + entry.pop("note", None) + completion[role] = {**entry, **file_artifact(path)} + materialize_bytes(canonical_json(completion), marker) + + def _close_attempt( args: argparse.Namespace, attempt: dict, @@ -1065,6 +1129,9 @@ def _close_attempt( replace_manifest(output / MANIFEST_FILENAME, manifest) if (output / SHA256SUMS_FILENAME).is_file(): refresh_sha256sums_entry(output, MANIFEST_FILENAME) + _refresh_completion( + output, {"rowwise_candidate_manifest": output / MANIFEST_FILENAME} + ) state.artifact_location = local_artifact_reference( output / f"{stem}.h5", repository_hint=REPOSITORY ) @@ -1526,6 +1593,818 @@ def output_record(path: Path) -> dict: ) +# --------------------------------------------------------------------------- +# The national release role through the graph +# --------------------------------------------------------------------------- + + +@dataclass +class PreparedUKNationalBuild: + national: UKNationalGraph + kernels: KernelRegistry + sources: dict[str, Path] + bindings: dict + pins: dict + inputs: dict + sidecar_path: Path + gates_path: Path + engine_identity: str + source_pins: dict + + +def _ledger_facts_pin(path: Path) -> dict: + facts = path / "consumer_facts.jsonl" if path.is_dir() else path + artifact = file_artifact(facts) + return {"sha256": artifact["sha256"], "size_bytes": int(artifact["size_bytes"])} + + +def prepare_national_build( + args: argparse.Namespace, *, telemetry=None, attempt: dict | None = None +) -> PreparedUKNationalBuild: + """Bind the national request to its graph: the checkpoint, the doctrine, the register.""" + from .spine_build import _rules_engine, _rules_engine_provenance + + posture = posture_of(args) + national_role.require_bound_input(args) + release = load_uk_frs_release() + input_h5 = national_role.require_file(args.input_h5, label="--input-h5") + pins = {"dataset": _pin(input_h5, args.input_sha256)} + inputs = {"dataset": _input_record(input_h5, pins["dataset"], pinned=True)} + frame, _ = load_uk_national_frame(input_h5) + sidecar_path = args.input_sidecar or input_h5.with_suffix(".build.json") + gates_path = args.input_spine_gates or input_h5.with_suffix(".spine_gates.json") + load_bound_spine_checkpoint(sidecar_path, frame, gate_report_path=gates_path) + spine = bound_spine_graph(frame) + _rules_engine() # the uk extra must be installed; the identity is provenance + engine_identity = hashlib.sha256( + canonical_json(_rules_engine_provenance()) + ).hexdigest() + stage( + telemetry, + "input_pinning", + "completed", + dataset_sha256=pins["dataset"]["sha256"], + ) + time_period = str(frame.metadata["time_period"]) + calibration_year = int(args.calibration_year or release.calibration_year) + doctrine, overrides = uk_doctrine_with_overrides( + **national_role.national_doctrine_overrides(args) + ) + if doctrine.target_weight_rule != args.target_weight_rule: + raise RuntimeError( + "national doctrine override did not bind the requested rule." + ) + args._calibration_year = calibration_year + args._frs_vintage = str(release.vintage) + args._doctrine_override_receipt = overrides + if not args.dry_run: + # The seam refused an occupied output directory before any work + # (``_validate_output_paths`` in ``_run_national_attempt``); the + # publication step replaces whatever is at ``--out``, so without this + # a second run would silently replace the first candidate's bytes + # under the Logbook row that names them. + national_role.validate_output_paths( + output_paths(args.out, posture=posture, vintage=args._frs_vintage), + input_h5=input_h5, + ladder_path=None, + ) + config = UKNationalBuildConfig( + calibration_year=calibration_year, + time_period=time_period, + source_year=national_role.source_year( + args.source_year, time_period=str(release.time_period) + ), + doctrine=doctrine, + doctrine_overrides=overrides, + allow_unpinned_feed=bool(args.allow_unpinned_feed), + ) + sources = { + "uk_spine": input_h5, + "uk_spine_evidence": sidecar_path, + "uk_spine_gates": gates_path, + "uk_ledger_facts": args.ledger_facts, + } + optional = [] + for name, path in ( + ("uk_measure_exclusions", args.measure_exclusions), + ("uk_frozen_register", args.register_json), + ): + if path is not None: + sources[name] = path + optional.append(name) + national = uk_national_graph( + config, + spine=spine, + spine_population="uk.full.spine_checkpoint", + optional_target_sources=tuple(optional), + review_date=args.review_date.isoformat(), + posture=posture, + ledger_pins={ + "facts_sha256": args.ledger_facts_sha256, + "manifest_sha256": args.ledger_manifest_sha256, + }, + ) + kernels = KernelRegistry() + register_uk_full_kernels( + kernels, + progress_callback=None if args.dry_run else _solve_observer(args, telemetry), + ) + register_uk_national_kernels(kernels) + compile_graph(national.graph) + source_pins = { + "input_h5": { + "sha256": pins["dataset"]["sha256"], + "size_bytes": int(pins["dataset"]["size_bytes"]), + }, + "ledger_facts": _ledger_facts_pin(Path(args.ledger_facts)), + } + bindings = { + "schema": "microcosm.uk.national-build-request.v1", + "release_role": posture.role, + "release_id": posture.release_id, + "configuration": config.payload(), + "review_date": args.review_date.isoformat(), + "engine_identity": engine_identity, + "inputs": inputs, + "targets": {"ledger": dict(source_pins["ledger_facts"])}, + } + if attempt is not None: + state = attempt["state"] + attempt["code_pin"] = git_code_pin(REPOSITORY) + state.input_pins_digest = role_pins_digest(source_pins) + append_phase(state, "configured") + append_phase(state, "input_sha_verified") + append_phase(state, "input_loaded") + append_phase(state, "input_sidecar_bound") + return PreparedUKNationalBuild( + national, + kernels, + sources, + bindings, + pins, + inputs, + sidecar_path, + gates_path, + engine_identity, + source_pins, + ) + + +def execute_national_build( + prepared: PreparedUKNationalBuild, + args: argparse.Namespace, + *, + telemetry=None, + attempt: dict | None = None, +) -> int: + """Stage complete files, then publish their completion marker last.""" + if args.dry_run: + return _execute_national_build(prepared, args) + from microcosm.build.artifact_files import publish_staged_bundle + + stage_sample(telemetry, sample_fraction=args.sample_fraction) + output, graph_store = _output_locations(None, args) + output.parent.mkdir(parents=True, exist_ok=True) + args.attempt_evidence = graph_store / "uk-national-attempts" / uuid.uuid4().hex + record: dict = {} + with tempfile.TemporaryDirectory( + prefix=f".{output.name}.national-build-", dir=output.parent + ) as temporary: + staged_args = argparse.Namespace(**vars(args)) + staged_args.out = Path(temporary) + staged_args.graph_store = graph_store + staged_args.published_out = output + status = _execute_national_build( + prepared, staged_args, telemetry=telemetry, attempt=attempt, record=record + ) + if not (staged_args.out / "build.json").exists(): + materialize_bytes( + canonical_json( + { + "schema_version": 1, + "kind": "uk_national_build_refused", + "request": prepared.bindings, + "release_authorized": False, + "artifact_permitted": False, + } + ), + staged_args.out / "build.json", + ) + staged = { + ("manifest" if path.name == "build.json" else path.name): path + for path in staged_args.out.iterdir() + if path.is_file() + } + destinations = {role: output / path.name for role, path in staged.items()} + publish_staged_bundle(staged, destinations, completion_role="manifest") + if status == 0: + print( + f"UK national build: {output / record['dataset_name']}; the " + "release verdict is tools/certify_uk_release_cut.py's.", + file=sys.stderr, + ) + if attempt is not None: + _close_national_attempt( + args, + attempt, + output=output, + status=status, + record=record, + telemetry=telemetry, + ) + return status + + +def _execute_national_build( + prepared: PreparedUKNationalBuild, + args: argparse.Namespace, + *, + telemetry=None, + attempt: dict | None = None, + record: dict | None = None, +) -> int: + from microcosm.build.gate_battery import GateBatteryBlockedError + from microcosm.build.logbook import canonical_json_bytes + from microcosm.diagnostics import DiagnosticsWriteFailure + + from .diagnostics import write_uk_calibration_diagnostics + from .target_support import write_uk_target_support_sidecars + + national, kernels, sources = prepared.national, prepared.kernels, prepared.sources + if args.dry_run: + print(json.dumps(national.operation_inventory(), indent=2)) + return 0 + posture = posture_of(args) + state: AttemptState | None = None if attempt is None else attempt["state"] + record = {} if record is None else record + vintage = args._frs_vintage + paths = output_paths(args.out, posture=posture, vintage=vintage) + published_root = Path(getattr(args, "published_out", args.out)) + published = output_paths(published_root, posture=posture, vintage=vintage) + record["dataset_name"] = paths["dataset"].name + args.out.mkdir(parents=True, exist_ok=True) + store = ContentStore(args.graph_store or args.out / ".graph-store") + graph = national.graph + materialize_bytes(graph_to_json(graph).encode(), args.out / "graph.json") + materialize_bytes( + canonical_json(national.operation_inventory()), args.out / "operations.json" + ) + materialize_bytes( + canonical_json(prepared.bindings), args.attempt_evidence / "request.json" + ) + build_id = "dry-run" if state is None else state.build_id + resume = args.resume + # 1. The bound checkpoint: its provenance artifact is the solve's admission. + stage(telemetry, "input_loading", "started") + checkpoint = run_graph( + compile_graph(_through(graph, "uk.full.spine_checkpoint")), + sources=sources, + store=store, + kernels=kernels, + resume=resume, + ) + _persist_checkpoint(checkpoint, store, args, "uk.full.spine_checkpoint") + resume = "require" if args.resume == "require" else "auto" + spine_frame = checkpoint.population("uk.full.spine_checkpoint") + stage( + telemetry, + "input_loading", + "completed", + entity_row_counts={ + entity: int(len(spine_frame.table(entity))) + for entity in spine_frame.entities + }, + ) + # 2. The register: compiled inside the graph, frozen beside the outputs. + stage(telemetry, "target_compilation", "started") + targets = run_graph( + compile_graph(_through(graph, NATIONAL_TARGETS_NODE)), + sources=sources, + store=store, + kernels=kernels, + resume=resume, + ) + registry_document = json.loads( + graph_payload(targets, store, NATIONAL_TARGETS_NODE, "registry") + ) + national_registry = registry_from_payload(registry_document["national_registry"]) + band_edge_registry = registry_from_payload(registry_document["band_edge_registry"]) + stage( + telemetry, + "target_compilation", + "completed", + compiled_target_count=len(band_edge_registry.specs), + active_target_count=len(national_registry.specs), + ) + # The frozen register is the scorer's input: the same artifact this run + # solved against, by content hash; the full compiled register beside it + # keeps the band edges a pruned scoring surface must never redraw (#803). + national_registry.to_json(paths["national_registry"]) + band_edge_registry.to_json(paths["contract_registry"]) + source_year = national.config.source_year + run_config = national_run_config( + posture=posture, + config=national.config, + registry_document=registry_document, + driver_parameters=rowwise_parameters(args, source_year=source_year), + ) + if state is not None: + state.identity_digest = hashlib.sha256( + canonical_json_bytes(run_config) + ).hexdigest() + append_phase(state, "targets_bound") + inputs = { + "national_registry": national_registry, + "band_edge_registry": band_edge_registry, + "calibration_year": int(registry_document["calibration_year"]), + "measure_exclusions": dict(registry_document["measure_exclusions"]), + "chronicle_provenance": dict(registry_document["chronicle_provenance"]), + } + record["inputs"] = inputs + record["incumbent"] = national_role.incumbent_arguments(args) + # 3. The solve and the seam battery, evaluated in the graph. + stage( + telemetry, + "calibration", + "started", + epochs=int(args.epochs), + epoch_every=staging_epoch_every(args), + ) + manifest = run_graph( + compile_graph(_through(graph, NATIONAL_GATES_NODE)), + sources=sources, + store=store, + kernels=kernels, + resume=resume, + ) + _persist_checkpoint(manifest, store, args, "numerical") + _materialize_evidence(manifest, store, args.out) + _require_gate_kernel_completed(manifest, NATIONAL_GATES_NODE) + if state is not None: + append_phase(state, "national_calibration_solved") + frame = manifest.population(national.population) + result, problem = national_result_from_manifest( + manifest, store, frame=frame, registry=national_registry + ) + evidence = json.loads( + graph_payload(manifest, store, NATIONAL_GATES_NODE, "calibration_evidence") + ) + gate_document = json.loads( + graph_payload(manifest, store, NATIONAL_GATES_NODE, "gate_report") + ) + stage( + telemetry, + "calibration", + "completed", + target_count=len(national_registry.specs), + entity_row_counts={ + entity: int(len(frame.table(entity))) for entity in frame.entities + }, + ) + # 4. The seam diagnostics, written through the seam writer with the + # build block; the digest is measured from the file, never declared. + sidecar = json.loads(prepared.sidecar_path.read_text(encoding="utf-8")) + spine_provenance = spine_provenance_from_sidecar(prepared.sidecar_path, sidecar) + input_posture = { + "tier": "staging_candidate", + "sha256": prepared.source_pins["input_h5"]["sha256"], + "size_bytes": prepared.source_pins["input_h5"]["size_bytes"], + } + build_block = { + "build_id": build_id, + "code_pin": "unresolved-local-git-code-pin" + if attempt is None + else str(attempt["code_pin"]), + "runtime": runtime_provenance(), + "source_pins": dict(prepared.source_pins), + "ledger": run_config["ledger"], + "input_posture": input_posture, + "doctrine": run_config["doctrine"], + "doctrine_overrides": run_config["doctrine_overrides"], + "measure_exclusions": inputs["measure_exclusions"], + "measure_resolution": evidence["calibration"].get("measure_resolution"), + "register": evidence["register"], + "spine_provenance": spine_provenance, + "score_vs_enhanced_frs": None, + } + stage(telemetry, "diagnostics", "started") + diagnostics_outcome = write_uk_calibration_diagnostics( + result, + paths["calibration_diagnostics"], + frame, + target_geography_levels=evidence["target_geography_levels"], + target_registry=national_registry, + build=build_block, + ) + if isinstance(diagnostics_outcome, DiagnosticsWriteFailure): + raise RuntimeError( + "UK release validation requires calibration diagnostics, but their " + f"canonical write failed [{diagnostics_outcome.error_code}]: " + f"{diagnostics_outcome.message}" + ) + diagnostics_sha = diagnostics_outcome.sha256 + if state is not None: + append_phase(state, "diagnostics_written") + stage( + telemetry, + "diagnostics", + "completed", + target_count=len(evidence["diagnostics"]), + ) + # 5. The solve's compiled system beside the diagnostics, before the + # terminal battery, so a blocked attempt keeps its anatomy readable. + stage(telemetry, "target_support_sidecars", "started") + sidecars = write_uk_target_support_sidecars(result, args.out) + if state is not None: + append_phase(state, "target_support_sidecars_written") + stage( + telemetry, + "target_support_sidecars", + "completed", + files=sorted(str(path.name) for path in sidecars.values()), + ) + # 6. The seam battery, replayed from the graph's phase report and signed. + stage(telemetry, "release_check_evaluation", "started") + gate_report_path = paths["terminal_gates"] + try: + gate_report = replay_uk_national_gate_battery( + gate_document, + report_path=gate_report_path, + release_id=posture.release_id, + diagnostics_sha256=diagnostics_sha, + posture=posture, + ) + except GateBatteryBlockedError as blocked: + record["blocking_failures"] = list(blocked.failures) + written = json.loads(gate_report_path.read_text(encoding="utf-8")) + gate_rows = written.get("gates", {}) + record["gate_report"] = written + if state is not None: + _apply_graph_gate_verdicts( + state, gate_rows, published_root / gate_report_path.name + ) + append_phase(state, "calibration_gates_evaluated") + append_phase(state, "candidate_blocked") + stage( + telemetry, + "release_check_evaluation", + "completed", + check_count=len(gate_rows), + blocking_failure_count=len(blocked.failures), + ) + print( + "Calibration-seam battery blocked the national artifact; evidence " + f"bundle written, no H5: {blocked.failures[:5]}", + file=sys.stderr, + ) + return 1 + gate_rows = gate_report["gates"] + record["gate_report"] = gate_report + if state is not None: + _apply_graph_gate_verdicts( + state, gate_rows, published_root / gate_report_path.name + ) + append_phase(state, "calibration_gates_evaluated") + stage( + telemetry, + "release_check_evaluation", + "completed", + check_count=len(gate_rows), + ) + # 7. The H5, then the graph's readback of it. + stage(telemetry, "candidate_h5_creation", "started") + write_uk_national_frame(frame, paths["dataset"]) + if state is not None: + append_phase(state, "staging_h5_written") + stage( + telemetry, + "candidate_h5_creation", + "completed", + size_bytes=paths["dataset"].stat().st_size, + ) + continued = add_uk_national_readback(graph, population=national.population) + final = run_graph( + compile_graph(continued), + sources={**sources, "exported_dataset": paths["dataset"]}, + store=store, + kernels=kernels, + resume="auto", + ) + _require_gate_kernel_completed(final, NATIONAL_READBACK_NODE) + readback = json.loads( + graph_payload(final, store, NATIONAL_READBACK_NODE, "readback") + ) + if not readback["passed"]: + raise RuntimeError( + "UK national H5 readback failed: " + "; ".join(readback["failures"]) + ) + final.save(args.out / "build.graph.json") + _materialize_evidence(final, store, args.out) + # 8. The build record the release-cut certifier reads. + stage(telemetry, "build_record_creation", "started") + artifacts = { + "staging_h5": { + "path": str(published["dataset"]), + "sha256": file_artifact(paths["dataset"])["sha256"], + }, + "diagnostics_json": { + "path": str(published["calibration_diagnostics"]), + "sha256": diagnostics_sha, + }, + "terminal_gate_json": { + "path": str(published["terminal_gates"]), + "sha256": file_artifact(gate_report_path)["sha256"], + }, + } + graph_keys = { + node_id: dict(receipt.opaque_artifacts) + for node_id, receipt in final.nodes.items() + } + build_record = national_build_record( + posture=posture, + build_id=build_id, + run_config=run_config, + source_pins=prepared.source_pins, + input_posture=input_posture, + spine_provenance=spine_provenance, + register=evidence["register"], + calibration=evidence["calibration"], + gate_report=gate_report, + artifacts=artifacts, + staging_delivery=staging_delivery(telemetry), + graph={"artifacts": graph_keys, "readback": readback}, + ) + _write_json(paths["build_record"], build_record) + if state is not None: + append_phase(state, "build_record_written") + state.artifact_location = local_artifact_reference( + published["dataset"], repository_hint=REPOSITORY + ) + stage( + telemetry, + "build_record_creation", + "completed", + size_bytes=paths["build_record"].stat().st_size, + ) + # 9. The national manifest over the seam-shaped evidence. + diagnostics = json.loads(paths["calibration_diagnostics"].read_text()) + manifest_payload = national_role.national_manifest( + args, + posture=posture, + build_id=build_id, + build_record=build_record, + build_record_path=paths["build_record"], + gate_report=gate_report, + diagnostics=diagnostics, + inputs=inputs, + doctrine_overrides=national.config.doctrine_overrides, + output_paths=paths, + input_artifact=prepared.inputs["dataset"], + source_year=source_year, + reported_paths=published, + graph={"artifacts": graph_keys, "readback": readback}, + ) + materialize_bytes(json_text(manifest_payload).encode(), paths["manifest"]) + record["manifest"] = manifest_payload + record["build_record"] = build_record + completion = { + "schema_version": 1, + "kind": "uk_national_build_completion", + "release_role": posture.role, + "release_id": posture.release_id, + "build_id": build_id, + "dataset": { + **file_artifact(paths["dataset"]), + "path": str(published["dataset"]), + }, + "build_record": { + **file_artifact(paths["build_record"]), + "path": str(published["build_record"]), + }, + "rowwise_candidate_manifest": { + **file_artifact(paths["manifest"]), + "note": "staging receipts are appended after publication", + }, + "graph": {"artifacts": graph_keys, "readback": readback}, + "release_authorized": False, + } + materialize_bytes(canonical_json(completion), args.out / "build.json") + return 0 + + +def _close_national_attempt( + args: argparse.Namespace, + attempt: dict, + *, + output: Path, + status: int, + record: dict, + telemetry, +) -> None: + """Stage the published bundle, evaluate, close the telemetry, spool the row. + + The seam's order: the manifest and the staged bundle first, the + incumbent evaluation after the bundle is on the Hub and before the + telemetry completes, then the build record and the manifest rewritten + with the delivery summary and the sums refreshed. + """ + from microcosm.build.staging_v2 import validate_staging_delivery + + posture = posture_of(args) + state: AttemptState = attempt["state"] + manifest = record.get("manifest") + blocked = bool(record.get("blocking_failures")) + paths = output_paths(output, posture=posture, vintage=args._frs_vintage) + if manifest is not None: + append_phase(state, "published") + args._gate_report = record["gate_report"] + staged_dataset = stage_dataset( + args, + manifest=manifest, + output_paths=paths, + run_id=state.build_id if telemetry is None else telemetry.run_id, + telemetry=telemetry, + ) + append_phase(state, STAGED_DATASET_PHASES[staged_dataset["status"]]) + evaluation = national_role.evaluate_against_incumbent( + args, + incumbent=record.get("incumbent"), + inputs=record["inputs"], + output_paths=paths, + telemetry=telemetry, + calibration_year=int(record["inputs"]["calibration_year"]), + out_dir=output, + ) + try: + finalize_staging_telemetry(args, telemetry) + finally: + delivery = staging_delivery(telemetry) + build_record = json.loads(paths["build_record"].read_text()) + build_record["staging_delivery"] = validate_staging_delivery(delivery) + _write_json(paths["build_record"], build_record) + manifest["outputs"]["build_record"] = national_role.artifact_info( + paths["build_record"] + ) + manifest["build_record"] = { + "path": str(paths["build_record"]), + "sha256": manifest["outputs"]["build_record"]["sha256"], + } + manifest["staging_delivery"] = delivery + manifest["staged_dataset"] = staged_dataset + manifest["evaluation"] = evaluation + if evaluation.get("status") == "completed": + manifest["outputs"]["score_receipt"] = evaluation["receipt"] + replace_manifest(paths["manifest"], manifest) + if (output / SHA256SUMS_FILENAME).is_file(): + refresh_sha256sums_entry(output, paths["build_record"].name) + if evaluation.get("status") == "completed": + national_role.list_sha256sums_entry( + output, paths["score_receipt"].name + ) + refresh_sha256sums_entry(output, MANIFEST_FILENAME) + _refresh_completion( + output, + { + "build_record": paths["build_record"], + "rowwise_candidate_manifest": paths["manifest"], + }, + ) + else: + finalize_staging_telemetry(args, telemetry) + spool_path = record_candidate_attempt( + state=state, + started_at=attempt["started_at"], + started_ts=attempt["started_ts"], + seed=args.seed, + code_pin=str(attempt["code_pin"]), + disposition="failed" if (blocked or status != 0) else "iterating", + predecessor=attempt["predecessor"], + spool_dir=output / "logbook-spool", + rung="f100", + pipeline=posture.pipeline, + ) + print(f"Wrote Logbook row: {spool_path}", file=sys.stderr) + if manifest is not None: + print(json_text(manifest), end="") + + +def _national_main(args: argparse.Namespace) -> int: + """The national role's envelope: the seam's attempt id and pipeline, the graph build.""" + posture = posture_of(args) + national_role.require_bound_input(args) + if args.dry_run: + return national_role.national_dry_run( + args, + operation_inventory=lambda: prepare_national_build( + args + ).national.operation_inventory(), + ) + preflight_staged_dataset(args) + started_at = time.perf_counter() + started_ts = datetime.now(UTC) + digest = preflight_digest(posture.pipeline) + state = AttemptState( + # The attempt id is minted before telemetry opens so the staging run + # id and the Logbook row agree, as the seam minted it. + build_id=new_uk_calibration_attempt_id(timestamp=started_ts), + identity_digest=digest, + input_pins_digest=digest, + phases_reached=["attempt_started"], + gate_verdicts={ + "pipeline": { + "verdict": "running", + "receipt": "pending-build-scoped-terminal-receipt", + } + }, + ) + predecessor = resolve_predecessor(args.logbook_prev_row_digest) + telemetry = create_staging_telemetry(args, build_id=state.build_id) + attempt = { + "state": state, + "started_at": started_at, + "started_ts": started_ts, + "code_pin": "unresolved-local-git-code-pin", + "predecessor": predecessor, + } + try: + prepared = prepare_national_build(args, telemetry=telemetry, attempt=attempt) + return execute_national_build( + prepared, args, telemetry=telemetry, attempt=attempt + ) + except KeyboardInterrupt as interrupt: + # An operator interrupt is a discarded attempt, not a failed one; the + # seam recorded the row so and re-raised (every terminal disposition + # records a row), and the staging run closes as failed. + _record_failure( + args, + interrupt, + state=state, + attempt=attempt, + pipeline=posture.pipeline, + disposition="discarded", + ) + fail_staging_telemetry(telemetry, interrupt) + raise + except Exception as error: + _record_failure( + args, error, state=state, attempt=attempt, pipeline=posture.pipeline + ) + fail_staging_telemetry(telemetry, error) + print(f"UK national build failed: {error}", file=sys.stderr) + return 1 + + +def _record_failure( + args: argparse.Namespace, + error: BaseException, + *, + state: AttemptState, + attempt: dict, + pipeline: str, + prepared: PreparedUKFullBuild | None = None, + disposition: str = "failed", +) -> None: + """The failure sidecar and the terminal Logbook row, when the output is safe. + + ``disposition`` is ``failed`` for an error and ``discarded`` for an + operator interrupt; the dense line passes its ``prepared`` build so the + output check sees the declared sources. + """ + try: + _output_locations(prepared, args) + except ValueError: + return + materialize_bytes( + canonical_json( + { + "schema": "microcosm.uk.full-build-failure.v1", + "error_type": type(error).__name__, + "message": str(error), + "evidence_directory": str(args.attempt_evidence) + if hasattr(args, "attempt_evidence") + else None, + "release_authorized": False, + } + ), + args.out / "failure.json", + ) + if state.spool_path is None: + record_candidate_error( + error=error, + state=state, + started_at=attempt["started_at"], + started_ts=attempt["started_ts"], + seed=args.seed, + code_pin=str(attempt["code_pin"]), + predecessor=attempt["predecessor"], + base_dir=args.out.resolve(), + spool_dir=args.out.resolve() / "logbook-spool", + rung=UK_SAMPLE_RUNG_TOKENS[args.sample_fraction], + pipeline=pipeline, + disposition=disposition, + ) + + def _dry_run(args: argparse.Namespace) -> int: """Plan without solving or writing; loop over candidate clone counts.""" counts = args.candidate_clone_counts @@ -1548,9 +2427,9 @@ def main(argv: list[str] | None = None) -> int: validate_cli_args(args) posture = posture_of(args) if posture.role == "national": - # The national line is the retained calibration seam under the - # driver's posture (microcosm#823); no graph is prepared for it. - return national_role.run_national_role(args) + # The national line: the bound checkpoint solved as bound under the + # national doctrine, on the same graph (microcosm#823 line). + return _national_main(args) if args.candidate_clone_counts is not None and not args.dry_run: raise ValueError("--candidate-clone-counts is valid only with --dry-run.") if args.dry_run: @@ -1595,41 +2474,29 @@ def main(argv: list[str] | None = None) -> int: try: prepared = prepare_full_build(args, telemetry=telemetry, attempt=attempt) return execute_full_build(prepared, args, telemetry=telemetry, attempt=attempt) + except KeyboardInterrupt as interrupt: + # As on the national line: a discarded row, the staging run failed, + # the interrupt re-raised. + _record_failure( + args, + interrupt, + state=state, + attempt=attempt, + pipeline=posture.pipeline, + prepared=prepared, + disposition="discarded", + ) + fail_staging_telemetry(telemetry, interrupt) + raise except Exception as error: - safe_output = False - try: - _output_locations(prepared, args) - safe_output = True - except ValueError: - pass - if safe_output: - materialize_bytes( - canonical_json( - { - "schema": "microcosm.uk.full-build-failure.v1", - "error_type": type(error).__name__, - "message": str(error), - "evidence_directory": str(args.attempt_evidence) - if hasattr(args, "attempt_evidence") - else None, - "release_authorized": False, - } - ), - args.out / "failure.json", - ) - if state.spool_path is None: - record_candidate_error( - error=error, - state=state, - started_at=started_at, - started_ts=started_ts, - seed=args.seed, - code_pin=str(attempt["code_pin"]), - predecessor=predecessor, - base_dir=args.out.resolve(), - spool_dir=args.out.resolve() / "logbook-spool", - rung=UK_SAMPLE_RUNG_TOKENS[args.sample_fraction], - ) + _record_failure( + args, + error, + state=state, + attempt=attempt, + pipeline=posture.pipeline, + prepared=prepared, + ) fail_staging_telemetry(telemetry, error) print(f"UK full build failed: {error}", file=sys.stderr) return 1 diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_measure.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_measure.py index a2ec27aac..0dac211c3 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_measure.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_measure.py @@ -33,6 +33,35 @@ ) +def _without_scratch_paths(value, scratch_dir: Path): + """Record scratch-relative paths in a receipt, never the scratch root. + + A scratch-mode resolver names the file it simulated from (its + ``source_path``), which lies under the per-run temporary directory the + measure node creates. The receipt rides into the problem's bindings and + the stored measure artifact, so an absolute scratch path would change a + deterministic node's output between identical runs; the path is kept + relative to the scratch root, which is stable (``simulation-input.h5``, + ``clone-0/simulation-input.h5``). + """ + if isinstance(value, Mapping): + return { + key: _without_scratch_paths(item, scratch_dir) + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [_without_scratch_paths(item, scratch_dir) for item in value] + if isinstance(value, str) and value: + root = scratch_dir.resolve() + candidate = Path(value) + if candidate.is_absolute(): + try: + return str(candidate.resolve().relative_to(root)) + except ValueError: + return value + return value + + def resolve_uk_full_measures( frame, national_registry, @@ -116,6 +145,7 @@ def resolve_uk_full_measures( measure_parts: dict[tuple[str, str], list[pd.Series]] = {} metric_parts: dict[str, list[pd.DataFrame]] = {grain: [] for grain in local_grains} resolver_receipts: list[Mapping[str, Any]] = [] + resolution_receipts: list[dict[str, Any]] = [] national_input_keys: set[tuple[str, str]] | None = None for clone_index, block_frame in block_frames: block_scratch = ( @@ -133,6 +163,9 @@ def resolve_uk_full_measures( resolver, period=period, ) + resolution_receipts.append( + _without_scratch_paths(dict(resolution.receipt), scratch_dir) + ) keys = set(resolution.measure_inputs) if national_input_keys is None: national_input_keys = keys @@ -159,7 +192,9 @@ def resolve_uk_full_measures( household_ids=block_household_ids, ) ) - resolver_receipts.append(resolver.receipt()) + resolver_receipts.append( + _without_scratch_paths(dict(resolver.receipt()), scratch_dir) + ) del resolver simulation_input = block_scratch / "simulation-input.h5" simulation_input.unlink(missing_ok=True) @@ -242,6 +277,11 @@ def resolve_uk_full_measures( for area_type, metrics in local_metrics.items() }, "blocks": blocks, + "target_materialization": materialized.report(), + # The resolution loop's own receipt per block (which measure came from + # which provider, the rounds, the provider's receipt): the seam + # manifest's ``measure_resolution`` block, one per engine block. + "resolution": resolution_receipts, } if cgt_period_contract is not None: receipt["cgt_period_contract"] = cgt_period_contract diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_targets.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_targets.py index afff50fff..91c4ba5b0 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/full_targets.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/full_targets.py @@ -17,7 +17,10 @@ _ledger_provenance, _validate_band_edge_registry, ) -from microcosm.build.uk_runtime.chronicle_feed import load_uk_chronicle_feed +from microcosm.build.uk_runtime.chronicle_feed import ( + load_uk_chronicle_feed, + require_committed_uk_chronicle_feed_pin, +) from microcosm.build.uk_runtime.frs_release import load_uk_frs_release from microcosm.build.uk_runtime.ledger_targets import ( compile_uk_local_target_registry, @@ -225,3 +228,94 @@ def load_uk_full_target_inputs( "uk_ledger_compiled_registries": national_registries, "uk_ledger_compiled_local_registries": local_registries, } + + +def load_uk_national_target_inputs( + facts_path: str | Path, + *, + measure_exclusions: str | Path | None = None, + register_json: str | Path | None = None, + calibration_year: int | None = None, + exclusions_evaluated_on: date | None = None, + allow_unpinned_feed: bool = False, + expected_facts_sha256: str | None = None, + expected_manifest_sha256: str | None = None, +) -> dict[str, Any]: + """The national role's target surface: the pinned Ledger artifact, compiled. + + The seam's loader (``national_role._load_national_target_inputs``) as the + graph's national target node runs it: the artifact must be the committed + Chronicle feed pin unless ``allow_unpinned_feed`` records a reviewed + diagnostic run; the compiled register less the measure exclusions is the + solve surface and the full compiled register keeps the band edges; + ``register_json`` requires the re-derived scoring surface to be the frozen + one. No local registry and no ladder: the national line has neither. + """ + + artifact = load_ledger_consumer_artifact( + Path(facts_path), + expected_facts_sha256=expected_facts_sha256, + expected_manifest_sha256=expected_manifest_sha256, + ) + pin = require_committed_uk_chronicle_feed_pin( + artifact.facts_sha256, + manifest_sha256=artifact.manifest_sha256, + allow_unpinned_feed=bool(allow_unpinned_feed), + ) + year = ( + load_uk_frs_release().calibration_year + if calibration_year is None + else calibration_year + ) + if type(year) is not int or year <= 0: + raise ValueError("calibration_year must be a positive integer.") + evaluated_on = exclusion_evaluation_date(exclusions_evaluated_on) + compilation = compile_uk_target_registry(artifact.facts, target_period=year) + if compilation.unsupported: + raise ValueError( + f"{len(compilation.unsupported)} national target references failed " + f"to compile for {year}: {compilation.unsupported!r}." + ) + exclusions = load_uk_calibration_measure_exclusions( + None if measure_exclusions is None else Path(measure_exclusions) + ) + registry, exclusion_receipt = apply_uk_calibration_measure_exclusions( + compilation.registry, exclusions, now=evaluated_on + ) + _validate_band_edge_registry( + register_registry=registry, + band_edge_registry=compilation.registry, + exclusion_receipt=exclusion_receipt, + ) + frozen_version = None + if register_json is not None: + try: + frozen = TargetRegistry.from_json(Path(register_json)) + except ValueError as error: + raise ValueError(f"frozen scoring register is unusable: {error}") from error + frozen_version = frozen.version + if frozen.version != registry.version: + raise ValueError( + "re-derived register differs from the frozen scoring register: " + f"{registry.version} vs {frozen.version}" + ) + return { + "artifact": artifact, + "calibration_year": year, + "national_registry": registry, + "band_edge_registry": compilation.registry, + "measure_exclusions": exclusion_receipt, + "chronicle_feed_pin": pin.to_dict(), + "chronicle_provenance": artifact.provenance(), + "ledger_provenance": _ledger_provenance(artifact), + "register_completeness": { + "compiled_registry_version": compilation.registry.version, + "approved_registry_version": registry.version, + "frozen_registry_version": frozen_version, + "compiled_reference_count": len(compilation.registry.specs), + "approved_reference_count": len(registry.specs), + "measure_exclusion_count": len(exclusion_receipt), + "exclusions_evaluated_on": evaluated_on.isoformat(), + "band_edge_registry_reconciled": True, + }, + } diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/geography_sources.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/geography_sources.py index e10e14fdd..3db7dffc6 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/geography_sources.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/geography_sources.py @@ -14,6 +14,7 @@ import urllib.error import urllib.request import zipfile +from collections.abc import Mapping from pathlib import Path from typing import Any @@ -202,20 +203,15 @@ def load_ew_oa_lad_region_lookup( Returns one row per OA with ``oa_code``, ``la_code`` and ``region_code`` (blank for Wales, whose region is the ``W99999999`` sentinel downstream). - The LAD column is accepted under any ``LADCD`` name; the December 2024 - file carries the April 2023 LAD code set as ``LAD24CD``. + The LAD column is accepted as ``LAD23CD`` or ``LAD24CD`` only (the + December 2024 file carries the April 2023 LAD code set as ``LAD24CD``); + a later vintage such as ``LAD25CD`` is the separate recode follow-up, and + a file carrying two of the accepted names is refused rather than guessed. """ frame = _read_csv_url(url, dtype=str) upper_to_column = {str(column).strip().upper(): column for column in frame} - lad_column = next( - ( - column - for upper, column in upper_to_column.items() - if upper.startswith("LAD") and upper.endswith("CD") - ), - None, - ) + lad_column = _lad23_code_column(upper_to_column) region_column = next( ( column @@ -1498,13 +1494,36 @@ def normalise_lad23_names(frame: pd.DataFrame) -> pd.DataFrame: ) +#: Column names under which the April 2023 LAD code set is published: the +#: April 2023 lookups name it ``LAD23CD``, the December 2024 OA lookup +#: ``LAD24CD`` (the same code set). ``LAD25CD`` carries the April 2025 +#: recode (Barnsley/Sheffield among others) and is not accepted here. +LAD23_CODE_COLUMNS: tuple[str, ...] = ("LAD23CD", "LAD24CD") + + +def _lad23_code_column(upper_to_column: Mapping[str, str]) -> str | None: + """The one accepted LAD-code column, ``None`` when absent, refused when two.""" + + present = [ + upper_to_column[name] for name in LAD23_CODE_COLUMNS if name in upper_to_column + ] + if len(present) > 1: + raise ValueError( + "E/W LAD lookup carries more than one LAD code column: " + f"{[str(column) for column in present]}." + ) + return present[0] if present else None + + def _normalise_ew_lad_lookup(frame: pd.DataFrame) -> pd.DataFrame: + upper_to_column = {str(column).strip().upper(): column for column in frame.columns} + lad_column = _lad23_code_column(upper_to_column) column_map = {} for column in frame.columns: - upper = str(column).upper() + upper = str(column).strip().upper() if upper == "OA21CD": column_map[column] = "oa_code" - elif upper.startswith("LAD") and upper.endswith("CD"): + elif lad_column is not None and column == lad_column: column_map[column] = "lad23_code" lookup = frame.rename(columns=column_map) missing = sorted({"oa_code", "lad23_code"} - set(lookup.columns)) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_build.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_build.py index 5795f20bd..55faff2ed 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_build.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_build.py @@ -354,7 +354,14 @@ def token(dtype): params={ "spine_gate_digests": canonical_json( calibration_run.uk_spine_checkpoint_gate_digests() - ).decode() + ).decode(), + # The checkpoint keeps the H5's own period: the CREATE + # normaliser would otherwise default it to the FRS + # vintage's, and a bound frame from another period (the + # seam fixtures, a historical checkpoint) would be + # re-dated on admission (found by the national line's + # projection fence, which declares the frame's period). + "time_period": str(frame.metadata["time_period"]), }, outputs=outputs, artifact_outputs=( diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_calibration.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_calibration.py index dccfc00ee..6d2c3bc56 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_calibration.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_calibration.py @@ -340,6 +340,33 @@ def __init__(self, *, progress_callback=None): self.progress_callback = progress_callback +#: The doctrine fields the dense node does not pass to the solver, with the +#: one value it solves under: free mass, the default target-loss scales and +#: no L0 penalty. A problem that binds another value would record a doctrine +#: the solve did not honour, so the node refuses it instead. +UK_DENSE_SOLVE_FIXED_DOCTRINE: Mapping[str, object] = { + "mass_rule": "free", + "scale_rule": "default_target_loss_scales", + "l0_lambda": 0.0, +} + + +def refuse_unhonoured_solve_doctrine(binding: Mapping[str, object]) -> None: + """Refuse a problem whose bound doctrine the dense solve cannot honour.""" + + for key, honoured in UK_DENSE_SOLVE_FIXED_DOCTRINE.items(): + if key not in binding: + continue + bound = binding[key] + if key == "l0_lambda": + bound = float(bound) + if bound != honoured: + raise ValueError( + f"UK dense solve cannot honour {key}={binding[key]!r}: it solves " + f"under {honoured!r}." + ) + + class UKDenseSolveKernel(_ObservedSolveKernel): ref = "uk.full.dense@1" capabilities = Capabilities( @@ -348,9 +375,34 @@ class UKDenseSolveKernel(_ObservedSolveKernel): dependencies=_DEPENDENCIES, ) - def run(self, context): + @staticmethod + def _admit(context) -> None: + """The solve's admission, declared by the node. + + The dense line (the default, ``admission`` absent) solves only after + its source preflight battery permitted the artifact. The national + line (``admission="bound_spine"``) runs no pre-solve battery: it + solves the bound spine checkpoint, whose provenance artifact the + bound-spine node emits only after the sidecar, content identity and + gate report authenticated (microcosm#823 through the graph). + """ from .graph_terminal import decode_full_gate_report + admission = str(context.params.get("admission", "preflight")) + if admission == "bound_spine": + if "spine_provenance" not in context.artifacts: + raise ValueError( + "National calibration requires the bound spine's provenance artifact." + ) + provenance = json.loads(context.artifacts["spine_provenance"].payload) + binding = provenance.get("spine_gate_report") + if not isinstance(binding, Mapping) or not binding.get("sha256"): + raise ValueError( + "National calibration requires a spine provenance bound to its gate report." + ) + return + if admission != "preflight": + raise ValueError(f"Unknown dense calibration admission {admission!r}.") if "preflight" not in context.artifacts: raise ValueError( "Dense calibration requires its source preflight artifact." @@ -362,6 +414,9 @@ def run(self, context): raise ValueError("Dense calibration requires a preflight phase report.") if not classification["artifact_permitted"]: raise ValueError("Dense calibration refused by the source preflight.") + + def run(self, context): + self._admit(context) frame, problem = _inputs(context) if "imported_dense" in context.artifacts: result = decode_calibration_result( @@ -386,6 +441,7 @@ def run(self, context): } if not required <= set(binding): raise ValueError("UK ordered problem is missing its solve doctrine.") + refuse_unhonoured_solve_doctrine(binding) result = calibrate( frame, problem.to_target_set(), diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_national.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_national.py new file mode 100644 index 000000000..3e6041cfc --- /dev/null +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_national.py @@ -0,0 +1,1255 @@ +"""The UK national release role on the shared executable graph. + +The certified national line (microcosm#823) used to run through the +calibration seam library, an in-process pipeline the graph driver dispatched +to before preparing any graph. This module composes the same computation as +graph nodes over the bound spine checkpoint, so the national and the dense +line share one executor, one content store and one evidence discipline: + +* ``uk.full.national_targets`` compiles the national register from the + pinned Chronicle artifact (measure exclusions, band edges, the optional + frozen scoring register); no ladder and no local surface. +* ``uk.full.national_problem`` resolves the engine measures on the bound + frame, materialises the register and encodes one ordered problem whose + bindings carry the national doctrine's loss weights, mass reason and + bounds. +* ``uk.full.dense`` / ``uk.full.calibrated`` are the existing calibration + nodes; the dense node's admission is the bound spine's provenance + artifact rather than a source preflight battery, because the national + line runs no pre-solve battery (its source and reference gates belong to + the release-cut certification producer). +* ``uk.full.gates.calibrated`` evaluates the calibration-seam gate scope on + the calibrated population and stores the calibration manifest block and + the per-target diagnostic rows beside the phase report. + +The driver (:mod:`.full_build_cli`) materialises the seam-shaped files from +those artifacts: the diagnostics through the seam writer, the battery +replayed from the stored phase report and signed, the H5, the build record, +the registries and the manifest (:mod:`.national_role`). +""" + +from __future__ import annotations + +import hashlib +import json +import sys +import tempfile +from collections.abc import Mapping +from dataclasses import asdict, dataclass, replace +from datetime import date +from pathlib import Path +from types import SimpleNamespace +from typing import Any + +import numpy as np +import pandas as pd + +from microcosm.build.country_spec import load_country_spec +from microcosm.build.gate_battery import ( + EvidenceContext, + _gates_manifest_payload, + _json_safe, + evaluate_phase, + gate_phase_report_payload, +) +from microcosm.build.target_materialization import assert_calibration_input_finite +from microcosm.calibrate import TargetRegistry, build_constraint_matrix +from microcosm.calibrate.artifacts import ( + PROBLEM_TYPE, + RESULT_TYPE, + SOLUTION_TYPE, + decode_calibration_result, + decode_problem, + decode_solution, + encode_problem, +) +from microcosm.frame import Frame, WeightKind +from microcosm.graph import ( + ArtifactInput, + ArtifactOutput, + ArtifactType, + Capabilities, + Determinism, + Graph, + KernelBase, + KernelContext, + KernelRegistry, + KernelResult, + KernelRole, + Node, + SourceRef, + compile_graph, + source_hash, +) +from microcosm.graph.canonical import canonical_json + +from . import ( + calibration_run, + full_measure, + full_targets, + ledger_targets, + national_calibration, + national_doctrine, +) +from .battery_bindings import UK_GATE_REGISTRY +from .calibration_run import ( + UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS, + uk_scoped_gate_manifest, +) +from .cgt_projection import UK_CGT_PROJECTION_ARTIFACT_KEY +from .content_identity import uk_frame_content_identity +from .diagnostics import uk_target_geography_levels +from .full_targets import CHRONICLE_SOURCE_CODEC, load_uk_national_target_inputs +from .graph_build import SPINE_PROVENANCE_TYPE +from .graph_calibration import ( + UKCalibrationNodes, + UKGraphCalibrationConfig, + uk_calibration_nodes, +) +from .graph_population import context_frame, population_columns, population_slices +from .graph_targets import ( + register_uk_source_codecs, + registry_from_payload, + registry_payload, + target_geography, +) +from .local_rowwise import UKRowwiseNationalRows +from .measure_simulation import UKMeasureResolver +from .national_calibration import ( + CalibrationFrameAdapter, + national_calibration_mass_reason, +) +from .national_doctrine import UKNationalSolveDoctrine, uk_national_target_loss_weights +from .national_frame import load_uk_national_frame +from .rowwise_posture import UK_ROWWISE_NATIONAL_POSTURE, UKRowwisePosture + +NATIONAL_TARGET_TYPE = ArtifactType("microcosm.uk.national-target-registry", 1) +NATIONAL_GATE_REPORT_TYPE = ArtifactType("microcosm.uk.national-gate-report", 1) +NATIONAL_EVIDENCE_TYPE = ArtifactType("microcosm.uk.national-calibration-evidence", 1) +NATIONAL_READBACK_TYPE = ArtifactType("microcosm.uk.national-export-readback", 1) +NATIONAL_MATERIALIZATION = "uk_national_measure" + +#: The doctrine fields every national evidence block records, in the seam's +#: order (``calibration_run._doctrine_payload``); the problem node binds the +#: solve-shaping ones so the ordered problem carries its own doctrine. +_DOCTRINE_FIELDS = ( + "epochs", + "learning_rate", + "max_weight_ratio", + "seed", + "target_loss_cap", + "scale_rule", + "target_weight_rule", + "mass_rule", + "l0_lambda", +) + + +def national_doctrine_payload(doctrine: UKNationalSolveDoctrine) -> dict[str, object]: + """The seam's nine-field doctrine block.""" + + return {field: getattr(doctrine, field) for field in _DOCTRINE_FIELDS} + + +def national_doctrine_from_payload( + payload: Mapping[str, object], +) -> UKNationalSolveDoctrine: + return UKNationalSolveDoctrine( + **{field: payload[field] for field in _DOCTRINE_FIELDS} + ) + + +# --------------------------------------------------------------------------- +# Configuration and composition +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class UKNationalBuildConfig: + """The national posture's declared build: one doctrine, one register.""" + + calibration_year: int + time_period: str + source_year: int + doctrine: UKNationalSolveDoctrine + doctrine_overrides: Mapping[str, Mapping[str, object]] + allow_unpinned_feed: bool = False + + def __post_init__(self) -> None: + if type(self.calibration_year) is not int or self.calibration_year <= 0: + raise ValueError("calibration_year must be a positive integer.") + if not str(self.time_period).strip(): + raise ValueError("time_period must be non-empty.") + if type(self.source_year) is not int or self.source_year <= 0: + raise ValueError("source_year must be a positive integer.") + if not isinstance(self.doctrine, UKNationalSolveDoctrine): + raise TypeError("doctrine must be a UKNationalSolveDoctrine.") + object.__setattr__( + self, + "doctrine_overrides", + {str(k): dict(v) for k, v in dict(self.doctrine_overrides).items()}, + ) + + @property + def calibration(self) -> UKGraphCalibrationConfig: + """The solve settings the shared calibration nodes bind.""" + + return UKGraphCalibrationConfig( + epochs=self.doctrine.epochs, + learning_rate=self.doctrine.learning_rate, + seed=self.doctrine.seed, + target_weight_rule=self.doctrine.target_weight_rule, + ) + + def payload(self) -> dict[str, object]: + return { + **asdict(self), + "doctrine": national_doctrine_payload(self.doctrine), + "doctrine_overrides": dict(self.doctrine_overrides), + } + + +@dataclass(frozen=True) +class UKNationalGraph: + graph: Graph + calibration: UKCalibrationNodes + config: UKNationalBuildConfig + posture: UKRowwisePosture + + @property + def population(self) -> str: + return self.calibration.population + + def operation_inventory(self) -> dict: + compiled = compile_graph(self.graph) + return { + "schema": "microcosm.uk.national-build-operations.v1", + "release_role": self.posture.role, + "configuration": self.config.payload(), + "nodes": [ + { + "id": node_id, + "kernel": self.graph.node(node_id).kernel, + "description": self.graph.node(node_id).description, + "population": compiled.versions[node_id], + "dependencies": list(compiled.predecessors[node_id]), + "artifacts": [ + o.name for o in self.graph.node(node_id).artifact_outputs + ], + } + for node_id in compiled.order + ], + } + + +NATIONAL_TARGETS_NODE = "uk.full.national_targets" +NATIONAL_PROBLEM_NODE = "uk.full.national_problem" +NATIONAL_GATES_NODE = "uk.full.gates.calibrated" +NATIONAL_READBACK_NODE = "uk.full.national.readback" + + +def uk_national_gate_manifest(posture: UKRowwisePosture = UK_ROWWISE_NATIONAL_POSTURE): + """The calibration-seam scope, filtered from the declared spec.""" + + return uk_scoped_gate_manifest( + tuple(posture.gate_scope), + phases=("terminal",), + policy_suffix=posture.gate_policy_suffix, + ) + + +def uk_national_graph( + config: UKNationalBuildConfig, + *, + spine: Graph, + spine_population: str, + optional_target_sources: tuple[str, ...] = (), + review_date: str | None = None, + posture: UKRowwisePosture = UK_ROWWISE_NATIONAL_POSTURE, + ledger_pins: Mapping[str, str | None] | None = None, +) -> UKNationalGraph: + """Append the national line to a bound spine checkpoint graph. + + ``spine`` must end in a node that emits the bound spine's provenance + artifact (:class:`~.graph_build.UKBoundSpineKernel`): the national line + solves the checkpoint as bound, never a sampled or cloned pool. + """ + + if posture.role != "national": + raise ValueError("uk_national_graph composes the national posture only.") + holder = spine.node(spine_population) + if not any( + output.name == "spine_provenance" and output.type == SPINE_PROVENANCE_TYPE + for output in holder.artifact_outputs + ): + raise ValueError( + "The national line requires a bound spine checkpoint population " + "that emits its spine_provenance artifact." + ) + review_date = date.today().isoformat() if review_date is None else review_date + date.fromisoformat(review_date) + cells = population_columns(spine, spine_population) + slices = population_slices(cells) + contract_identity = load_country_spec("uk").fingerprint + compile_sources = ("uk_ledger_facts", *optional_target_sources) + registry_input = ArtifactInput( + "registry", NATIONAL_TARGETS_NODE, "registry", NATIONAL_TARGET_TYPE + ) + provenance_input = ArtifactInput( + "spine_provenance", spine_population, "spine_provenance", SPINE_PROVENANCE_TYPE + ) + doctrine = config.doctrine + nodes: list[Node] = [ + Node( + NATIONAL_TARGETS_NODE, + UKNationalTargetKernel.ref, + population=spine_population, + sources=compile_sources, + params={ + "calibration_year": int(config.calibration_year), + "review_date": review_date, + "country_contract_sha256": contract_identity, + "allow_unpinned_feed": bool(config.allow_unpinned_feed), + # The request's Ledger pins: the artifact must measure them, + # as the seam's loader required (the source identity binds + # the bytes; the pins bind what the operator asked for). + "ledger_facts_sha256": (ledger_pins or {}).get("facts_sha256"), + "ledger_manifest_sha256": (ledger_pins or {}).get("manifest_sha256"), + }, + artifact_outputs=(ArtifactOutput("registry", NATIONAL_TARGET_TYPE),), + description="Compile the pinned national register, its exclusions and band edges.", + ), + Node( + NATIONAL_PROBLEM_NODE, + UKNationalProblemKernel.ref, + population=spine_population, + inputs=slices, + params={ + "time_period": str(config.time_period), + "target_weight_rule": doctrine.target_weight_rule, + "target_loss_cap": float(doctrine.target_loss_cap), + "max_weight_ratio": doctrine.max_weight_ratio, + "l0_lambda": float(doctrine.l0_lambda), + "mass_rule": doctrine.mass_rule, + "scale_rule": doctrine.scale_rule, + }, + artifact_inputs=(registry_input,), + artifact_outputs=(ArtifactOutput("problem", PROBLEM_TYPE),), + description="Resolve engine measures on the bound spine and build the one ordered national problem.", + ), + ] + calibration = uk_calibration_nodes( + base=spine_population, + columns=cells, + problem_producer=NATIONAL_PROBLEM_NODE, + config=config.calibration, + ) + for node in calibration.nodes: + if node.id == calibration.dense_producer: + node = replace( + node, + params={**node.params, "admission": "bound_spine"}, + artifact_inputs=(*node.artifact_inputs, provenance_input), + description="Solve the national doctrine on the bound spine checkpoint.", + ) + nodes.append(node) + gates = uk_national_gate_manifest(posture) + nodes.append( + Node( + NATIONAL_GATES_NODE, + UKNationalGateKernel.ref, + population=calibration.population, + inputs=population_slices(population_columns(spine, spine_population)), + params={ + "time_period": str(config.time_period), + "review_date": review_date, + "gate_posture": posture.gate_posture, + "gate_policy_suffix": posture.gate_policy_suffix, + # The scope the kernel evaluates is the posture's, read from + # the node; the manifest digest below pins its compilation. + "gate_scope": canonical_json(list(posture.gate_scope)).decode(), + "gate_manifest": canonical_json( + _gates_manifest_payload(gates) + ).decode(), + "doctrine": canonical_json( + national_doctrine_payload(doctrine) + ).decode(), + }, + artifact_inputs=( + registry_input, + ArtifactInput( + "problem", NATIONAL_PROBLEM_NODE, "problem", PROBLEM_TYPE + ), + ArtifactInput( + "result", calibration.result_producer, "result", RESULT_TYPE + ), + ArtifactInput( + "solution", calibration.solution_producer, "solution", SOLUTION_TYPE + ), + ), + artifact_outputs=( + ArtifactOutput("gate_report", NATIONAL_GATE_REPORT_TYPE), + ArtifactOutput("calibration_evidence", NATIONAL_EVIDENCE_TYPE), + ), + description="Evaluate the calibration-seam gate scope on the calibrated national population.", + ) + ) + existing = {source.name for source in spine.sources} + sources = tuple( + SourceRef( + name, + CHRONICLE_SOURCE_CODEC if name == "uk_ledger_facts" else "raw-bytes-v1", + ) + for name in compile_sources + if name not in existing + ) + graph = replace( + spine, nodes=(*spine.nodes, *nodes), sources=(*spine.sources, *sources) + ) + compile_graph(graph) + return UKNationalGraph(graph, calibration, config, posture) + + +def add_uk_national_readback( + graph: Graph, *, population: str, dataset_source: str = "exported_dataset" +) -> Graph: + """Continue the graph over the written H5: a readback gate on its content.""" + + node = Node( + NATIONAL_READBACK_NODE, + UKNationalReadbackKernel.ref, + population=population, + inputs=population_slices(population_columns(graph, population)), + sources=(dataset_source,), + params={"dataset_source": dataset_source}, + artifact_outputs=(ArtifactOutput("readback", NATIONAL_READBACK_TYPE),), + description="Read the written national H5 back and compare its content identity with the calibrated population.", + ) + return replace( + graph, + nodes=(*graph.nodes, node), + sources=( + *graph.sources, + SourceRef( + dataset_source, + "uk-spine-h5-v1", + "Materialized UK national H5; graph-owned readback.", + ), + ), + ) + + +# --------------------------------------------------------------------------- +# Kernels +# --------------------------------------------------------------------------- + + +class _NationalKernel(KernelBase): + capabilities = Capabilities( + Determinism.DETERMINISTIC, dependencies=("policyengine-uk",) + ) + + def implementation_hash(self) -> str: + implementation = source_hash( + type(self), + sys.modules[__name__], + full_measure, + full_targets, + ledger_targets, + national_calibration, + national_doctrine, + ) + return hashlib.sha256( + canonical_json( + { + "implementation": implementation, + "country_spec": load_country_spec("uk").fingerprint, + } + ) + ).hexdigest() + + +class UKNationalTargetKernel(_NationalKernel): + ref = "uk.full.national_targets@1" + + def run(self, context: KernelContext) -> KernelResult: + inputs = load_uk_national_target_inputs( + context.sources["uk_ledger_facts"], + measure_exclusions=context.sources.get("uk_measure_exclusions"), + register_json=context.sources.get("uk_frozen_register"), + calibration_year=int(context.params["calibration_year"]), + exclusions_evaluated_on=date.fromisoformat( + str(context.params["review_date"]) + ), + allow_unpinned_feed=bool(context.params["allow_unpinned_feed"]), + expected_facts_sha256=context.params.get("ledger_facts_sha256"), + expected_manifest_sha256=context.params.get("ledger_manifest_sha256"), + ) + payload = { + "schema_version": 1, + "kind": "uk_national_target_registry", + "calibration_year": int(inputs["calibration_year"]), + "national_registry": registry_payload(inputs["national_registry"]), + "band_edge_registry": registry_payload(inputs["band_edge_registry"]), + "measure_exclusions": inputs["measure_exclusions"], + "register_completeness": inputs["register_completeness"], + "ledger_provenance": inputs["ledger_provenance"], + "chronicle_feed_pin": inputs["chronicle_feed_pin"], + "chronicle_provenance": inputs["chronicle_provenance"], + "allow_unpinned_feed": bool(context.params["allow_unpinned_feed"]), + } + return KernelResult(artifacts={"registry": canonical_json(payload)}) + + +def _registry_artifact(context: KernelContext) -> dict[str, Any]: + payload = json.loads(context.artifacts["registry"].payload) + if ( + payload.get("kind") != "uk_national_target_registry" + or payload.get("schema_version") != 1 + ): + raise ValueError("Unsupported UK national target registry artifact.") + return payload + + +def materialize_uk_national_rows( + frame: Frame, + registry: TargetRegistry, + *, + period: int, + band_edge_registry: TargetRegistry, + resolver_factory=UKMeasureResolver, + scratch_dir: Path | None = None, +) -> tuple[Frame, Any, UKRowwiseNationalRows, dict[str, Any]]: + """Resolve, materialise and return the national rows over a prepared frame. + + With a resolver factory the engine route of + :func:`~.full_measure.resolve_uk_full_measures` runs (one block, no local + grains). Without one (``None``) the register materialises from the + frame's own columns, the seam's ``measure_resolver=None`` route: the + hermetic tests' path, and a data-only environment's. + """ + + if resolver_factory is None: + adapter = CalibrationFrameAdapter(frame) + materialized = ledger_targets.materialize_uk_ledger_targets( + adapter, registry, period=period, band_edge_registry=band_edge_registry + ) + if materialized.skipped: + raise RuntimeError( + "UK national calibration could not materialize every activated " + f"target reference: skipped={[s.__dict__ for s in materialized.skipped]}." + ) + rows = UKRowwiseNationalRows( + targets=registry.to_target_set(), + registry=registry, + families=tuple(sorted({spec.family for spec in registry.specs})), + ) + evidence = { + "mode": "frame_only", + "national_inputs": 0, + "target_materialization": materialized.report(), + } + return adapter.prepared_frame(), adapter.restore, rows, evidence + if scratch_dir is None: + raise ValueError("engine measure resolution needs a scratch directory.") + prepared, restore, rows, _metrics, evidence = full_measure.resolve_uk_full_measures( + frame, + registry, + period=period, + scratch_dir=scratch_dir, + band_edge_registry=band_edge_registry, + resolver_factory=resolver_factory, + blocks=1, + local_grains=(), + ) + return prepared, restore, rows, dict(evidence) + + +class UKNationalProblemKernel(_NationalKernel): + """One ordered national problem with the doctrine bound into it. + + ``resolver_factory`` is instance state like the gate kernels' coverage + engine: the engine identity the run stands on is provenance recorded by + the driver, and the dependency declaration on the capabilities binds the + installed engine version into the node key. + """ + + ref = "uk.full.national_problem@1" + + def __init__(self, *, resolver_factory=UKMeasureResolver) -> None: + self.resolver_factory = resolver_factory + + def run(self, context: KernelContext) -> KernelResult: + frame = context_frame(context) + registry_document = _registry_artifact(context) + with tempfile.TemporaryDirectory( + prefix="microcosm-uk-national-measures-" + ) as scratch: + payload = encode_uk_national_problem( + frame, + national_registry=registry_from_payload( + registry_document["national_registry"] + ), + band_edge_registry=registry_from_payload( + registry_document["band_edge_registry"] + ), + period=int(registry_document["calibration_year"]), + measure_exclusions=registry_document["measure_exclusions"], + doctrine={ + "target_weight_rule": str(context.params["target_weight_rule"]), + "target_loss_cap": float(context.params["target_loss_cap"]), + "max_weight_ratio": context.params["max_weight_ratio"], + "l0_lambda": float(context.params["l0_lambda"]), + "mass_rule": str(context.params["mass_rule"]), + "scale_rule": str(context.params["scale_rule"]), + }, + resolver_factory=self.resolver_factory, + scratch_dir=Path(scratch), + ) + return KernelResult(artifacts={"problem": payload}) + + +def encode_uk_national_problem( + frame: Frame, + *, + national_registry: TargetRegistry, + band_edge_registry: TargetRegistry, + period: int, + doctrine: Mapping[str, object], + measure_exclusions: Mapping[str, Mapping[str, str]] | None = None, + resolver_factory=UKMeasureResolver, + scratch_dir: Path | None = None, +) -> bytes: + """One ordered national problem, the doctrine bound into it. + + The seam stage's route up to the solve: refuse non-finite inputs, + resolve the engine measures (or materialise from the frame's own + columns with ``resolver_factory=None``), materialise the register on + the adapter, compile the constraint matrix row for row, restore the + pristine tables, and encode the problem with the national doctrine's + ``family_equal`` loss weights, mass reason and bounds in its bindings, + which the shared dense solve node reads. + """ + + assert_calibration_input_finite(frame) + if not len(national_registry.specs): + raise ValueError("The national register selects no target.") + prepared, restore, rows, evidence = materialize_uk_national_rows( + frame, + national_registry, + period=period, + band_edge_registry=band_edge_registry, + resolver_factory=resolver_factory, + scratch_dir=scratch_dir, + ) + problem = build_constraint_matrix(prepared, rows.targets, "household") + if problem.skipped: + failures = "; ".join( + f"{item.target.key}: {item.reason}" for item in problem.skipped + ) + raise ValueError("National constraints failed to compile: " + failures) + clean = restore(prepared) + for entity in frame.entities: + pd.testing.assert_frame_equal(clean.table(entity), frame.table(entity)) + if len(problem.targets) != len(national_registry.specs): + raise ValueError( + "UK national calibration matrix did not contain every activated " + f"reference: declared={len(national_registry.specs)}, " + f"rows={len(problem.targets)}." + ) + families = [spec.family for spec in national_registry.specs] + rule = str(doctrine["target_weight_rule"]) + target_loss_weights = uk_national_target_loss_weights(families, rule=rule) + mass_reason = national_calibration_mass_reason(families) + ids = frame.table("household")["household_id"].tolist() + levels = national_target_geography_levels(national_registry) + target_metadata = [ + { + **spec.metadata, + "family": spec.family, + "source": spec.source, + "geography_level": levels[spec.to_target().row_name], + "materialization": NATIONAL_MATERIALIZATION, + } + for spec in national_registry.specs + ] + return encode_problem( + problem, + entity_ids=ids, + target_metadata=target_metadata, + bindings={ + "release_role": "national", + "mass_reason": mass_reason, + "mass_rule": str(doctrine["mass_rule"]), + "max_weight_ratio": doctrine["max_weight_ratio"], + "target_loss_cap": float(doctrine["target_loss_cap"]), + "target_loss_weights": ( + np.ones(problem.n_targets, dtype=np.float64) + if target_loss_weights is None + else target_loss_weights + ).tolist(), + "target_weight_rule": rule, + "scale_rule": str(doctrine["scale_rule"]), + "l0_lambda": float(doctrine["l0_lambda"]), + "bound_families": [ + f"national/{family}" for family in sorted(set(families)) + ], + "measure_resolution": _json_safe(evidence), + "measure_exclusions": dict(measure_exclusions or {}), + "register_sha256": national_registry.version, + "band_edge_register_sha256": band_edge_registry.version, + "calibration_year": int(period), + "activated_reference_count": len(national_registry.specs), + "resolved_reference_count": len(national_registry.specs), + }, + ) + + +def _axis(frame: Frame) -> list: + return frame.table("household")["household_id"].tolist() + + +def national_target_geography_levels(registry: TargetRegistry) -> dict[str, str]: + """Each national row's geography level, by row name. + + A compiled national spec that declares its level (``geography_level`` or + ``ledger_geography_level``, as the Chronicle compiler writes) resolves + through the shared rule; one that declares none, as the fixture registers + do, resolves through the population-target contract, the seam + diagnostics' rule (:func:`~.diagnostics.uk_target_geography_levels`). + """ + + declared = { + spec.to_target().row_name: target_geography(spec) + for spec in registry.specs + if spec.metadata.get("geography_level") is not None + or spec.metadata.get("ledger_geography_level") is not None + } + if len(declared) == len(registry.specs): + return declared + contract = uk_target_geography_levels( + TargetRegistry( + [ + spec + for spec in registry.specs + if spec.to_target().row_name not in declared + ], + country="uk", + ) + ) + return {**contract, **declared} + + +def national_calibration_manifest( + result, + frame: Frame, + *, + problem, + doctrine: Mapping[str, object], +) -> dict[str, object]: + """The seam's ``calibration`` evidence block, from the graph's artifacts. + + ``result`` is the decoded dense result rebound to the calibrated + population ``frame``; ``problem`` its ordered problem, whose bindings + carry the materialisation evidence and the activation counts. + """ + + from microcosm.calibrate import effective_sample_size + + bindings = problem.bindings + weights = frame.weights_for("household") + if weights.kind is not WeightKind.CALIBRATED: + raise RuntimeError( + "UK national calibration returned household weights whose kind is " + f"{weights.kind.value!r}, not 'calibrated'." + ) + if not frame.mass_log: + raise RuntimeError("UK national calibration must append one mass record.") + record = frame.mass_log[-1] + if record.entity != "household" or "calibration" not in record.reason: + raise RuntimeError( + "UK national calibration latest mass record is not the calibration " + f"record: entity={record.entity!r}, reason={record.reason!r}." + ) + before_count = len(frame.mass_log) - 1 + ratios = np.asarray(result.weights, dtype=np.float64) / np.asarray( + result.initial_weights, dtype=np.float64 + ) + old_total = float(record.old_total) + new_total = float(record.new_total) + before_kind = problem.problem.initial_weights.kind + measure_resolution = dict(bindings.get("measure_resolution", {})) + materialization = measure_resolution.pop("target_materialization", None) + # The seam manifest carried the resolution loop's own receipt (attached + # routes, rounds, the provider's receipt); the wider engine receipt stays + # in the problem bindings. + resolutions = measure_resolution.get("resolution") + if isinstance(resolutions, list) and len(resolutions) == 1: + seam_block = dict(resolutions[0]) + else: + seam_block = measure_resolution + manifest: dict[str, object] = { + "activated_reference_count": int(bindings["activated_reference_count"]), + "resolved_reference_count": int(bindings["resolved_reference_count"]), + "matrix_target_count": len(result.problem.names), + "loss": float(result.final_loss), + "effective_sample_size": effective_sample_size(result.weights), + "max_weight_ratio": float(ratios.max()), + "max_weight_ratio_bound": doctrine["max_weight_ratio"], + "target_materialization": materialization, + "weights": { + "household_weight_kind": weights.kind.value, + "household_weight_kind_chain": [ + {"stage": "staging", "kind": before_kind.value}, + {"stage": "national_calibration", "kind": weights.kind.value}, + ], + "mass_log_records_before_calibration": before_count, + "mass_log_records": len(frame.mass_log), + "calibration_mass_change": { + "entity": str(record.entity), + "old_total": old_total, + "new_total": new_total, + "relative_shift": (new_total - old_total) / old_total, + "declared_factor": record.declared_factor, + "reason": str(record.reason), + }, + }, + "solve": { + "n_targets": len(result.problem.names), + "n_households": len(frame.table("household")), + "initial_loss": float(result.initial_loss), + "final_loss": float(result.final_loss), + "n_nonzero": int(np.count_nonzero(result.weights)), + }, + "parameters": {"doctrine": dict(doctrine)}, + } + if measure_resolution.get("mode") != "frame_only": + manifest["measure_resolution"] = seam_block + return manifest + + +def decode_national_result(context: KernelContext, frame: Frame): + """The dense result and its ordered problem, rebound to ``frame``.""" + + problem = decode_problem(context.artifacts["problem"].payload) + solution = decode_solution( + context.artifacts["solution"].payload, + problem_sha256=problem.sha256, + entity_ids=_axis(frame), + ) + initial_frame = Frame( + {entity: frame.table(entity) for entity in frame.entities}, + frame.schema, + {"household": problem.problem.initial_weights}, + frame.strata, + metadata=frame.metadata, + ) + result = decode_calibration_result( + context.artifacts["result"].payload, frame=initial_frame, problem=problem + ) + if not np.array_equal(result.weights, solution.weights): + raise ValueError( + "Final diagnostics result differs from the installed solution." + ) + if not np.array_equal(frame.weights_for("household").values, solution.weights): + raise ValueError("Gate population does not carry the bound solution weights.") + return replace(result, frame=frame), problem + + +class UKNationalGateKernel(_NationalKernel): + ref = "uk.full.national-gates@1" + capabilities = Capabilities( + Determinism.DETERMINISTIC, + role=KernelRole.GATE, + dependencies=("policyengine-uk",), + ) + + def implementation_hash(self) -> str: + from .. import gate_battery + from . import battery_bindings, diagnostics, weighted_integrity + + return hashlib.sha256( + canonical_json( + { + "code": source_hash( + type(self), + sys.modules[__name__], + gate_battery, + battery_bindings, + calibration_run, + weighted_integrity, + diagnostics, + ), + "country_resources": load_country_spec("uk").fingerprint, + } + ) + ).hexdigest() + + def run(self, context: KernelContext) -> KernelResult: + frame = context_frame(context) + registry_document = _registry_artifact(context) + national = registry_from_payload(registry_document["national_registry"]) + result, problem = decode_national_result(context, frame) + doctrine = json.loads(str(context.params["doctrine"])) + scope = tuple( + str(gate) for gate in json.loads(str(context.params["gate_scope"])) + ) + gates = uk_scoped_gate_manifest( + scope, + phases=("terminal",), + policy_suffix=str(context.params["gate_policy_suffix"]), + ) + if ( + canonical_json(_gates_manifest_payload(gates)).decode() + != context.params["gate_manifest"] + ): + raise ValueError( + "UK national gate manifest differs from its declared binding." + ) + manifest_block = national_calibration_manifest( + result, frame, problem=problem, doctrine=doctrine + ) + diagnostics_rows = [ + { + "name": row.name, + "estimate": row.final_estimate, + "target": row.target, + "relative_error": row.relative_error, + } + for row in result.diagnostics + ] + admin_totals, admin_receipt = calibration_run.uk_aggregate_admin_totals( + frame, gates + ) + projection = calibration_run.uk_cgt_projection_artifact(frame, gates) + artifacts = { + "national_calibration": manifest_block, + "parity_evidence": SimpleNamespace( + target_relative_errors={ + str(row["name"]): float(row["relative_error"]) + for row in diagnostics_rows + } + ), + "aggregate_admin": admin_totals, + UK_CGT_PROJECTION_ARTIFACT_KEY: projection, + "exclusions_evaluated_on": date.fromisoformat( + str(context.params["review_date"]) + ), + } + report = evaluate_phase( + gates, + "terminal", + EvidenceContext(frame=frame, artifacts=artifacts), + registry=UK_GATE_REGISTRY, + ) + blocking = report.blocking_outcomes(release_candidate=False) + payload = { + "schema_version": 1, + "kind": "uk_national_gate_report", + "posture": str(context.params["gate_posture"]), + "policy_suffix": str(context.params["gate_policy_suffix"]), + "scope": list(scope), + "scope_exclusions": dict(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS), + "report": gate_phase_report_payload(report, gates=gates), + "aggregate_admin_measurement": _json_safe(admin_receipt), + "blocking": [outcome.entry.id for outcome in blocking], + "artifact_permitted": not blocking, + "artifacts": {name: value.key for name, value in context.artifacts.items()}, + } + evidence = { + "schema_version": 1, + "kind": "uk_national_calibration_evidence", + "calibration": _json_safe(manifest_block), + "diagnostics": _json_safe(diagnostics_rows), + "target_geography_levels": uk_target_geography_levels(national), + "register": calibration_run._register_census( + national, registry_document["measure_exclusions"] + ), + } + return KernelResult( + artifacts={ + "gate_report": canonical_json(_json_safe(payload)), + "calibration_evidence": canonical_json(evidence), + }, + receipt={"outcome": "pass" if not blocking else "fail"}, + ) + + +class UKNationalReadbackKernel(KernelBase): + ref = "uk.full.national-readback@1" + capabilities = Capabilities(Determinism.DETERMINISTIC, role=KernelRole.GATE) + + def implementation_hash(self) -> str: + from . import content_identity, national_frame + + return source_hash(type(self), content_identity, national_frame) + + def run(self, context: KernelContext) -> KernelResult: + from ..artifact_files import file_artifact + + frame = context_frame(context) + path = Path(context.sources[str(context.params["dataset_source"])]) + dataset = file_artifact(path) + written, _provenance = load_uk_national_frame(path) + failures = [] + expected = uk_frame_content_identity(frame) + actual = uk_frame_content_identity(written) + if actual != expected: + failures.append( + "Written national H5 content identity differs from the calibrated population." + ) + if written.weights_for("household").kind is not WeightKind.CALIBRATED: + failures.append("Written national H5 household weights are not calibrated.") + if written.mass_log != frame.mass_log: + failures.append( + "Written national H5 mass log differs from the calibrated population." + ) + if file_artifact(path) != dataset: + raise ValueError("UK national H5 changed during graph readback validation.") + report = { + "schema_version": 1, + "kind": "uk_national_export_readback", + "passed": not failures, + "failures": failures, + "dataset": dataset, + "content_identity": actual, + "expected_content_identity": expected, + } + return KernelResult( + artifacts={"readback": canonical_json(report)}, + receipt={"outcome": "pass" if not failures else "fail"}, + ) + + +#: "The module's engine resolver at registration time": the default the +#: driver registers with, resolved when the registry is built so a hermetic +#: suite that stands the resolver in (or out, ``None``) on this module is +#: honoured, as the seam's tests stood theirs in on the role module. +ENGINE_RESOLVER = object() + + +def register_uk_national_kernels( + registry: KernelRegistry, *, resolver_factory=ENGINE_RESOLVER +) -> KernelRegistry: + """Register the national kernels beside the shared ones the graph binds.""" + + if resolver_factory is ENGINE_RESOLVER: + resolver_factory = UKMeasureResolver + register_uk_source_codecs() + for kernel in ( + UKNationalTargetKernel(), + UKNationalProblemKernel(resolver_factory=resolver_factory), + UKNationalGateKernel(), + UKNationalReadbackKernel(), + ): + try: + registry.get(kernel.ref) + except KeyError: + registry.register(kernel) + return registry + + +# --------------------------------------------------------------------------- +# Driver-side projections: the seam-shaped files from the stored artifacts +# --------------------------------------------------------------------------- + + +def graph_payload(manifest, store, node: str, artifact: str) -> bytes: + return store.load_bytes(manifest.nodes[node].opaque_artifacts[artifact]) + + +def national_result_from_manifest( + manifest, store, *, frame: Frame, registry: TargetRegistry | None = None +): + """The dense result and its ordered problem, rebound to the calibrated frame. + + With ``registry`` the result's target descriptors are the register's + (each spec's entity and column measure), as the seam's result carried + them: the graph solves the compiled rows, whose descriptors would + otherwise read as callables on the weight entity in the diagnostics. + The numbers are the compiled rows' either way. + """ + + problem = decode_problem( + graph_payload(manifest, store, NATIONAL_PROBLEM_NODE, "problem") + ) + dense = manifest.nodes["uk.full.dense"].opaque_artifacts + solution = decode_solution( + store.load_bytes(dense["solution"]), + problem_sha256=problem.sha256, + entity_ids=_axis(frame), + ) + initial_frame = Frame( + {entity: frame.table(entity) for entity in frame.entities}, + frame.schema, + {"household": problem.problem.initial_weights}, + frame.strata, + metadata=frame.metadata, + ) + result = decode_calibration_result( + store.load_bytes(dense["result"]), frame=initial_frame, problem=problem + ) + if not np.array_equal(result.weights, solution.weights): + raise ValueError("Stored national result differs from the installed solution.") + if not np.array_equal(frame.weights_for("household").values, solution.weights): + raise ValueError( + "Calibrated national population does not carry the bound solution weights." + ) + result = replace(result, frame=frame) + if registry is not None: + described = tuple(spec.to_target() for spec in registry.specs) + if tuple(target.row_name for target in described) != tuple( + result.problem.names + ): + raise ValueError( + "The register's rows do not align with the solved national problem." + ) + result = replace(result, problem=replace(result.problem, targets=described)) + return result, problem + + +def replay_uk_national_gate_battery( + report_document: Mapping[str, Any], + *, + report_path: Path, + release_id: str, + diagnostics_sha256: str, + posture: UKRowwisePosture = UK_ROWWISE_NATIONAL_POSTURE, +) -> dict[str, object]: + """Persist, enforce and sign the seam battery from the graph's phase report. + + The graph evaluated the gates once (``uk.full.gates.calibrated``); the + battery is replayed from that stored phase report through the same + write-then-block boundary the seam used (``GateBatteryRun.record_phase``, + microcosm#901), the diagnostics digest measured from the written file + rides its attestation, and the scoped trio is grafted and signed exactly + as the seam signed it. A blocking outcome raises + :class:`~microcosm.build.gate_battery.GateBatteryBlockedError` after the + report, block included, is on disk. + """ + + from microcosm.build.gate_battery import ( + BlockingMode, + GateBatteryRun, + gate_phase_report_from_payload, + ) + + if ( + report_document.get("kind") != "uk_national_gate_report" + or report_document.get("schema_version") != 1 + ): + raise ValueError("Unsupported UK national gate report artifact.") + gates = uk_scoped_gate_manifest( + tuple(posture.gate_scope), + phases=("terminal",), + policy_suffix=posture.gate_policy_suffix, + ) + if report_document.get("policy_suffix") != posture.gate_policy_suffix: + raise ValueError("UK national gate report was evaluated under another scope.") + report = gate_phase_report_from_payload(report_document["report"], gates=gates) + battery = GateBatteryRun( + gates, + release_id=release_id, + report_path=report_path, + # The seam never runs release-candidate posture: its scoped battery + # covers the seven entries and must never sign a shippability claim; + # shippability comes only from the release-cut certification. + release_candidate=False, + registry=UK_GATE_REGISTRY, + release_evidence={"calibration_diagnostics_sha256": diagnostics_sha256}, + ) + battery.record_phase(report) + battery.enforce("terminal", mode=BlockingMode.BLOCKS_ARTIFACT) + payload = battery.report_payload() + calibration_run.finalize_uk_scoped_gate_report( + payload, + posture=str(report_document["posture"]), + scope_exclusions=dict(report_document["scope_exclusions"]), + aggregate_admin_measurement=report_document["aggregate_admin_measurement"], + ) + calibration_run._write_json(report_path, payload) + return payload + + +def national_run_config( + *, + posture: UKRowwisePosture, + config: UKNationalBuildConfig, + registry_document: Mapping[str, Any], + driver_parameters: Mapping[str, Any], +) -> dict[str, object]: + """The seam's ``run_config``: the attempt identity every record carries.""" + + if not registry_document.get("national_registry"): + raise ValueError( + "The national run_config requires a compiled national register." + ) + return { + "pipeline": posture.pipeline, + "release_id": posture.release_id, + "register_sha256": registry_from_payload( + registry_document["national_registry"] + ).version, + "calibration_year": int(config.calibration_year), + "doctrine": national_doctrine_payload(config.doctrine), + "doctrine_overrides": dict(config.doctrine_overrides), + "ledger": dict(registry_document["ledger_provenance"]), + "release_role": posture.role, + "allow_unpinned_feed": bool(config.allow_unpinned_feed), + "chronicle_feed_pin": dict(registry_document["chronicle_feed_pin"]), + "rowwise_driver_parameters": dict(driver_parameters), + "band_edge_register_sha256": registry_from_payload( + registry_document["band_edge_registry"] + ).version, + } + + +def national_build_record( + *, + posture: UKRowwisePosture, + build_id: str, + run_config: Mapping[str, object], + source_pins: Mapping[str, Mapping[str, object]], + input_posture: Mapping[str, object], + spine_provenance: Mapping[str, object], + register: Mapping[str, object], + calibration: Mapping[str, object], + gate_report: Mapping[str, object], + artifacts: Mapping[str, Mapping[str, object]], + staging_delivery: Mapping[str, object] | None, + graph: Mapping[str, object], +) -> dict[str, object]: + """The seam-shaped ``build_record.json`` the release-cut certifier reads.""" + + from microcosm.build.logbook_adoption import role_pins_digest + from microcosm.build.staging_v2 import validate_staging_delivery + + record: dict[str, object] = { + "schema_version": 1, + "pipeline": posture.pipeline, + "build_id": build_id, + "run_config": dict(run_config), + "source_pins": {k: dict(v) for k, v in source_pins.items()}, + "role_pins_digest": role_pins_digest(source_pins), + "input_posture": dict(input_posture), + "spine_provenance": dict(spine_provenance), + "register": dict(register), + "calibration": dict(calibration), + "gate_summary": calibration_run._gate_summary(gate_report), + # No shippability claim lives here: the calibration-scoped battery + # covers seven of the declared gate entries. The release verdict is + # the release-cut certification's, produced over this record. + "certification": { + "expected_artifact": str( + Path(str(artifacts["staging_h5"]["path"])).with_suffix( + ".release_certification.json" + ) + ), + "producer": "tools/certify_uk_release_cut.py", + }, + "artifacts": {k: dict(v) for k, v in artifacts.items()}, + "graph": dict(graph), + } + if staging_delivery is not None: + record["staging_delivery"] = validate_staging_delivery(staging_delivery) + return record diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_population.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_population.py index 375630895..cd0613ec7 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_population.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/graph_population.py @@ -495,9 +495,15 @@ class UKPoolCheckpointKernel(_PopulationKernel): """Checkpoint the complete pool; refuse unless the shared atomic gate passed. The shared ``geography.gate@1`` result is a platform-scoped typed artifact - (amendment 17). This FILTER node consumes it and emits no typed artifact of - its own, so the scope stops here instead of laundering into the bitwise - UK target and calibration chain through a gate report. + (amendment 17), and a bitwise UK kernel consuming it as a typed input + would be refused. This FILTER node consumes it and emits no typed + artifact of its own, so that refusal is avoided here. It is only the + typed-artifact edge that stops: the shared assignment kernels are + ``platform_bitwise`` too, and their columns reach the UK target and + calibration chain through population slices, which the executor already + reads at platform scope. Whether the shared kernels can honestly declare + ``bitwise`` (exact keyed draws, integer inverse CDF) is the open question + recorded on microcosm#932. """ ref = "uk.full.pool@1" diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/national_calibration.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/national_calibration.py index a98c96f4f..67f08dd59 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/national_calibration.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/national_calibration.py @@ -1,344 +1,42 @@ -"""Ledger-backed calibration stage for the UK national build.""" +"""UK national target materialisation helpers. + +The in-process ``UKNationalCalibrationStage`` retired when the national +release role moved onto the shared executable graph +(:mod:`.graph_national`: the register materialises inside +``uk.full.national_problem`` and the shared calibration nodes solve it). +What stays is the frame adapter with its prepared-frame/restore lifecycle, +the measure-input injection pair, the canonical mass reason and the scoring +route that materialises a register onto a frame without solving. +""" from __future__ import annotations -from collections.abc import Callable, Mapping, Sequence +from collections.abc import Mapping, Sequence from typing import Any import numpy as np -from microcosm.build.observation import StageEventObserver, StageEventRun -from microcosm.build.plan import Stage from microcosm.build.target_materialization import ( - MeasureResolution, resolve_target_measures, ) -from microcosm.build.uk_runtime.content_identity import uk_frame_content_identity from microcosm.build.uk_runtime.ledger_targets import ( UKFrameTargetAdapter, - UKLedgerTargetCompilation, materialize_uk_ledger_targets, ) -from microcosm.build.uk_runtime.national_doctrine import ( - UK_NATIONAL_SOLVE_DOCTRINE, - UKNationalSolveDoctrine, - uk_national_target_loss_weights, -) from microcosm.calibrate import ( - CalibrationResult, TargetRegistry, - calibrate, - effective_sample_size, ) -from microcosm.frame import Frame, WeightKind, Weights +from microcosm.frame import Frame, Weights __all__ = [ "CalibrationFrameAdapter", - "UKNationalCalibrationStage", "drop_injected_measure_inputs", "inject_measure_inputs", "national_calibration_mass_reason", - "uk_national_calibration_stage", + "prepare_uk_target_frame", ] -class UKNationalCalibrationStage: - """Fail-closed national calibration transform and its manifest evidence.""" - - def __init__( - self, - registry: UKLedgerTargetCompilation | TargetRegistry, - *, - period: int, - doctrine: UKNationalSolveDoctrine = UK_NATIONAL_SOLVE_DOCTRINE, - measure_resolver: object | None = None, - band_edge_registry: TargetRegistry, - progress_callback: Callable[[dict[str, object]], None] | None = None, - stage_callback: StageEventObserver | None = None, - ) -> None: - self.compilation = ( - registry - if isinstance(registry, UKLedgerTargetCompilation) - else UKLedgerTargetCompilation(registry=registry, unsupported=()) - ) - self.registry = self.compilation.registry - # Required, never defaulted: the stage cannot tell a pruned registry - # from a full one, so a fallback to self.registry would quietly - # restate #792 for any caller holding an exclusion-pruned roster. - # A caller whose registry is unpruned passes it explicitly. - self.band_edge_registry = band_edge_registry - # The materialization period is the declared calibration year the - # registry was compiled at — never the input frame's base-year - # time_period, which lags it (survey 2024, calibration 2025). - # A binding can separately declare its observed measurement period, - # as the three individual CGT rows do for FY2024-25. - if not isinstance(period, int) or isinstance(period, bool) or period <= 0: - raise ValueError( - f"period must be the declared calibration year, got {period!r}." - ) - self.period = period - self.doctrine = doctrine - self.measure_resolver = measure_resolver - self.progress_callback = progress_callback - self.stage_callback = stage_callback - self.manifest: dict[str, object] | None = None - self.diagnostics: tuple[dict[str, object], ...] = () - self.solve_result: CalibrationResult | None = None - self.output_content_identity: str | None = None - - def __call__(self, frame: Frame) -> Frame: - with self._observe("calibration_input_validation") as operation: - declared = len(self.registry.specs) + len(self.compilation.unsupported) - resolved = len(self.registry.specs) - if self.compilation.unsupported: - raise RuntimeError( - "UK national calibration resolved " - f"{resolved} of {declared} activated target references; " - f"unsupported={self.compilation.unsupported!r}." - ) - adapter = _CalibrationFrameAdapter(frame) - original_columns = { - entity: set(table.columns) for entity, table in adapter.tables.items() - } - operation.complete( - activated_reference_count=declared, - resolved_reference_count=resolved, - ) - - with self._observe("measure_resolution") as operation: - measure_resolution = self._resolve_measures(frame) - operation.complete( - resolved_measure_count=( - 0 - if measure_resolution is None - else len(measure_resolution.measure_inputs) - ) - ) - - with self._observe("target_materialization") as operation: - if measure_resolution is not None: - _inject_measure_inputs(adapter, measure_resolution.measure_inputs) - materialized = materialize_uk_ledger_targets( - adapter, - self.registry, - period=self.period, - band_edge_registry=self.band_edge_registry, - ) - if materialized.skipped: - skipped = [skip.__dict__ for skip in materialized.skipped] - raise RuntimeError( - "UK national calibration could not materialize every activated " - f"target reference: skipped={skipped}." - ) - if measure_resolution is not None: - _drop_injected_measure_inputs( - adapter, - measure_resolution.measure_inputs, - original_columns, - ) - operation.complete(materialized_target_count=len(self.registry.specs)) - - with self._observe("solver_preparation") as operation: - prepared = adapter.prepared_frame() - mass_reason = national_calibration_mass_reason( - spec.family for spec in self.registry.specs - ) - mass_log_records_before_calibration = len(frame.mass_log) - # Target-set rows follow registry spec order, so the doctrine weight - # vector aligns positionally; under the default "uniform" rule this - # is None — the kernel's own equal weighting. - target_loss_weights = uk_national_target_loss_weights( - [spec.family for spec in self.registry.specs], - rule=self.doctrine.target_weight_rule, - ) - target_set = self.registry.to_target_set() - operation.complete( - target_count=len(self.registry.specs), - household_count=len(prepared.table("household")), - ) - - with self._observe("solver_execution") as operation: - result = calibrate( - prepared, - target_set, - weight_entity="household", - epochs=self.doctrine.epochs, - learning_rate=self.doctrine.learning_rate, - mass=self.doctrine.mass_rule, - mass_reason=mass_reason, - max_weight_ratio=self.doctrine.max_weight_ratio, - seed=self.doctrine.seed, - l0_lambda=self.doctrine.l0_lambda, - target_loss_cap=self.doctrine.target_loss_cap, - target_loss_weights=target_loss_weights, - progress_callback=self.progress_callback, - ) - operation.complete( - matrix_target_count=len(result.problem.names), - final_loss=float(result.final_loss), - ) - - with self._observe("calibration_result_validation") as operation: - if result.skipped or len(result.problem.names) != declared: - skipped = [item.name for item in result.skipped] - raise RuntimeError( - "UK national calibration matrix did not contain every activated " - f"reference: declared={declared}, rows={len(result.problem.names)}, " - f"skipped={skipped}." - ) - clean_frame = adapter.restore(result.frame) - self.solve_result = result - calibration_record = _post_solve_calibration_record( - frame, - clean_frame, - before_count=mass_log_records_before_calibration, - ) - operation.complete( - entity_row_counts={ - entity: int(len(clean_frame.table(entity))) - for entity in clean_frame.entities - } - ) - - with self._observe("calibration_evidence_construction") as operation: - self.diagnostics = tuple( - { - "name": row.name, - "estimate": row.final_estimate, - "target": row.target, - "relative_error": row.relative_error, - } - for row in result.diagnostics - ) - ratios = result.weights / result.initial_weights - old_total = float(calibration_record.old_total) - new_total = float(calibration_record.new_total) - before_kind = frame.weights_for("household").kind - after_kind = clean_frame.weights_for("household").kind - manifest = { - "activated_reference_count": declared, - "resolved_reference_count": resolved, - "matrix_target_count": len(result.problem.names), - "loss": result.final_loss, - "effective_sample_size": effective_sample_size(result.weights), - "max_weight_ratio": float(ratios.max()), - "max_weight_ratio_bound": self.doctrine.max_weight_ratio, - "target_materialization": materialized.report(), - "weights": { - "household_weight_kind": after_kind.value, - "household_weight_kind_chain": [ - {"stage": "staging", "kind": before_kind.value}, - {"stage": "national_calibration", "kind": after_kind.value}, - ], - "mass_log_records_before_calibration": ( - mass_log_records_before_calibration - ), - "mass_log_records": len(clean_frame.mass_log), - "calibration_mass_change": { - "entity": str(calibration_record.entity), - "old_total": old_total, - "new_total": new_total, - "relative_shift": (new_total - old_total) / old_total, - "declared_factor": calibration_record.declared_factor, - "reason": str(calibration_record.reason), - }, - }, - "solve": { - "n_targets": len(result.problem.names), - "n_households": len(clean_frame.table("household")), - "initial_loss": float(result.initial_loss), - "final_loss": float(result.final_loss), - "n_nonzero": int(np.count_nonzero(result.weights)), - }, - "parameters": {"doctrine": _doctrine_bounds(self.doctrine)}, - } - if measure_resolution is not None: - manifest["measure_resolution"] = dict(measure_resolution.receipt) - self.manifest = manifest - self.output_content_identity = uk_frame_content_identity(clean_frame) - operation.complete(diagnostic_count=len(self.diagnostics)) - return clean_frame - - def _observe(self, stage_id: str) -> StageEventRun: - return StageEventRun(stage_id=stage_id, observer=self.stage_callback) - - def _resolve_measures(self, frame: Frame) -> MeasureResolution | None: - if self.measure_resolver is None: - return None - resolve = getattr(self.measure_resolver, "resolve", None) - if callable(resolve): - return resolve( - lambda: _CalibrationFrameAdapter(frame), - self.registry, - period=self.period, - band_edge_registry=self.band_edge_registry, - ) - return resolve_target_measures( - lambda: _CalibrationFrameAdapter(frame), - self.registry, - self.measure_resolver, - period=self.period, - band_edge_registry=self.band_edge_registry, - ) - - def checkpoint_metadata(self) -> Mapping[str, object]: - if self.manifest is None: - raise RuntimeError("UK national calibration has not run.") - return { - "calibration": self.manifest, - "diagnostics": self.diagnostics, - "output_content_identity": self.output_content_identity, - } - - def resume_from_checkpoint( - self, - metadata: Mapping[str, object], - frame: Frame, - ) -> None: - """Rehydrate completed calibration evidence from its checkpoint record.""" - - calibration = metadata.get("calibration") - diagnostics = metadata.get("diagnostics") - output_identity = metadata.get("output_content_identity") - count_keys = ( - "activated_reference_count", - "resolved_reference_count", - "matrix_target_count", - ) - if ( - not isinstance(calibration, Mapping) - or not all(key in calibration for key in count_keys) - or not isinstance(diagnostics, list) - or not all(isinstance(row, Mapping) for row in diagnostics) - or not isinstance(output_identity, str) - or not output_identity - ): - raise RuntimeError( - "UK national calibration resume requires the checkpoint record " - "to carry calibration counts, diagnostics, and output content " - "identity; a record without them cannot feed the calibration " - "reference coverage gate or the drift check." - ) - if uk_frame_content_identity(frame) != output_identity: - raise RuntimeError( - "UK national calibration checkpoint content does not match its " - "recorded output identity; refusing to resume from a drifted " - "record." - ) - self.manifest = dict(calibration) - self.diagnostics = tuple(dict(row) for row in diagnostics) - self.output_content_identity = output_identity - - -def uk_national_calibration_stage( - registry: UKLedgerTargetCompilation, **kwargs: Any -) -> Stage: - """Return the named ordered-stage entry for national calibration.""" - - transform = UKNationalCalibrationStage(registry, **kwargs) - return Stage(name="national_calibration", transform=transform) - - def national_calibration_mass_reason(bound_families: Sequence[str]) -> str: """The mass-record reason a national doctrine calibration declares.""" @@ -351,49 +49,6 @@ def national_calibration_mass_reason(bound_families: Sequence[str]) -> str: ) -def _post_solve_calibration_record( - before: Frame, - after: Frame, - *, - before_count: int, -): - if after.weights_for("household").kind is not WeightKind.CALIBRATED: - raise RuntimeError( - "UK national calibration returned household weights whose kind is " - f"{after.weights_for('household').kind.value!r}, not 'calibrated'." - ) - if len(after.mass_log) != before_count + 1: - raise RuntimeError( - "UK national calibration must append exactly one mass record; " - f"before={before_count}, after={len(after.mass_log)}." - ) - if before.mass_log != after.mass_log[:before_count]: - raise RuntimeError( - "UK national calibration changed pre-existing mass-log records." - ) - record = after.mass_log[-1] - if record.entity != "household" or "calibration" not in record.reason: - raise RuntimeError( - "UK national calibration latest mass record is not the calibration " - f"record: entity={record.entity!r}, reason={record.reason!r}." - ) - return record - - -def _doctrine_bounds(doctrine: UKNationalSolveDoctrine) -> dict[str, object]: - return { - "epochs": doctrine.epochs, - "learning_rate": doctrine.learning_rate, - "max_weight_ratio": doctrine.max_weight_ratio, - "seed": doctrine.seed, - "target_loss_cap": doctrine.target_loss_cap, - "scale_rule": doctrine.scale_rule, - "target_weight_rule": doctrine.target_weight_rule, - "mass_rule": doctrine.mass_rule, - "l0_lambda": doctrine.l0_lambda, - } - - def inject_measure_inputs( adapter: UKFrameTargetAdapter, measure_inputs: Mapping[tuple[str, str], np.ndarray], diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/national_role.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/national_role.py index 786b10c90..5d6f6cf94 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/national_role.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/national_role.py @@ -1,26 +1,17 @@ -"""The UK rowwise driver's national release role (microcosm#823). +"""The UK build driver's national release role (microcosm#823): evidence shapes. ``microcosm-build-uk --release-role national`` builds the certified national -line by dispatching to the retained calibration seam -(:func:`~microcosm.build.uk_runtime.calibration_run.run_uk_calibration`): no -cloning, national targets only, the seam doctrine with explicit flags as -receipted overrides, the six calibration-seam gates, the seam-shaped -``build_record.json`` that ``tools/certify_uk_release_cut.py`` certifies, a -frozen ``national_target_registry.json`` for the scorer, and the same -staging telemetry (run id = the attempt id) and staged bundle as the dense -role. The driver adds the pinned input, the rowwise manifest beside the -seam's evidence, and the optional end-of-build evaluation against the -incumbent (microcosm#578 rule 1 on the common surface). - -These functions were moved from ``tools/build_uk_rowwise_candidate.py`` -(microcosm#901 phase 4) and consume only the shared rowwise command surface -(:mod:`~microcosm.build.uk_runtime.rowwise_cli`, -:mod:`~microcosm.build.uk_runtime.rowwise_staging`) and package APIs; the -graph full-build driver (:mod:`~microcosm.build.uk_runtime.full_build_cli`) -dispatches here before any graph preparation. The private spellings are -kept as aliases so the drivers' tests can patch the names they always did. -The posture-driven graph national path (the next PR on this line) replaces -this dispatch. +line on the shared executable graph (:mod:`.graph_national`; the driver's +national path lives in :mod:`.full_build_cli`). This module holds what the +role adds around the graph: the dry-run plan, the receipted doctrine +overrides, the schema-4 national manifest over the seam-shaped evidence, the +optional end-of-build evaluation against the incumbent (microcosm#578 rule 1 +on the common surface), the pinned-input and output-path checks and the +local sums. These functions were moved from +``tools/build_uk_rowwise_candidate.py`` (microcosm#901 phase 4) and consume +only the shared rowwise command surface (:mod:`.rowwise_cli`, +:mod:`.rowwise_staging`) and package APIs; the private spellings are kept +as aliases. """ from __future__ import annotations @@ -28,9 +19,8 @@ import argparse import hashlib import importlib -import json import sys -from collections.abc import Mapping +from collections.abc import Callable, Mapping from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -38,7 +28,6 @@ import numpy as np import pandas as pd -from microcosm.build.ledger_artifact import load_ledger_consumer_artifact from microcosm.build.logbook_adoption import atomic_write_json from microcosm.build.staging_dataset import ( SHA256SUMS_FILENAME, @@ -46,125 +35,60 @@ refresh_sha256sums_entry, ) from microcosm.build.staging_v2 import StagingTelemetryV2 -from microcosm.build.uk_runtime.calibration_run import ( - UKCalibrationRunPaths, - new_uk_calibration_attempt_id, - run_uk_calibration, - runtime_provenance, -) -from microcosm.build.uk_runtime.chronicle_feed import ( - require_committed_uk_chronicle_feed_pin, -) +from microcosm.build.uk_runtime.calibration_run import runtime_provenance from microcosm.build.uk_runtime.diagnostics import uk_fit_by_family from microcosm.build.uk_runtime.frs_release import load_uk_frs_release -from microcosm.build.uk_runtime.ledger_targets import compile_uk_target_registry -from microcosm.build.uk_runtime.measure_simulation import ( - UKMeasureResolver, - apply_uk_calibration_measure_exclusions, - load_uk_calibration_measure_exclusions, -) +from microcosm.build.uk_runtime.full_targets import load_uk_national_target_inputs from microcosm.build.uk_runtime.national_doctrine import uk_doctrine_with_overrides from microcosm.build.uk_runtime.rowwise_cli import ( git_commit, git_dirty, json_text, - output_paths, posture_of, rowwise_parameters, ) from microcosm.build.uk_runtime.rowwise_posture import UKRowwisePosture from microcosm.build.uk_runtime.rowwise_staging import ( add_staging_artifact, - create_staging_telemetry, - fail_staging_telemetry, - finalize_staging_telemetry, gate_statuses, - preflight_staged_dataset, - replace_manifest, stage, - stage_dataset, - stage_sample, - staging_delivery, - staging_epoch_every, - thinned_epochs, ) from microcosm.calibrate import TargetRegistry -__all__ = ["national_dry_run", "run_national_role"] - - -def _read_json(path: Path) -> dict[str, Any]: - payload = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(payload, dict): - raise ValueError(f"{path} must hold a JSON object.") - return payload - - -def _ledger_facts_pin(artifact: Any) -> dict[str, object]: - facts_path = ( - artifact.path / "consumer_facts.jsonl" - if artifact.path.is_dir() - else artifact.path - ) - return {"sha256": artifact.facts_sha256, "size_bytes": facts_path.stat().st_size} +__all__ = [ + "evaluate_against_incumbent", + "incumbent_arguments", + "national_doctrine_overrides", + "national_dry_run", + "national_manifest", + "require_bound_input", +] -def _load_national_target_inputs(args: argparse.Namespace) -> dict[str, Any]: - """The national role's target surface: the pinned Ledger artifact, compiled. +def load_national_target_inputs(args: argparse.Namespace) -> dict[str, Any]: + """The national role's target surface, compiled as the graph node compiles it. - The artifact must be the committed Chronicle feed pin unless - ``--allow-unpinned-feed`` records a reviewed diagnostic run; the - compiled register, less the measure exclusions, is the solve surface and - the full compiled register keeps the band edges; ``--register-json`` - requires the re-derived register to be the frozen scoring surface. + The dry run plans over it; a build compiles inside + ``uk.full.national_targets`` with the same loader and records the result + as the registry artifact. """ - artifact = load_ledger_consumer_artifact( + return load_uk_national_target_inputs( args.ledger_facts, expected_facts_sha256=args.ledger_facts_sha256, expected_manifest_sha256=args.ledger_manifest_sha256, - ) - pin = require_committed_uk_chronicle_feed_pin( - artifact.facts_sha256, - manifest_sha256=artifact.manifest_sha256, + measure_exclusions=args.measure_exclusions, + register_json=args.register_json, + calibration_year=int(load_uk_frs_release().calibration_year), + exclusions_evaluated_on=args.review_date, allow_unpinned_feed=bool(args.allow_unpinned_feed), ) - calibration_year = int(load_uk_frs_release().calibration_year) - compilation = compile_uk_target_registry( - artifact.facts, target_period=calibration_year - ) - if compilation.unsupported: - raise SystemExit( - f"{len(compilation.unsupported)} national target references " - "failed to compile" - ) - exclusions = load_uk_calibration_measure_exclusions(args.measure_exclusions) - registry, exclusion_receipt = apply_uk_calibration_measure_exclusions( - compilation.registry, exclusions - ) - if args.register_json is not None: - try: - frozen = TargetRegistry.from_json(args.register_json) - except ValueError as error: - raise SystemExit( - f"error: frozen scoring register is unusable: {error}" - ) from error - if frozen.version != registry.version: - raise SystemExit( - "re-derived register differs from the frozen scoring register: " - f"{registry.version} vs {frozen.version}" - ) - return { - "artifact": artifact, - "calibration_year": calibration_year, - "national_registry": registry, - "band_edge_registry": compilation.registry, - "measure_exclusions": exclusion_receipt, - "chronicle_feed_pin": pin.to_dict(), - } -def _national_doctrine_overrides(args: argparse.Namespace) -> dict[str, Any]: +_load_national_target_inputs = load_national_target_inputs + + +def national_doctrine_overrides(args: argparse.Namespace) -> dict[str, Any]: """The receipted per-run overrides of the seam doctrine, explicit flags only.""" explicit = args._explicit_arguments @@ -180,11 +104,15 @@ def _national_doctrine_overrides(args: argparse.Namespace) -> dict[str, Any]: return overrides -def _require_bound_input(args: argparse.Namespace) -> None: +_national_doctrine_overrides = national_doctrine_overrides + + +def require_bound_input(args: argparse.Namespace) -> None: """The national role builds from a bound spine checkpoint, never a request. The graph driver admits ``--spine-request`` as the dense build's - population source; the seam engine reads a pinned ``--input-h5``. + population source; the national line solves a pinned ``--input-h5`` + checkpoint as bound. """ if args.input_h5 is None: @@ -195,17 +123,28 @@ def _require_bound_input(args: argparse.Namespace) -> None: ) -def national_dry_run(args: argparse.Namespace) -> int: - """Compile the national target surface and print the plan; write nothing.""" +_require_bound_input = require_bound_input + + +def national_dry_run( + args: argparse.Namespace, + *, + operation_inventory: Callable[[], Mapping[str, Any]] | None = None, +) -> int: + """Compile the national target surface and print the plan; write nothing. + + ``operation_inventory`` (the driver's) adds the compiled graph's node + inventory to the plan. + """ posture = posture_of(args) - input_h5 = _require_file(args.input_h5, label="--input-h5") - input_artifact = _artifact_info(input_h5) - _verify_requested_pin("--input-h5", input_artifact, requested=args.input_sha256) + input_h5 = require_file(args.input_h5, label="--input-h5") + input_artifact = artifact_info(input_h5) + verify_requested_pin("--input-h5", input_artifact, requested=args.input_sha256) frs_release = load_uk_frs_release() - inputs = _load_national_target_inputs(args) + inputs = load_national_target_inputs(args) doctrine, doctrine_overrides = uk_doctrine_with_overrides( - **_national_doctrine_overrides(args) + **national_doctrine_overrides(args) ) plan = { "schema_version": 3, @@ -216,7 +155,7 @@ def national_dry_run(args: argparse.Namespace) -> int: "calibration_year": inputs["calibration_year"], "inputs": {"dataset": dict(input_artifact)}, "targets": { - "chronicle": inputs["artifact"].provenance(), + "chronicle": inputs["chronicle_provenance"], "compiled": len(inputs["band_edge_registry"].specs), "active": len(inputs["national_registry"].specs), "excluded": len(inputs["measure_exclusions"]), @@ -239,14 +178,16 @@ def national_dry_run(args: argparse.Namespace) -> int: "doctrine_overrides": dict(doctrine_overrides), "parameters": rowwise_parameters( args, - source_year=_source_year( + source_year=source_year( args.source_year, time_period=str(frs_release.time_period) ), ), "engine": "not_run", - "incumbent": _incumbent_arguments(args), + "incumbent": incumbent_arguments(args), "releasable": False, } + if operation_inventory is not None: + plan["graph"] = dict(operation_inventory()) print(json_text(plan)) return 0 @@ -254,257 +195,7 @@ def national_dry_run(args: argparse.Namespace) -> int: _national_dry_run = national_dry_run -def run_national_role(args: argparse.Namespace) -> int: - """Serve ``--release-role national``: the dry-run plan or the seam build. - - The validated request arrives from the driver's ``main`` before any graph - preparation. A dry run plans without solving, writing or staging; a build - runs the staged-dataset pre-flight (argument refusals cost nothing; the - credential check reaches the Hub, so it runs last, still before any input - is read) and then the seam build under the driver's posture. - """ - - _require_bound_input(args) - if args.dry_run: - return national_dry_run(args) - preflight_staged_dataset(args) - return _run_national_role(args) - - -def _run_national_role(args: argparse.Namespace) -> int: - """Build the national line: the calibration seam under the driver's posture. - - The seam library (:func:`run_uk_calibration`) resolves the measures from - the input file, solves under the seam doctrine, runs the six - calibration-seam gates, writes the H5, the diagnostics, the signed gate - report, the build record and the Logbook row exactly as the retired - seam command did, so a national cut built here is bit-for-bit the seam's. - The driver adds what the dense role has: the pinned input, the role's - doctrine and overrides, staging telemetry under the attempt id, the - rowwise manifest beside the seam's evidence, and the staged bundle. - """ - - posture = posture_of(args) - out_dir = args.out.expanduser().resolve() - input_h5 = _require_file(args.input_h5, label="--input-h5") - if out_dir.exists() and not out_dir.is_dir(): - raise ValueError(f"--out must be a directory path, got {out_dir}.") - input_artifact = _artifact_info(input_h5) - _verify_requested_pin("--input-h5", input_artifact, requested=args.input_sha256) - incumbent = _incumbent_arguments(args) - # The attempt id is minted before telemetry opens so the staging run id - # and the Logbook row agree, as on the dense role. - build_id = new_uk_calibration_attempt_id(timestamp=datetime.now(UTC)) - telemetry = create_staging_telemetry(args, build_id=build_id) - try: - return _run_national_attempt( - args, - posture=posture, - out_dir=out_dir, - input_h5=input_h5, - input_artifact=input_artifact, - build_id=build_id, - telemetry=telemetry, - incumbent=incumbent, - ) - except BaseException as error: - fail_staging_telemetry(telemetry, error) - raise - - -def _run_national_attempt( - args: argparse.Namespace, - *, - posture: UKRowwisePosture, - out_dir: Path, - input_h5: Path, - input_artifact: Mapping[str, Any], - build_id: str, - telemetry: StagingTelemetryV2 | None, - incumbent: Mapping[str, Any] | None = None, -) -> int: - stage( - telemetry, - "input_pinning", - "completed", - dataset_sha256=input_artifact["sha256"], - ) - stage_sample(telemetry, sample_fraction=args.sample_fraction) - frs_release = load_uk_frs_release() - calibration_year = int(frs_release.calibration_year) - args._calibration_year = calibration_year - args._frs_vintage = str(frs_release.vintage) - source_year = _source_year( - args.source_year, time_period=str(frs_release.time_period) - ) - paths_by_role = output_paths(out_dir, posture=posture, vintage=args._frs_vintage) - _validate_output_paths(paths_by_role, input_h5=input_h5, ladder_path=None) - stage(telemetry, "target_compilation", "started") - inputs = _load_national_target_inputs(args) - stage( - telemetry, - "target_compilation", - "completed", - compiled_target_count=len(inputs["band_edge_registry"].specs), - active_target_count=len(inputs["national_registry"].specs), - ) - doctrine, doctrine_overrides = uk_doctrine_with_overrides( - **_national_doctrine_overrides(args) - ) - if doctrine.target_weight_rule != args.target_weight_rule: - raise RuntimeError( - "national doctrine override did not bind the requested rule." - ) - args._doctrine_override_receipt = doctrine_overrides - out_dir.mkdir(parents=True, exist_ok=True) - # The frozen register is the scorer's input: the same artifact this run - # solved against, by content hash. - inputs["national_registry"].to_json(paths_by_role["national_registry"]) - # The full compiled register beside it: the band edges a pruned scoring - # surface must never redraw (#803), for the end-of-build evaluation and - # for a re-score by hand (--band-edge-registry-json). - inputs["band_edge_registry"].to_json(paths_by_role["contract_registry"]) - resolver = UKMeasureResolver( - simulation_source=input_h5, - scratch_dir=out_dir, - year=calibration_year, - frame=None, - ) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=paths_by_role["dataset"], - diagnostics_json=paths_by_role["calibration_diagnostics"], - build_record_json=paths_by_role["build_record"], - terminal_gate_json=paths_by_role["terminal_gates"], - ) - evidence: dict[str, Any] = {} - - def publish_manifest() -> None: - # The seam has written its evidence; the manifest describes it and the - # staged bundle (every manifest-registered output) follows, as on the - # dense role. The staged copy predates the evidence blocks appended - # below; staged_manifest.json describes the remote side. - args._gate_report = _read_json(paths.terminal_gate_json) - manifest = _national_manifest( - args, - posture=posture, - build_id=build_id, - build_record=_read_json(paths.build_record_json), - build_record_path=paths.build_record_json, - gate_report=args._gate_report, - diagnostics=_read_json(paths.diagnostics_json), - inputs=inputs, - doctrine_overrides=doctrine_overrides, - output_paths=paths_by_role, - input_artifact=input_artifact, - source_year=source_year, - ) - replace_manifest(paths_by_role["manifest"], manifest) - evidence["manifest"] = manifest - evidence["staged_dataset"] = stage_dataset( - args, - manifest=manifest, - output_paths=paths_by_role, - run_id=build_id if telemetry is None else telemetry.run_id, - telemetry=telemetry, - ) - - def evaluate() -> None: - # After the bundle is staged (the evaluation never blocks staging), - # before the telemetry completes (the receipt rides it as an artifact). - evidence["evaluation"] = _evaluate_against_incumbent( - args, - incumbent=incumbent, - inputs=inputs, - output_paths=paths_by_role, - telemetry=telemetry, - calibration_year=calibration_year, - out_dir=out_dir, - ) - - def finalize_staging() -> None: - publish_manifest() - evaluate() - finalize_staging_telemetry(args, telemetry) - - def event_callback(stage_id: str, status: str, details: Mapping[str, Any]) -> None: - stage(telemetry, stage_id, status, **dict(details)) - - result = run_uk_calibration( - paths=paths, - build_id=build_id, - input_sha256=str(args.input_sha256), - ledger_artifact=inputs["artifact"], - register_registry=inputs["national_registry"], - band_edge_registry=inputs["band_edge_registry"], - calibration_year=calibration_year, - exclusion_receipt=inputs["measure_exclusions"], - doctrine=doctrine, - doctrine_overrides=doctrine_overrides, - measure_resolver=resolver, - source_pins={ - "input_h5": { - "sha256": str(args.input_sha256), - "size_bytes": int(input_artifact["bytes"]), - }, - "ledger_facts": _ledger_facts_pin(inputs["artifact"]), - }, - run_config_extra={ - "release_role": posture.role, - "calibration_year": calibration_year, - "allow_unpinned_feed": bool(args.allow_unpinned_feed), - "chronicle_feed_pin": inputs["chronicle_feed_pin"], - "rowwise_driver_parameters": rowwise_parameters( - args, source_year=source_year - ), - }, - release_id=posture.release_id, - logbook_prev_row_digest=args.logbook_prev_row_digest, - progress_callback=( - None - if telemetry is None - else thinned_epochs( - telemetry.calibration_progress, every=staging_epoch_every(args) - ) - ), - event_callback=None if telemetry is None else event_callback, - staging_delivery=staging_delivery(telemetry), - staging_finalizer=None if telemetry is None else finalize_staging, - staging_delivery_provider=( - None if telemetry is None else (lambda: telemetry.delivery_summary) - ), - ) - if telemetry is None: - publish_manifest() - evaluate() - manifest = evidence["manifest"] - # The seam rewrote its record with the delivery summary after the - # finalizer; the manifest binds the record as it now is. - manifest["outputs"]["build_record"] = _artifact_info(paths.build_record_json) - manifest["build_record"] = { - "path": str(paths.build_record_json), - "sha256": result.build_record_sha256, - } - manifest["staging_delivery"] = staging_delivery(telemetry) - manifest["staged_dataset"] = evidence["staged_dataset"] - evaluation = evidence.get("evaluation", {"status": "not_requested"}) - manifest["evaluation"] = evaluation - if evaluation.get("status") == "completed": - manifest["outputs"]["score_receipt"] = evaluation["receipt"] - replace_manifest(paths_by_role["manifest"], manifest) - if (out_dir / SHA256SUMS_FILENAME).is_file(): - # The uploaded copies list the files as uploaded; the local sums - # list the record, the receipt and the manifest as they now are, - # evidence included. - refresh_sha256sums_entry(out_dir, paths.build_record_json.name) - if evaluation.get("status") == "completed": - _list_sha256sums_entry(out_dir, paths_by_role["score_receipt"].name) - refresh_sha256sums_entry(out_dir, paths_by_role["manifest"].name) - print(json_text(manifest)) - return 0 - - -def _national_fit_by_family( +def national_fit_by_family( diagnostics: Mapping[str, Any], registry: TargetRegistry ) -> list[dict[str, object]]: families = {str(spec.name): str(spec.family or "") for spec in registry.specs} @@ -536,7 +227,10 @@ def _national_fit_by_family( return uk_fit_by_family(pd.DataFrame(rows)) -def _national_manifest( +_national_fit_by_family = national_fit_by_family + + +def national_manifest( args: argparse.Namespace, *, posture: UKRowwisePosture, @@ -550,9 +244,19 @@ def _national_manifest( output_paths: Mapping[str, Path], input_artifact: Mapping[str, Any], source_year: int, + reported_paths: Mapping[str, Path] | None = None, + graph: Mapping[str, Any] | None = None, ) -> dict[str, Any]: - """The rowwise manifest of a national-role run, over the seam's evidence.""" + """The rowwise manifest of a national-role run, over the seam-shaped evidence. + ``inputs`` carries the compiled surface (``national_registry``, + ``band_edge_registry``, ``calibration_year``, ``measure_exclusions``, + ``chronicle_provenance``); ``reported_paths`` names the published + locations when the files were written to a staging directory first; + ``graph`` names the graph artifacts the evidence stood on. + """ + + reported = dict(output_paths) if reported_paths is None else dict(reported_paths) calibration = build_record["calibration"] solve = calibration["solve"] statuses = gate_statuses(gate_report) @@ -564,18 +268,22 @@ def _national_manifest( ], dtype=np.float64, ) - fit_rows = _national_fit_by_family(diagnostics, inputs["national_registry"]) + fit_rows = national_fit_by_family(diagnostics, inputs["national_registry"]) + + def output(role: str) -> dict[str, Any]: + return artifact_info(output_paths[role], reported_path=reported[role]) + outputs = { - "dataset": _artifact_info(output_paths["dataset"]), - "calibration_diagnostics": _artifact_info( - output_paths["calibration_diagnostics"] + "dataset": output("dataset"), + "calibration_diagnostics": output("calibration_diagnostics"), + "build_record": artifact_info( + build_record_path, reported_path=reported["build_record"] ), - "build_record": _artifact_info(build_record_path), - "terminal_gate_report": _artifact_info(output_paths["terminal_gates"]), - "national_target_registry": _artifact_info(output_paths["national_registry"]), - "national_contract_registry": _artifact_info(output_paths["contract_registry"]), + "terminal_gate_report": output("terminal_gates"), + "national_target_registry": output("national_registry"), + "national_contract_registry": output("contract_registry"), } - return { + manifest = { "schema_version": 4, "build_kind": "uk_national_calibrated_candidate", "release_role": posture.role, @@ -592,7 +300,7 @@ def _national_manifest( **dict(input_artifact), "spine_provenance": dict(build_record["spine_provenance"]), }, - "targets": {"chronicle": inputs["artifact"].provenance()}, + "targets": {"chronicle": dict(inputs["chronicle_provenance"])}, "code": {"git_commit": git_commit(), "git_dirty": git_dirty()}, "runtime": runtime_provenance(), "sampling": {"mode": "full"}, @@ -657,13 +365,19 @@ def _national_manifest( }, "measure_exclusions": dict(inputs["measure_exclusions"]), "build_record": { - "path": str(build_record_path), + "path": str(reported["build_record"]), "sha256": outputs["build_record"]["sha256"], }, } + if graph is not None: + manifest["graph"] = dict(graph) + return manifest + + +_national_manifest = national_manifest -def _verify_requested_pin( +def verify_requested_pin( label: str, artifact: Mapping[str, Any], *, @@ -680,6 +394,9 @@ def _verify_requested_pin( artifact["pin_verified"] = True +_verify_requested_pin = verify_requested_pin + + def _load_candidate_evaluator(importer=importlib.import_module): """The common-surface scorer (microcosm#967), loaded when an incumbent is given. @@ -696,7 +413,7 @@ def _load_candidate_evaluator(importer=importlib.import_module): ) from error -def _incumbent_arguments(args: argparse.Namespace) -> dict[str, Any] | None: +def incumbent_arguments(args: argparse.Namespace) -> dict[str, Any] | None: """The national role's optional incumbent, pinned and verified up front.""" if args.incumbent_h5 is None and args.incumbent_sha256 is None: @@ -706,9 +423,9 @@ def _incumbent_arguments(args: argparse.Namespace) -> dict[str, Any] | None: "--incumbent-h5 and --incumbent-sha256 must be given together." ) _load_candidate_evaluator() - incumbent_h5 = _require_file(args.incumbent_h5, label="--incumbent-h5") - info = _artifact_info(incumbent_h5) - _verify_requested_pin("--incumbent-h5", info, requested=args.incumbent_sha256) + incumbent_h5 = require_file(args.incumbent_h5, label="--incumbent-h5") + info = artifact_info(incumbent_h5) + verify_requested_pin("--incumbent-h5", info, requested=args.incumbent_sha256) return { "path": str(incumbent_h5), "sha256": info["sha256"], @@ -717,7 +434,10 @@ def _incumbent_arguments(args: argparse.Namespace) -> dict[str, Any] | None: } -def _list_sha256sums_entry(out_dir: Path, name: str) -> None: +_incumbent_arguments = incumbent_arguments + + +def list_sha256sums_entry(out_dir: Path, name: str) -> None: """List a file the build wrote after the sidecars in the local sums.""" sums_path = out_dir / SHA256SUMS_FILENAME @@ -725,13 +445,16 @@ def _list_sha256sums_entry(out_dir: Path, name: str) -> None: if name in {listed for _, listed in entries}: refresh_sha256sums_entry(out_dir, name) return - digest = _artifact_info(out_dir / name)["sha256"] + digest = artifact_info(out_dir / name)["sha256"] sums_path.write_text( sums_path.read_text(encoding="utf-8") + f"{digest} {name}\n", encoding="utf-8", ) +_list_sha256sums_entry = list_sha256sums_entry + + #: Receipt blocks that are record arrays (lists of mappings): the reviewed #: telemetry artifact policy refuses them, and the verdict, the pruned block #: and the aggregates carry everything a reviewer reads. The full receipt stays @@ -751,7 +474,7 @@ def _score_receipt_telemetry_summary(score: Mapping[str, Any]) -> dict[str, Any] } -def _evaluate_against_incumbent( +def evaluate_against_incumbent( args: argparse.Namespace, *, incumbent: Mapping[str, Any] | None, @@ -786,7 +509,7 @@ def _evaluate_against_incumbent( "started", incumbent_sha256=incumbent["sha256"], ) - candidate = _artifact_info(output_paths["dataset"]) + candidate = artifact_info(output_paths["dataset"]) try: module = _load_candidate_evaluator() score = module.evaluate_uk_candidate_against_incumbent( @@ -848,12 +571,15 @@ def _evaluate_against_incumbent( "scored_surface": dict(evaluation["scored_surface"]), "pruned_measures": list(pruned["measures"]), "pruned_families": dict(pruned["families"]), - "receipt": _artifact_info(output_paths["score_receipt"]), + "receipt": artifact_info(output_paths["score_receipt"]), "incumbent": dict(incumbent), } -def _validate_output_paths( +_evaluate_against_incumbent = evaluate_against_incumbent + + +def validate_output_paths( output_paths: Mapping[str, Path], *, input_h5: Path, @@ -878,14 +604,20 @@ def _validate_output_paths( ) -def _require_file(path: Path, *, label: str) -> Path: +_validate_output_paths = validate_output_paths + + +def require_file(path: Path, *, label: str) -> Path: resolved = path.expanduser().resolve() if not resolved.is_file(): raise FileNotFoundError(f"{label} artifact not found: {resolved}.") return resolved -def _source_year(requested: int | None, *, time_period: str) -> int: +_require_file = require_file + + +def source_year(requested: int | None, *, time_period: str) -> int: if requested is not None: if requested <= 0: raise ValueError("--source-year must be positive.") @@ -898,7 +630,10 @@ def _source_year(requested: int | None, *, time_period: str) -> int: return int(prefix) -def _artifact_info( +_source_year = source_year + + +def artifact_info( path: Path, *, reported_path: Path | None = None, @@ -912,3 +647,6 @@ def _artifact_info( "sha256": digest.hexdigest(), "bytes": int(path.stat().st_size), } + + +_artifact_info = artifact_info diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/rowwise_cli.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/rowwise_cli.py index fd3f03180..28d9f1236 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/rowwise_cli.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/rowwise_cli.py @@ -295,12 +295,15 @@ def record_candidate_attempt( predecessor: str | None, spool_dir: Path, rung: str = "f100", + pipeline: str = UK_CANDIDATE_PIPELINE, ) -> Path: + """Record the attempt on the role's Logbook pipeline (the dense one by default).""" + return record_terminal_attempt( state=state, started_at=started_at, started_ts=started_ts, - pipeline=UK_CANDIDATE_PIPELINE, + pipeline=pipeline, rung=rung, seed=seed, code_pin=code_pin, @@ -325,11 +328,18 @@ def record_candidate_error( base_dir: Path, spool_dir: Path, rung: str = "f100", + pipeline: str = UK_CANDIDATE_PIPELINE, + disposition: str = "failed", ) -> None: + """The error receipt and the terminal row of an attempt that did not complete. + + ``disposition`` is ``failed`` for an error and ``discarded`` for an + operator interrupt (the seam's ``_record_failed_attempt`` distinction). + """ error_path = write_error_receipt( error_receipt_path(base_dir, build_id=state.build_id), state=state, - pipeline=UK_CANDIDATE_PIPELINE, + pipeline=pipeline, error=error, ) apply_error_verdict( @@ -342,10 +352,11 @@ def record_candidate_error( started_ts=started_ts, seed=seed, code_pin=code_pin, - disposition="failed", + disposition=disposition, predecessor=predecessor, spool_dir=spool_dir, rung=rung, + pipeline=pipeline, ) diff --git a/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py b/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py index fd2a33622..83c12b80a 100644 --- a/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py +++ b/packages/microcosm-build/src/microcosm/build/uk_runtime/terminal_gates.py @@ -180,6 +180,14 @@ def __post_init__(self) -> None: "household.clone_index", "household.constituency_code_oa", "household.consumer_debt", + # microcosm#932: the five nation-native aliases of the atomic assignment + # (output_area_code, data_zone_code, intermediate_zone_code, + # super_data_zone_code, district_electoral_area_code) never reach an + # artifact: graph_terminal._tables drops them at the single-year export + # boundary. They are listed because the full-build export-surface gate + # reads the graph frame's in-memory columns (full_gates.py, + # ``parity_evidence.candidate_columns``) before that boundary, and would + # otherwise flag them as unreviewed extras (the F8 gap on #932). "household.data_zone_code", "household.district_electoral_area_code", "household.electricity_consumption", diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_atomic_assignment_measurement.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_atomic_assignment_measurement.py index 9457cf37d..9a9106a3b 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_atomic_assignment_measurement.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_atomic_assignment_measurement.py @@ -174,6 +174,18 @@ def test_breach_tables_and_disclosure_suppression(harness, toy_ladder): assert harness._suppress(2.0, 3) == "<3" assert harness._suppress(0.0, 3) == 0.0 assert harness._suppress(3.0, 3) == 3.0 + # z is a function of the suppressed count, so it is blanked with it + # (#932 round 1: the counts were recoverable from expected_rows and z). + row = {"rows": 2.0, "ess": 1.5, "sources": 2.0, "z": 0.3, "expected_rows": 1.0} + assert harness._suppressed_area(row, 3) == { + **row, + "rows": "<3", + "ess": "<3", + "sources": "<3", + "z": None, + } + assert harness._suppressed_area({**row, "rows": 3.0}, 3)["z"] == 0.3 + assert harness._suppressed_area({**row, "rows": 0.0}, 3)["z"] == 0.3 def test_committed_931_evidence_is_internally_consistent(): @@ -197,6 +209,8 @@ def test_committed_931_evidence_is_internally_consistent(): assert value == f"<{evidence['minimum_count']}" else: assert value == 0 or value >= evidence["minimum_count"] + if isinstance(area["rows"], str): + assert area["z"] is None assert "household_id" not in area and "source_household_id" not in area if cell["law"] == "keyed": assert all(cell["identity_stability"].values()) diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_calibration_run.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_calibration_run.py index 9e294ecc2..cf8fc9f9d 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_calibration_run.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_calibration_run.py @@ -91,577 +91,6 @@ def test_import_hygiene_does_not_load_national_build_in_fresh_subprocess(): assert " ".join(("from", legacy_module, "import")) not in source -def test_run_uk_calibration_writes_cross_pinned_outputs(monkeypatch, tmp_path: Path): - pytest.importorskip("tables") # pandas HDF backend - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - spine_sidecar = _write_spine_sidecar(input_h5, frame) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - source_pins = { - "input_h5": {"sha256": _sha(input_h5), "size_bytes": input_h5.stat().st_size}, - "ledger_facts": {"sha256": "a" * 64, "size_bytes": 1}, - } - progress: list[dict[str, object]] = [] - events: list[tuple[str, str, dict[str, object]]] = [] - delivery = { - "contract_version": 2, - "enabled": False, - "mode": "disabled", - "run_id": None, - "configured_repository": None, - "upload_attempts": 0, - "upload_successes": 0, - "read_back": "not_requested", - "last_error_code": None, - "opt_out_reason": "test", - } - - result = run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=5), - doctrine_overrides={}, - measure_resolver=None, - source_pins=source_pins, - run_config_extra={"calibration_year": 2025}, - release_id="test-run", - progress_callback=progress.append, - event_callback=lambda stage_id, status, details: events.append( - (stage_id, status, dict(details)) - ), - staging_delivery=delivery, - ) - - assert paths.staging_h5.exists() - assert paths.diagnostics_json.exists() - assert paths.build_record_json.exists() - assert paths.terminal_gate_json.exists() - assert progress and progress[0]["kind"] == "calibration_epoch" - completed = { - stage_id for stage_id, status, _details in events if status == "completed" - } - assert { - "input_loading", - "calibration_input_validation", - "measure_resolution", - "target_materialization", - "solver_preparation", - "solver_execution", - "calibration_result_validation", - "calibration_evidence_construction", - "calibration", - "diagnostics", - "release_check_evaluation", - "candidate_h5_creation", - "build_record_creation", - } <= completed - assert result.build_record["staging_delivery"] == delivery - assert result.build_record["artifacts"]["staging_h5"]["sha256"] == _sha( - paths.staging_h5 - ) - assert result.build_record["artifacts"]["diagnostics_json"]["sha256"] == _sha( - paths.diagnostics_json - ) - assert result.build_record["artifacts"]["terminal_gate_json"]["sha256"] == _sha( - paths.terminal_gate_json - ) - # The record makes no shippability claim of its own — the hand-written - # literal retired with the #757 release-cut audit — and instead points - # at the certification artifact whose verdict is authoritative. - assert "shippable" not in result.build_record - assert "shippable_reason" not in result.build_record - certification = result.build_record["certification"] - assert certification["producer"] == "tools/certify_uk_release_cut.py" - assert certification["expected_artifact"] == str( - paths.staging_h5.with_suffix(".release_certification.json") - ) - spine_provenance = result.build_record["spine_provenance"] - assert spine_provenance["stages"] == spine_sidecar["stages"] - assert spine_provenance["stage_records"] == spine_sidecar["stage_records"] - assert spine_provenance["stage_evidence"] == spine_sidecar["stage_evidence"] - assert spine_provenance["artifact_pins"] == spine_sidecar["artifact_pins"] - assert ( - spine_provenance["input_artifact_pins"] == spine_sidecar["input_artifact_pins"] - ) - assert spine_provenance["resource_pins"] == spine_sidecar["resource_pins"] - assert ( - spine_provenance["stage_artifact_pins"] == spine_sidecar["stage_artifact_pins"] - ) - assert spine_provenance["declared_seeds"] == spine_sidecar["declared_seeds"] - assert spine_provenance["rules_engine"] == spine_sidecar["rules_engine"] - assert spine_provenance["source_vintages"] == spine_sidecar["source_vintages"] - assert ( - spine_provenance["stochastic_contract_sha256"] - == spine_sidecar["stochastic_contract_sha256"] - ) - diagnostics = json.loads(paths.diagnostics_json.read_text()) - assert diagnostics["build"]["spine_provenance"] == spine_provenance - staged, _ = load_uk_national_frame(paths.staging_h5) - assert staged.weights_for("household").kind is WeightKind.CALIBRATED - report = json.loads(paths.terminal_gate_json.read_text()) - assert report["posture"] == "calibration_seam" - assert set(report["scope_exclusions"]) == set(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS) - attestation = report["attestation"] - signature = attestation["signature"] - attestation["signature"] = None - key = b"0123456789abcdef0123456789abcdef" - assert ( - hmac.new(key, canonical_json_bytes(report), hashlib.sha256).hexdigest() - == signature - ) - assert result.logbook_spool.exists() - - -def test_readback_failure_updates_build_record_and_failed_attempt( - monkeypatch, tmp_path: Path -): - pytest.importorskip("tables") - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - paths = _paths(tmp_path) - initial_delivery = { - "contract_version": 2, - "enabled": True, - "mode": "local_and_remote", - "run_id": "readback-failure", - "configured_repository": "policyengine/populace-uk-staging", - "upload_attempts": 3, - "upload_successes": 0, - "read_back": "not_requested", - "last_error_code": "UPLOAD_FAILED", - "opt_out_reason": None, - } - failed_delivery = { - **initial_delivery, - "read_back": "failed", - "last_error_code": "READ_BACK_FAILED", - } - - def fail_readback() -> None: - raise StagingReadBackError("authenticated read-back failed") - - with pytest.raises(StagingReadBackError, match="read-back failed"): - run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="readback-failure", - staging_delivery=initial_delivery, - staging_finalizer=fail_readback, - staging_delivery_provider=lambda: failed_delivery, - ) - - record = json.loads(paths.build_record_json.read_text()) - assert record["staging_delivery"] == failed_delivery - spooled = sorted((tmp_path / "logbook-spool").rglob("*.json")) - assert len(spooled) == 1 - assert json.loads(spooled[0].read_text())["disposition"] == "failed" - - -def test_run_uk_calibration_requires_the_band_edge_register( - tmp_path: Path, -): - # Required, never defaulted: an empty receipt is a claim that nothing was - # pruned, not permission to skip the reconciliation, so the seam takes no - # register-without-edges path at all (#803 review findings 1 and 3). - paths = _paths(tmp_path) - - with pytest.raises(TypeError, match="band_edge_registry"): - run_uk_calibration( - paths=paths, - input_sha256="a" * 64, - ledger_artifact=object(), - register_registry=_registry(), - calibration_year=2025, - exclusion_receipt={"excluded.target": {"reason": "reviewed"}}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={}, - run_config_extra={}, - release_id="pruned-without-edge-register", - ) - - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.build_record_json.exists() - - -def test_run_uk_calibration_reconciles_an_empty_receipt_as_no_prunes( - tmp_path: Path, -): - # A pruned register handed in with an empty receipt must refuse: with - # nothing declared excluded, the two rosters have to be name-identical. - paths = _paths(tmp_path) - full = _registry() - pruned = TargetRegistry([], country="uk") - - with pytest.raises(ValueError, match="exclusion receipt"): - run_uk_calibration( - paths=paths, - input_sha256="a" * 64, - ledger_artifact=object(), - register_registry=pruned, - band_edge_registry=full, - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={}, - run_config_extra={}, - release_id="empty-receipt-pruned-register", - ) - - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.build_record_json.exists() - - -def test_run_uk_calibration_refuses_incoherent_band_edge_register(tmp_path: Path): - paths = _paths(tmp_path) - edge_registry = TargetRegistry( - [ - *_registry().specs, - TargetSpec( - name="different.excluded", - entity="benunit", - measure="different/excluded", - value=1.0, - source="test", - metadata={"contract_target_id": "different.excluded"}, - ), - ], - country="uk", - ) - - with pytest.raises(ValueError, match="exclusion receipt"): - run_uk_calibration( - paths=paths, - input_sha256="a" * 64, - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=edge_registry, - calibration_year=2025, - exclusion_receipt={"other.excluded": {"reason": "reviewed"}}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={}, - run_config_extra={}, - release_id="incoherent-edge-register", - ) - - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.build_record_json.exists() - - -def test_run_uk_calibration_records_band_edge_register_sha256( - monkeypatch, tmp_path: Path -): - pytest.importorskip("tables") # pandas HDF backend - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - paths = _paths(tmp_path) - register = _registry() - edge_registry = TargetRegistry( - [ - TargetSpec( - name="dwp.uc.households", - entity="benunit", - measure="dwp/uc/households", - value=99.0, - source="test", - family="dwp_universal_credit", - metadata={"contract_target_id": "dwp.uc.households"}, - hierarchy=_uc_hierarchy(), - ) - ], - country="uk", - ) - - result = run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=register, - band_edge_registry=edge_registry, - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=5), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={}, - release_id="band-edge-provenance", - ) - - assert ( - result.build_record["run_config"]["band_edge_register_sha256"] - == edge_registry.version - ) - - -def test_run_uk_calibration_refuses_input_sha_before_outputs(tmp_path: Path): - pytest.importorskip("tables") # pandas HDF backend - input_h5 = tmp_path / "input.h5" - write_uk_national_frame(_frame(), input_h5) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - with pytest.raises(ValueError, match="sha mismatch"): - run_uk_calibration( - paths=paths, - input_sha256="0" * 64, - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="bad-sha", - ) - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - - -def test_run_uk_calibration_refuses_absent_input_sidecar(tmp_path: Path): - pytest.importorskip("tables") # pandas HDF backend - input_h5 = tmp_path / "input.h5" - write_uk_national_frame(_frame(), input_h5) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - - with pytest.raises(ValueError, match="build sidecar absent"): - run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="missing-sidecar", - ) - - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.terminal_gate_json.exists() - - -@pytest.mark.parametrize( - ("override", "message"), - [ - ( - {"entity_row_counts": {"person": 999, "benunit": 4, "household": 4}}, - "row-count mismatch", - ), - ({"household_weight_total": 1.0}, "household_weight_total mismatch"), - ], -) -def test_run_uk_calibration_refuses_unbound_input_sidecar( - override, message, tmp_path: Path -): - pytest.importorskip("tables") # pandas HDF backend - frame = _frame() - input_h5 = tmp_path / "input.h5" - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame, **override) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - - with pytest.raises(ValueError, match=message): - run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="unbound-sidecar", - ) - - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.terminal_gate_json.exists() - - -def test_seam_never_modifies_data_variables(monkeypatch, tmp_path: Path): - """The seam's defining invariant: weights move, data never does. - - Every data column of every entity table in the staged H5 must be - byte-identical to the input; only the household weights, the weight - kind, and exactly one appended mass record may differ. - """ - - pytest.importorskip("tables") # pandas HDF backend - - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - - # Target 30 against an initial weighted UC count of 20, so the solve - # genuinely has to move weights while the data stays untouched. - pulling_registry = TargetRegistry( - [ - TargetSpec( - name="dwp.uc.households", - entity="benunit", - measure="dwp/uc/households", - value=30.0, - source="test", - family="dwp_universal_credit", - metadata={"contract_target_id": "dwp.uc.households"}, - hierarchy=_uc_hierarchy(), - ) - ], - country="uk", - ) - run_uk_calibration( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=pulling_registry, - band_edge_registry=pulling_registry, - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=50), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={}, - release_id="invariant-run", - ) - - source, _ = load_uk_national_frame(input_h5) - staged, _ = load_uk_national_frame(paths.staging_h5) - for entity in ("person", "benunit", "household"): - left = source.table(entity) - right = staged.table(entity) - assert list(left.columns) == list(right.columns), entity - for column in left.columns: - pd.testing.assert_series_equal(left[column], right[column]) - assert staged.weights_for("household").kind is WeightKind.CALIBRATED - assert not np.allclose( - staged.weights_for("household").values, - source.weights_for("household").values, - ) - assert len(staged.mass_log) == len(source.mass_log) + 1 - - def test_aggregate_admin_measurement_convention_and_refusals(): frame = _frame() manifest = _manifest_with_small_anchor() @@ -735,241 +164,6 @@ def test_seam_pipeline_derives_a_ratified_logbook_scope(): assert scope in logbook_tool.DECLARED_SCOPES -def test_refusal_records_a_failed_attempt_and_stages_nothing(tmp_path: Path): - pytest.importorskip("tables") # pandas HDF backend - input_h5 = tmp_path / "input.h5" - write_uk_national_frame(_frame(), input_h5) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - - with pytest.raises(ValueError, match="sha mismatch"): - run_uk_calibration( - paths=paths, - input_sha256="0" * 64, - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="refused-run", - ) - - # Every terminal disposition is a row; a refusal that left the chain - # silent would hide the attempt entirely. - spooled = sorted((tmp_path / "logbook-spool").rglob("*.json")) - assert spooled, "refusal recorded no Logbook row" - rows = [json.loads(path.read_text()) for path in spooled] - assert [row["disposition"] for row in rows] == ["failed"] - assert rows[0]["pipeline"] == calibration_run._PIPELINE - # The refusal is explained on disk, not only in the row. - receipts = sorted((tmp_path / "logbook-receipts").rglob("error.json")) - assert len(receipts) == 1 - assert not paths.staging_h5.exists() - assert not paths.diagnostics_json.exists() - assert not paths.terminal_gate_json.exists() - - -def test_attempt_ids_are_unique_across_reruns_of_one_release( - monkeypatch, tmp_path: Path -): - pytest.importorskip("tables") # pandas HDF backend - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - source_pins = { - "input_h5": {"sha256": _sha(input_h5), "size_bytes": input_h5.stat().st_size} - } - build_ids = [] - for attempt in ("a", "b"): - run_dir = tmp_path / attempt - run_dir.mkdir() - result = run_uk_calibration( - paths=UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=run_dir / "staged.h5", - diagnostics_json=run_dir / "diagnostics.json", - build_record_json=run_dir / "build_record.json", - terminal_gate_json=run_dir / "terminal_gates.json", - ), - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=5), - doctrine_overrides={}, - measure_resolver=None, - source_pins=source_pins, - run_config_extra={"calibration_year": 2025}, - release_id="one-release-id", - ) - build_ids.append(result.build_record["build_id"]) - - # Both the local chain and the store reject a duplicate build id, so one - # release re-run twice must not collide. - assert build_ids[0] != build_ids[1] - assert all(value.startswith("uk-frs-calibration-attempt-") for value in build_ids) - - -def test_verified_ledger_identity_reaches_the_run_evidence(monkeypatch, tmp_path: Path): - pytest.importorskip("tables") # pandas HDF backend - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - artifact = SimpleNamespace( - facts_sha256="d" * 64, - fact_row_count=107_550, - manifest_sha256="e" * 64, - manifest={ - "artifact_id": "chronicle-uk-artifact-1cab809", - "profile": "uk-national", - "schema_version": 1, - "unrelated": "not carried", - }, - ) - - result = run_uk_calibration( - paths=UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ), - input_sha256=_sha(input_h5), - ledger_artifact=artifact, - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=5), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="ledger-identity", - ) - - ledger = result.build_record["run_config"]["ledger"] - assert ledger["facts_sha256"] == "d" * 64 - assert ledger["manifest_sha256"] == "e" * 64 - assert ledger["fact_row_count"] == 107_550 - assert ledger["manifest"] == { - "artifact_id": "chronicle-uk-artifact-1cab809", - "profile": "uk-national", - "schema_version": 1, - } - # A bare feed carries no manifest, and that absence is recorded rather - # than invented. - assert calibration_run._ledger_provenance(object())["manifest_sha256"] is None - - -def test_mixed_epoch_feed_epochs_reach_the_uk_release_evidence(monkeypatch, tmp_path): - """A UK run says which Chronicle era resolved its targets. - - The run's own provenance block used to be assembled field by field from - the artifact, which is how it came to carry the hashes but not the epoch - witnesses the loader had already computed. It now delegates to the shared - block, so a cutover-window feed is visible in the signed diagnostics and - in the build record — including the one Chronicle-namespace spelling this - build does not declare, named rather than folded into an era. - """ - pytest.importorskip("tables") # pandas HDF backend - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - artifact = load_ledger_consumer_artifact(_mixed_epoch_artifact_dir(tmp_path)) - diagnostics_json = tmp_path / "diagnostics.json" - - result = run_uk_calibration( - paths=UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=tmp_path / "staged.h5", - diagnostics_json=diagnostics_json, - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ), - input_sha256=_sha(input_h5), - ledger_artifact=artifact, - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=5), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": { - "sha256": _sha(input_h5), - "size_bytes": input_h5.stat().st_size, - } - }, - run_config_extra={"calibration_year": 2025}, - release_id="chronicle-mixed-epoch", - ) - - ledger = result.build_record["run_config"]["ledger"] - # The observed manifest id, verbatim, and the era it belongs to. - assert ledger["manifest"]["schema_version"] == ( - PUBLISHED_CONSUMER_ARTIFACT_SCHEMA_VERSION - ) - assert ledger["schema_epoch"] == "ledger" - # The feed straddles the cutover, and says so rather than reporting one era. - assert ledger["fact_key_epochs"] == ["ledger", "chronicle", "undeclared"] - assert ledger["undeclared_fact_key_domains"] == ["chronicle.source_release.v9"] - assert ledger["fact_schema_versions"] == [ - CHRONICLE_CONSUMER_FACT_SCHEMA_VERSION, - LEDGER_CONSUMER_FACT_SCHEMA_VERSION, - ] - # The hashes the block always carried are unchanged by the delegation. - assert ledger["facts_sha256"] == artifact.facts_sha256 - assert ledger["fact_row_count"] == 3 - - # The same block is what the signed diagnostics carry, so the evidence a - # release assembler reads witnesses the era too. - diagnostics = json.loads(diagnostics_json.read_text()) - assert diagnostics["build"]["ledger"] == ledger - - def test_the_uk_block_delegates_rather_than_reassembling_the_shared_one(tmp_path): """Every field of the shared provenance block reaches the UK block. @@ -991,51 +185,56 @@ def test_the_uk_block_delegates_rather_than_reassembling_the_shared_one(tmp_path assert block["manifest"]["schema_version"] == shared["schema_version"] -def test_run_uk_calibration_accepts_a_caller_minted_attempt_id(monkeypatch, tmp_path): - """The rowwise driver mints the id before telemetry opens (microcosm#823).""" +# --- The contracts the graph's national path still stands on -------------- - pytest.importorskip("tables") - monkeypatch.setattr( - calibration_run, - "uk_aggregate_admin_totals", - lambda frame, manifest: (_admin_anchor_values(), []), - ) - input_h5 = tmp_path / "input.h5" - frame = _frame() - write_uk_national_frame(frame, input_h5) - _write_spine_sidecar(input_h5, frame) - paths = _paths(tmp_path) - paths = UKCalibrationRunPaths( - input_h5=input_h5, - staging_h5=paths.staging_h5, - diagnostics_json=paths.diagnostics_json, - build_record_json=paths.build_record_json, - terminal_gate_json=paths.terminal_gate_json, + +def _spec(name: str) -> TargetSpec: + return TargetSpec( + name=name, + entity="benunit", + measure=f"test/{name}", + value=1.0, + source="test", + family="test", + metadata={"contract_target_id": name}, ) - minted = calibration_run.new_uk_calibration_attempt_id( - timestamp=__import__("datetime").datetime(2026, 9, 20, 12, 0, tzinfo=UTC) + + +def test_band_edge_register_must_reconstitute_the_compiled_roster(): + """The reconciliation always runs: an empty receipt claims nothing was + pruned, so the two rosters must then be name-identical; a receipt that + disagrees with the pruned names refuses. The graph's national target + node and the dense target compiler both stand on this check.""" + compiled = TargetRegistry([_spec("a"), _spec("b")], country="uk") + pruned = TargetRegistry([_spec("a")], country="uk") + calibration_run._validate_band_edge_registry( + register_registry=pruned, + band_edge_registry=compiled, + exclusion_receipt={"b": {"reason": "test"}}, ) - assert minted.startswith("uk-frs-calibration-attempt-20260920T120000Z-") - common = dict( - paths=paths, - input_sha256=_sha(input_h5), - ledger_artifact=object(), - register_registry=_registry(), - band_edge_registry=_registry(), - calibration_year=2025, - exclusion_receipt={}, - doctrine=UKNationalSolveDoctrine(epochs=1), - doctrine_overrides={}, - measure_resolver=None, - source_pins={ - "input_h5": {"sha256": _sha(input_h5), "size_bytes": 1}, - "ledger_facts": {"sha256": "a" * 64, "size_bytes": 1}, - }, - run_config_extra={"calibration_year": 2025}, - release_id="test-run", + calibration_run._validate_band_edge_registry( + register_registry=compiled, band_edge_registry=compiled, exclusion_receipt={} ) - with pytest.raises(ValueError, match="uk-frs-calibration-attempt-"): - run_uk_calibration(build_id="uk-local-candidate-f100-s42-x", **common) - result = run_uk_calibration(build_id=minted, **common) - assert result.build_record["build_id"] == minted - assert result.logbook_spool is not None + with pytest.raises(ValueError, match="must match the measure exclusion receipt"): + calibration_run._validate_band_edge_registry( + register_registry=pruned, band_edge_registry=compiled, exclusion_receipt={} + ) + with pytest.raises(ValueError, match="must include every materialized"): + calibration_run._validate_band_edge_registry( + register_registry=compiled, + band_edge_registry=pruned, + exclusion_receipt={}, + ) + + +def test_attempt_ids_carry_the_seam_prefix_and_are_unique(): + """The driver's national role mints the seam's attempt id before telemetry + opens (microcosm#823); two mints of one instant differ.""" + from datetime import UTC, datetime + + instant = datetime(2026, 9, 29, 12, 0, 0, tzinfo=UTC) + first = calibration_run.new_uk_calibration_attempt_id(timestamp=instant) + second = calibration_run.new_uk_calibration_attempt_id(timestamp=instant) + prefix = calibration_run.UK_CALIBRATION_ATTEMPT_ID_PREFIX + "20260929T120000Z-" + assert first.startswith(prefix) and second.startswith(prefix) + assert first != second diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_cgt_observation_period.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_cgt_observation_period.py index 9b01fbc5b..04c183d12 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_cgt_observation_period.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_cgt_observation_period.py @@ -26,20 +26,36 @@ def resolver_factory(**kwargs): registry = _registry() if route == "national": - resolver = resolver_factory( - frame=original, scratch_dir=tmp_path / "engine", year=2025 - ) - stage = UKNationalCalibrationStage( + from microcosm.calibrate import build_constraint_matrix, calibrate + + # The graph's national route: resolve on the bound frame, materialise, + # compile the rows, solve under the doctrine, restore the pristine + # tables around the calibrated weights. + prepared, restore, rows, evidence = materialize_uk_national_rows( + original, registry, - band_edge_registry=registry, period=2025, - doctrine=UKNationalSolveDoctrine(epochs=2), - measure_resolver=resolver, + band_edge_registry=registry, + resolver_factory=resolver_factory, + scratch_dir=tmp_path / "engine", + ) + problem = build_constraint_matrix(prepared, rows.targets, "household") + assert not problem.skipped + doctrine = UKNationalSolveDoctrine(epochs=2) + result = calibrate( + prepared, + rows.targets, + weight_entity="household", + epochs=doctrine.epochs, + learning_rate=doctrine.learning_rate, + seed=doctrine.seed, + mass="free", + mass_reason="national doctrine calibration (test)", + max_weight_ratio=doctrine.max_weight_ratio, + target_loss_cap=doctrine.target_loss_cap, ) - fitted = stage(original) - receipt = stage.manifest["measure_resolution"]["provider"][ - "cgt_period_contract" - ] + fitted = restore(result.frame) + receipt = evidence["cgt_period_contract"] else: from microcosm.build.uk_runtime import full_measure from microcosm.build.uk_runtime.local_rowwise import ( diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_country_adapter.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_country_adapter.py index e2a8d4377..4564401ab 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_country_adapter.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_country_adapter.py @@ -6,6 +6,7 @@ from microcosm.build.country_spec import load_country_spec from microcosm.build.uk_runtime.country_adapter import ( + uk_atomic_support_register, validate_uk_country_source_projection, ) from test_support.paths import paths_for @@ -46,3 +47,23 @@ def test_country_source_projection_refuses_divergent_header_pin(tmp_path): spec = load_country_spec(destination) with pytest.raises(ValueError, match="raw-source pins differ"): validate_uk_country_source_projection(spec) + + +def test_atomic_support_register_is_the_committed_provenance(): + """The register a release candidate is checked against (microcosm#932 + round 1) is sources.yaml's, and it agrees with the provenance resource.""" + import json + + from microcosm.build.uk_runtime.atomic_area_support import SYSTEMS + + register = uk_atomic_support_register() + assert tuple(register) == SYSTEMS + package = paths_for("microcosm-build").package / "src/microcosm/build/uk" + provenance = json.loads( + (package / "uk_atomic_area_supports.provenance.json").read_text() + ) + for system, row in register.items(): + assert row == { + "sha256": provenance["supports"][system]["sha256"], + "size_bytes": provenance["supports"][system]["size_bytes"], + } diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_dense_release_assembler.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_dense_release_assembler.py index dd19d03d7..b6af98901 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_dense_release_assembler.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_dense_release_assembler.py @@ -15,6 +15,10 @@ from microcosm.build.uk_runtime.calibration_run import UK_LOCAL_GATE_SCOPE from microcosm.build.uk_runtime.release_identity import UK_DENSE_RELEASE_ID from microcosm.data import contract as dc +from test_support.microcosm_build.uk_full_build_cli import ( + patch_support_register, + synthetic_geography_binding, +) from test_support.paths import paths_for _TEST_PATHS = paths_for("microcosm-build") @@ -97,6 +101,14 @@ def _signed_report(): return report +@pytest.fixture(autouse=True) +def _registered_supports(monkeypatch) -> None: + """The assembler runs the release pre-flight, which checks the candidate's + geography support pins against the register; the synthetic candidates + bind the synthetic pins, so register those.""" + patch_support_register(monkeypatch) + + def _staging_delivery(**overrides) -> dict: """A validated version 2 telemetry receipt the run carried (local-only).""" @@ -286,6 +298,7 @@ def _candidate_dir(root: Path) -> tuple[Path, Path, Path]: } }, "staging_delivery": _staging_delivery(), + "geography": {"assignment": synthetic_geography_binding()}, "identity": { "spine": { "pin_verified": True, diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_cli.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_cli.py index c66ea5aaf..96a19b1e0 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_cli.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_cli.py @@ -1,6 +1,8 @@ """The canonical CLI restores declared files and preserves failure/scope semantics.""" # ruff: noqa: F403, F405 +from types import SimpleNamespace + from test_support.microcosm_build.uk_full_build_cli import * @@ -39,8 +41,9 @@ def test_geography_assignment_arguments_are_closed(tmp_path): release = arguments(tmp_path, "--release-candidate") assert release.release_candidate and release.geography_assignment == "atomic" cli.validate_cli_args(release) - # The national role is dispatched to the seam: the supports are refused - # by name with the other dense-only flags, and its default needs none. + # The national role builds on the bound checkpoint with no geography + # assignment: the supports are refused by name with the other dense-only + # flags, and its default needs none. cli.validate_cli_args(cli.parse_args(_national_argv(tmp_path))) national = cli.parse_args( _national_argv(tmp_path, "--atomic-support-sha256-ni", "e" * 64) @@ -163,12 +166,13 @@ def test_dense_role_requires_the_ladder_and_the_pins(tmp_path): cli.validate_cli_args(cli.parse_args(request)) -def test_national_role_is_validated_then_dispatched_to_the_seam(tmp_path, monkeypatch): - """The national line never prepares a graph (microcosm#901 phase 4). +def test_national_role_is_validated_then_built_through_the_graph(tmp_path, monkeypatch): + """The national line prepares the national graph, never the dense one. - The validated request goes to ``national_role.run_national_role`` after - the role tables have run; the seam's own pre-flight, Logbook, staging - and manifest live behind that call. + The validated request goes to ``prepare_national_build`` / + ``execute_national_build`` after the role tables have run; the seam's + attempt id, the Hub pre-flight, the Logbook and the staging telemetry + live in the driver's national envelope. """ argv = _national_argv(tmp_path) national = cli.parse_args(argv) @@ -178,58 +182,89 @@ def test_national_role_is_validated_then_dispatched_to_the_seam(tmp_path, monkey cli.validate_cli_args(national) served = [] monkeypatch.setattr( - cli.national_role, "run_national_role", lambda args: served.append(args) or 7 + cli, "preflight_staged_dataset", lambda args: served.append("preflight") ) monkeypatch.setattr( cli, - "prepare_full_build", - lambda *a, **k: pytest.fail("the national role prepared a graph"), + "prepare_national_build", + lambda args, *, telemetry=None, attempt=None: served.append(args) or "prepared", ) monkeypatch.setattr( cli, - "preflight_staged_dataset", - lambda args: pytest.fail("the driver pre-flighted before dispatching"), + "execute_national_build", + lambda prepared, args, *, telemetry=None, attempt=None: ( + served.append(prepared) or 7 + ), + ) + monkeypatch.setattr( + cli, + "prepare_full_build", + lambda *a, **k: pytest.fail("the national role prepared the dense graph"), ) assert cli.main(argv) == 7 - (args,) = served + assert served[0] == "preflight" + args = served[1] assert args.release_role == "national" assert args._posture is national._posture - assert not (tmp_path / "out").exists() - # The dense refusal table still applies before the dispatch. + assert served[2] == "prepared" + # The dense refusal table still applies before anything is prepared. with pytest.raises(ValueError, match="--release-role national refuses"): cli.main([*argv, "--ladder", str(tmp_path / "ladder.npz")]) - assert served == [args] + assert len(served) == 3 -def test_national_dry_run_dispatches_to_the_seam_plan(tmp_path, monkeypatch, capsys): - """``--dry-run`` on the national role plans through ``national_dry_run``: - no graph, no staged-dataset pre-flight, nothing written.""" +def test_national_dry_run_plans_with_the_graph_inventory(tmp_path, monkeypatch, capsys): + """``--dry-run`` on the national role plans through ``national_dry_run`` + with the compiled graph's inventory: no solve, no staged-dataset + pre-flight, nothing written.""" monkeypatch.setattr( cli.national_role, "national_dry_run", - lambda args: ( - print(json.dumps({"dry_run": args.dry_run, "role": args.release_role})) or 0 + lambda args, *, operation_inventory=None: ( + print( + json.dumps( + { + "dry_run": args.dry_run, + "role": args.release_role, + "graph": operation_inventory(), + } + ) + ) + or 0 + ), + ) + monkeypatch.setattr( + cli, + "prepare_national_build", + lambda args, *, telemetry=None, attempt=None: SimpleNamespace( + national=SimpleNamespace( + operation_inventory=lambda: {"nodes": ["uk.full.national_problem"]} + ) ), ) monkeypatch.setattr( cli, "prepare_full_build", - lambda *a, **k: pytest.fail("the national dry run prepared a graph"), + lambda *a, **k: pytest.fail("the national dry run prepared the dense graph"), ) monkeypatch.setattr( - cli.national_role, + cli, "preflight_staged_dataset", lambda args: pytest.fail("a dry run reached the Hub pre-flight"), ) assert cli.main(_national_argv(tmp_path, "--dry-run")) == 0 - assert json.loads(capsys.readouterr().out) == {"dry_run": True, "role": "national"} + assert json.loads(capsys.readouterr().out) == { + "dry_run": True, + "role": "national", + "graph": {"nodes": ["uk.full.national_problem"]}, + } assert not (tmp_path / "out").exists() def test_national_role_refuses_a_spine_request(tmp_path, monkeypatch): - """The seam engine reads a bound spine; ``--spine-request`` is dense-only.""" + """The national line solves a bound spine; ``--spine-request`` is dense-only.""" monkeypatch.setattr( - cli.national_role, + cli, "preflight_staged_dataset", lambda args: pytest.fail("the refusal must precede the Hub pre-flight"), ) @@ -1288,3 +1323,70 @@ def test_gate_battery_recorded_exception_is_named_not_a_missing_artifact(): } ) cli._require_gate_kernel_completed(passed, "uk.full.gates.calibrated") + + +def test_dense_completion_marker_binds_the_closed_manifest(tmp_path, monkeypatch): + """``build.json`` is re-issued after the close step appends the staging + receipts to the manifest, so its digest is the manifest's final bytes.""" + status, out = run_dense_main(tmp_path, monkeypatch) + assert status == 0 + completion = json.loads((out / "build.json").read_text()) + entry = completion["rowwise_candidate_manifest"] + manifest_path = out / "rowwise_candidate_manifest.json" + manifest = json.loads(manifest_path.read_text()) + assert "staged_dataset" in manifest and "staging_delivery" in manifest + assert entry["sha256"] == hashlib.sha256(manifest_path.read_bytes()).hexdigest() + assert entry["size_bytes"] == manifest_path.stat().st_size + assert "note" not in entry + + +def test_national_interrupt_records_a_discarded_row_and_re_raises( + tmp_path, monkeypatch +): + """A Ctrl-C during the national build is a discarded attempt, not a failed + one: the seam's terminal-disposition contract, kept by the driver (the + failure sidecar names the interrupt, the row says ``discarded``, the + interrupt propagates).""" + argv = _national_argv(tmp_path) + monkeypatch.setattr(cli, "preflight_staged_dataset", lambda args: None) + monkeypatch.setattr( + cli, + "prepare_national_build", + lambda args, *, telemetry=None, attempt=None: "prepared", + ) + + def interrupted(prepared, args, *, telemetry=None, attempt=None): + raise KeyboardInterrupt + + monkeypatch.setattr(cli, "execute_national_build", interrupted) + with pytest.raises(KeyboardInterrupt): + cli.main(argv) + out = tmp_path / "out" + failure = json.loads((out / "failure.json").read_text()) + assert failure["error_type"] == "KeyboardInterrupt" + rows = load_spool_rows(out / "logbook-spool") + assert [row.disposition for row in rows] == ["discarded"] + assert rows[0].pipeline == "uk-frs-calibration" + + +def test_dense_interrupt_records_a_discarded_row_and_re_raises(tmp_path, monkeypatch): + """The dense line closes an interrupted attempt the same way.""" + monkeypatch.delenv("POPULACE_LOGBOOK_PREV_ROW_DIGEST", raising=False) + patch_certification(monkeypatch) + args = arguments(tmp_path) + build = prepared(tmp_path) + monkeypatch.setattr(cli, "parse_args", lambda argv: args) + monkeypatch.setattr( + cli, "prepare_full_build", lambda args, *, telemetry=None, attempt=None: build + ) + + def interrupted(prepared, args, *, telemetry=None, attempt=None): + raise KeyboardInterrupt + + monkeypatch.setattr(cli, "execute_full_build", interrupted) + with pytest.raises(KeyboardInterrupt): + cli.main([]) + failure = json.loads((args.out / "failure.json").read_text()) + assert failure["error_type"] == "KeyboardInterrupt" + rows = load_spool_rows(args.out / "logbook-spool") + assert [row.disposition for row in rows] == ["discarded"] diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_preparation.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_preparation.py index 073b6c009..ebb234ef3 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_preparation.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_build_preparation.py @@ -13,6 +13,7 @@ from microcosm.graph import ContentStore, compile_graph, run_graph from test_support.microcosm_build.uk_atomic_support_fixtures import write_toy_supports from test_support.microcosm_build.uk_calibration_run import _bound_checkpoint +from test_support.microcosm_build.uk_full_build_cli import patch_support_register from test_support.microcosm_build.uk_full_population_graph import source_frame from test_support.microcosm_build.uk_ladder_rowwise_clone import ( toy_ladder as toy_ladder, @@ -151,3 +152,56 @@ def test_prepare_refuses_checkpoint_drift_before_registering_a_full_build( with pytest.raises(ValueError, match="identity mismatch|SHA-256 mismatch"): cli.prepare_full_build(args) assert not args.out.exists() + + +def test_prepare_release_candidate_requires_the_registered_supports( + checkpoint_request, monkeypatch +): + """Under ``--release-candidate`` the operator's support pins must be the + sources.yaml rows (microcosm#932 round 1): a pin that proves the bytes are + the ones requested is not proof they are the ones reviewed.""" + import hashlib + from pathlib import Path + + args, sidecar_path, gates_path = checkpoint_request + + def sha(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + argv = [ + "--release-role", + "dense", + "--input-h5", + str(args.input_h5), + "--input-sha256", + sha(Path(args.input_h5)), + "--input-sidecar", + str(sidecar_path), + "--input-spine-gates", + str(gates_path), + "--ladder", + str(args.ladder), + "--ladder-sha256", + sha(Path(args.ladder)), + "--ledger-facts", + str(args.ledger_facts), + "--out", + str(args.out), + "--release-candidate", + ] + pins = {} + for system, label in cli._SUPPORT_ARGUMENTS.items(): + path = Path(getattr(args, f"atomic_support_{label}")) + pins[system] = {"sha256": sha(path), "size_bytes": path.stat().st_size} + argv += [ + f"--atomic-support-{label}", + str(path), + f"--atomic-support-sha256-{label}", + pins[system]["sha256"], + ] + release = cli.parse_args(argv) + with pytest.raises(ValueError, match="sources.yaml"): + cli.prepare_full_build(release) + patch_support_register(monkeypatch, pins) + prepared = cli.prepare_full_build(release) + assert prepared.bindings["geography"]["support_pins"] == pins diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_calibration_graph.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_calibration_graph.py index 5fb4c2947..38b9e4f13 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_calibration_graph.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_calibration_graph.py @@ -431,7 +431,7 @@ def test_blocking_preflight_refuses_before_dense_or_checkpoint_work(imported): # decoding an imported solution, just as it must before optimization. artifacts["imported_dense"] = SimpleNamespace(payload=b"must not read") with pytest.raises(ValueError, match="refused by the source preflight"): - UKDenseSolveKernel().run(SimpleNamespace(artifacts=artifacts)) + UKDenseSolveKernel().run(SimpleNamespace(artifacts=artifacts, params={})) def test_size_kernels_forward_phased_epochs_to_the_registered_observer( diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_measure.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_measure.py index 55ee9587d..d6abd74ff 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_full_measure.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_full_measure.py @@ -68,6 +68,12 @@ def receipt(self): ) assert len(constructions) == 1 + # The resolution loop's own receipt rides the measure receipt, one per + # engine block; its shape is the resolver's, checked by its keys here. + resolution = receipt.pop("resolution") + assert [sorted(entry) for entry in resolution] == [ + ["attached", "provider", "rounds"] + ] assert receipt == { "mode": "stub", "engine_version": "test", @@ -77,6 +83,11 @@ def receipt(self): "national_inputs": 0, "local_metrics": {"constituency": 1, "la": 1}, "blocks": 1, + "target_materialization": { + "prepared_count": 0, + "skipped_count": 0, + "skipped": [], + }, "cgt_period_contract": cgt_period_contract, } assert set(local_metrics) == {"constituency", "la"} @@ -223,6 +234,7 @@ def receipt(self): full_measure, "resolve_target_measures", lambda _factory, _registry, provider, **kwargs: SimpleNamespace( + receipt={"attached": {}, "provider": {}, "rounds": []}, measure_inputs={ ("person", "region"): np.zeros(provider.frame.n("person")), ("household", "raw_engine_input"): provider.frame.table("household")[ @@ -231,7 +243,7 @@ def receipt(self): ("household", "ons/corporate_land_value"): np.ones( provider.frame.n("household") ), - } + }, ), ) @@ -241,7 +253,10 @@ def materialize(adapter, _registry, **kwargs): assert "region" in adapter.tables["person"] table = adapter.tables["household"] table["prepared_count"] = table["raw_engine_input"].to_numpy() - return SimpleNamespace(skipped=()) + return SimpleNamespace( + skipped=(), + report=lambda: {"prepared_count": 0, "skipped_count": 0, "skipped": []}, + ) monkeypatch.setattr(full_measure, "materialize_uk_ledger_targets", materialize) prepared, restore, national, metrics, receipt = ( @@ -272,3 +287,51 @@ def materialize(adapter, _registry, **kwargs): assert receipt["block_sensitivity"]["present_in_this_run"] == [ "ons/corporate_land_value" ] + + +def test_scratch_paths_are_recorded_relative_to_the_scratch_root(monkeypatch, tmp_path): + """A scratch-mode resolver names the file it simulated from, under the + per-run temporary directory; the measure receipt records that path + relative to the scratch root, so two identical resolutions in different + scratch directories yield the same receipt (the measure and national + problem nodes are declared deterministic; #1057 round 1).""" + import json + + frame = source_frame() + + class StubResolver: + def __init__(self, **kwargs): + self.scratch_dir = kwargs["scratch_dir"] + self.simulation = object() + self.contract_targets = {} + + def receipt(self): + return { + "mode": "scratch_frame_export", + "source_path": str(self.scratch_dir / "simulation-input.h5"), + "policyengine_uk_version": "test", + } + + monkeypatch.setattr( + full_measure, + "compute_household_metrics", + lambda _simulation, area_type, *, household_ids, **_kwargs: pd.DataFrame( + {f"{area_type}_metric": np.ones(len(household_ids))}, + index=household_ids, + ), + ) + registry = TargetRegistry([], country="uk") + receipts = [] + for name in ("first", "second"): + _, _, _, _, receipt = full_measure.resolve_uk_full_measures( + frame, + registry, + period=2025, + scratch_dir=tmp_path / name, + resolver_factory=StubResolver, + ) + receipts.append(receipt) + assert receipts[0] == receipts[1] + provider = receipts[0]["resolution"][0]["provider"] + assert provider["source_path"] == "simulation-input.h5" + assert str(tmp_path) not in json.dumps(receipts[0]) diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_geography_sources.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_geography_sources.py index 4198efafd..9a60e2beb 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_geography_sources.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_geography_sources.py @@ -822,3 +822,51 @@ def test_build_complete_crosswalk_drops_noncanonical_stale_ni_rows() -> None: "N20000001", "N20000002", } + + +def test_ew_lad_lookup_accepts_only_the_april_2023_code_columns() -> None: + """``LAD23CD`` and ``LAD24CD`` carry the April 2023 code set; a later + vintage (``LAD25CD``, the recode follow-up) is refused rather than + relabelled, and two accepted names are refused rather than guessed + (microcosm#932 round 1).""" + normalise = geography_sources._normalise_ew_lad_lookup + accepted = normalise(pd.DataFrame({"OA21CD": ["E0001"], "LAD24CD": ["E06000063"]})) + assert accepted["lad23_code"].tolist() == ["E06000063"] + with pytest.raises(ValueError, match="missing column"): + normalise(pd.DataFrame({"OA21CD": ["E0001"], "LAD25CD": ["E08000038"]})) + with pytest.raises(ValueError, match="more than one LAD code column"): + normalise( + pd.DataFrame( + { + "OA21CD": ["E0001"], + "LAD22CD": ["E06000063"], + "LAD23CD": ["E06000063"], + "LAD24CD": ["E06000063"], + } + ) + ) + # An older, unaccepted name beside the accepted one is simply ignored. + ignored = normalise( + pd.DataFrame( + {"OA21CD": ["E0001"], "LAD22CD": ["E06000001"], "LAD23CD": ["E06000063"]} + ) + ) + assert ignored["lad23_code"].tolist() == ["E06000063"] + + +def test_ew_oa_lad_region_lookup_refuses_a_later_lad_vintage(monkeypatch) -> None: + rows = "OA21CD,LAD25CD,RGN24CD\nE00000001,E08000038,E12000003\n" + monkeypatch.setattr( + geography_sources, "_read_url_bytes", lambda url: rows.encode("utf-8") + ) + with pytest.raises(ValueError, match="missing OA, LAD or region"): + geography_sources.load_ew_oa_lad_region_lookup("https://example.invalid/oa") + accepted = "OA21CD,LAD24CD,RGN24CD\nE00000001,E08000038,E12000003\n" + monkeypatch.setattr( + geography_sources, "_read_url_bytes", lambda url: accepted.encode("utf-8") + ) + monkeypatch.setattr(geography_sources, "ENGLAND_WALES_OA2021_COUNT", 1) + lookup = geography_sources.load_ew_oa_lad_region_lookup( + "https://example.invalid/oa" + ) + assert lookup["la_code"].tolist() == ["E08000038"] diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_graph_evidence.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_graph_evidence.py index 429103b4c..7ad40ac2f 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_graph_evidence.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_graph_evidence.py @@ -256,3 +256,53 @@ def test_sample_admission_carries_the_transferred_gate_posture(synthetic_smoke): ) assert sample.params["spine_gate_synthetic_smoke"] is synthetic_smoke assert gate.params["synthetic_smoke"] is synthetic_smoke + + +@pytest.mark.parametrize("synthetic_smoke", [False, True]) +def test_sample_admission_holds_under_the_default_atomic_law(synthetic_smoke): + """The same admission composed under the default law (microcosm#932): the + identity-keyed assignment follows the sample node and does not move the + transferred-gate parameters the sample admission reads.""" + from microcosm.build.uk_runtime.atomic_area_support import ( + uk_atomic_assignment_definition, + ) + from microcosm.build.uk_runtime.graph_population import append_uk_population_nodes + from test_support.microcosm_build.uk_atomic_support_fixtures import ( + toy_support_payloads, + ) + from test_support.microcosm_build.uk_full_population_graph import SOURCE_VINTAGE + + country = load_country_spec("uk") + spine = uk_spine_graph(country) + gated = add_uk_spine_gate_nodes( + spine, + spec=country, + engine_identity="test-engine", + synthetic_smoke=synthetic_smoke, + ) + graph = append_uk_population_nodes( + gated, + population=uk_spine_endpoint(spine).population, + time_period="2024", + weight_kind="importance", + sample_fraction=0.1, + n_clones=2, + seed=7, + atomic_geography_definition=uk_atomic_assignment_definition( + toy_support_payloads(), seed=7 + ), + source_vintage=SOURCE_VINTAGE, + ) + gate = graph.node("spine.gates.transferred") + sample = graph.node("uk.full.sample") + assert {edge.producer for edge in sample.artifact_inputs} >= {gate.id} + assert sample.params["spine_gate_phase"] == "transferred" + assert sample.params["spine_gate_release_candidate"] is bool( + gate.params["release_candidate"] + ) + assert sample.params["spine_gate_synthetic_smoke"] is synthetic_smoke + assert graph.node("uk.full.identity").kernel == "uk.full.identity@1" + compiled = compile_graph(graph) + assert compiled.order.index("uk.full.sample") < compiled.order.index( + "uk.full.identity" + ) diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_local_release_preflight.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_local_release_preflight.py index 6dc4720ae..7e36f7795 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_local_release_preflight.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_local_release_preflight.py @@ -10,8 +10,16 @@ import pytest +from microcosm.build.uk_runtime import country_adapter +from test_support.microcosm_build.uk_full_build_cli import ( + patch_support_register, + synthetic_geography_binding, +) from test_support.paths import paths_for +#: The committed register reader, kept before the autouse fixture replaces it. +_REAL_REGISTER = country_adapter.uk_atomic_support_register + _TEST_PATHS = paths_for("microcosm-build") @@ -118,6 +126,7 @@ def _good_candidate(tmp_path: Path) -> Path: } }, "identity": {"spine": {"pin_verified": True}, "ladder": {"pin_verified": True}}, + "geography": {"assignment": synthetic_geography_binding()}, "outputs": { "dataset": {"path": str(h5), "sha256": hashlib.sha256(b"h5").hexdigest()} }, @@ -137,6 +146,12 @@ def _trusted_key(monkeypatch) -> None: monkeypatch.setenv("MICROCOSM_UK_TERMINAL_GATE_SIGNING_KEY", KEY) +@pytest.fixture(autouse=True) +def _registered_supports(monkeypatch) -> None: + """The synthetic candidates bind synthetic support pins; register them.""" + patch_support_register(monkeypatch) + + def _signed_report() -> dict: """A complete schema-4 local report signed the way the battery signs.""" @@ -289,6 +304,59 @@ def test_each_missing_flag_is_named(tmp_path: Path, mutate, needle: str) -> None assert any(needle in failure for failure in failures), failures +@pytest.mark.parametrize( + "mutate, needle", + [ + ( + lambda m: m["geography"]["assignment"].__setitem__("assignment", "legacy"), + "geography.assignment.assignment is 'legacy', not 'atomic'", + ), + ( + lambda m: m["geography"]["assignment"]["support_pins"][ + "uk_ni_data_zone_2021" + ].__setitem__("sha256", "9" * 64), + "support_pins.uk_ni_data_zone_2021 is not the registered support", + ), + ( + lambda m: m["geography"]["assignment"]["support_pins"][ + "uk_ew_output_area_2021" + ].__setitem__("size_bytes", 1), + "support_pins.uk_ew_output_area_2021 is not the registered support", + ), + (lambda m: m.pop("geography"), "geography.assignment.assignment is None"), + ], +) +def test_preflight_requires_the_registered_atomic_assignment( + tmp_path: Path, mutate, needle: str +) -> None: + """A release candidate is the identity-keyed assignment on the registered + supports (microcosm#932 round 1): the pre-flight refuses the legacy law, a + pre-#932 manifest with no binding, and a pin that is not the register's.""" + candidate = _good_candidate(tmp_path) + path = candidate / "rowwise_candidate_manifest.json" + manifest = json.loads(path.read_text()) + mutate(manifest) + path.write_text(json.dumps(manifest)) + failures = _load().check_candidate_dir(candidate, today=date(2026, 9, 4)) + assert any(needle in line for line in failures), failures + + +def test_preflight_reads_the_committed_support_register( + tmp_path: Path, monkeypatch +) -> None: + """Against sources.yaml itself the synthetic pins are not the register's.""" + monkeypatch.setattr(country_adapter, "uk_atomic_support_register", _REAL_REGISTER) + candidate = _good_candidate(tmp_path) + failures = _load().check_candidate_dir(candidate, today=date(2026, 9, 4)) + refused = [line for line in failures if "support_pins" in line] + assert len(refused) == 3, failures + register = _REAL_REGISTER() + assert set(register) == set(synthetic_geography_binding()["support_pins"]) + assert all( + len(row["sha256"]) == 64 and row["size_bytes"] > 0 for row in register.values() + ) + + def test_env_check_names_the_missing_key_and_pins(tmp_path: Path) -> None: module = _load() pins = tmp_path / "pins.txt" diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_national_calibration.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_national_calibration.py index 461fa2519..6c7f391fc 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_national_calibration.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_national_calibration.py @@ -1,9 +1,19 @@ -"""The UK national Ledger-backed calibration stage.""" +"""The UK national register on the graph's national problem route. + +The in-process calibration stage retired with the national release role's +move onto the graph; these tests exercise the same route as a library: +``encode_uk_national_problem`` (materialise, compile, bind the doctrine), +the shared solve under the bindings, and ``national_calibration_manifest`` +(the seam's evidence block). +""" from __future__ import annotations import json +import tempfile from importlib import resources as importlib_resources +from pathlib import Path +from types import SimpleNamespace import numpy as np import pandas as pd @@ -21,25 +31,27 @@ UK_NATIONAL_TARGET_LOSS_CAP, UK_NATIONAL_TARGET_WEIGHT_RULE, UKNationalSolveDoctrine, - national_calibration, uk_doctrine_with_overrides, uk_national_target_loss_weights, ) +from microcosm.build.uk_runtime.graph_national import ( + encode_uk_national_problem, + materialize_uk_national_rows, + national_calibration_manifest, +) from microcosm.build.uk_runtime.ledger_targets import ( - UKLedgerTargetCompilation, _uk_contract_targets, ) from microcosm.build.uk_runtime.national_calibration import ( - UKNationalCalibrationStage, - _post_solve_calibration_record, national_calibration_mass_reason, ) from microcosm.build.uk_runtime.national_frame import ( validate_uk_national_frame, write_uk_national_frame, ) -from microcosm.calibrate import TargetRegistry, TargetSpec -from microcosm.frame import EntitySchema, Frame, WeightKind, Weights +from microcosm.calibrate import TargetRegistry, TargetSpec, calibrate +from microcosm.calibrate.artifacts import decode_problem +from microcosm.frame import EntitySchema, Frame, MassChange, WeightKind, Weights ACTIVE_REFERENCE_COUNT = 1124 @@ -382,192 +394,188 @@ def _materialization_binding_frame( ) -def test_uc_calibration_compiles_and_moves_weighted_count_towards_fact() -> None: - frame = _frame() - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), +def _solve(frame, registry, doctrine, *, resolver_factory=None, band_edge=None): + """The graph's national route as a library call: the problem the node + encodes, the solve the shared dense node runs on its bindings, the + calibrated population and the seam's evidence block.""" + payload = encode_uk_national_problem( + frame, + national_registry=registry, + band_edge_registry=registry if band_edge is None else band_edge, period=2025, - doctrine=UKNationalSolveDoctrine(epochs=200, learning_rate=0.05), + doctrine={ + "target_weight_rule": doctrine.target_weight_rule, + "target_loss_cap": doctrine.target_loss_cap, + "max_weight_ratio": doctrine.max_weight_ratio, + "l0_lambda": doctrine.l0_lambda, + "mass_rule": doctrine.mass_rule, + "scale_rule": doctrine.scale_rule, + }, + resolver_factory=resolver_factory, + scratch_dir=None if resolver_factory is None else Path(tempfile.mkdtemp()), + ) + problem = decode_problem(payload) + binding = problem.bindings + result = calibrate( + frame, + problem.to_target_set(), + weight_entity="household", + epochs=doctrine.epochs, + learning_rate=doctrine.learning_rate, + seed=doctrine.seed, + mass="free", + mass_reason=binding["mass_reason"], + max_weight_ratio=binding["max_weight_ratio"], + target_loss_weights=np.asarray( + binding["target_loss_weights"], dtype=np.float64 + ), + target_loss_cap=binding["target_loss_cap"], + ) + calibrated = frame.with_weights( + "household", + Weights(result.weights, WeightKind.CALIBRATED), + mass=MassChange( + factor=float(result.weights.sum()) + / float(frame.weights_for("household").total), + reason=binding["mass_reason"], + ), + ) + manifest = national_calibration_manifest( + result, + calibrated, + problem=problem, + doctrine={ + **binding, + "epochs": doctrine.epochs, + "learning_rate": doctrine.learning_rate, + "seed": doctrine.seed, + }, + ) + return SimpleNamespace( + result=result, + frame=calibrated, + problem=problem, + manifest=manifest, + diagnostics=[ + { + "name": row.name, + "estimate": row.final_estimate, + "target": row.target, + "relative_error": row.relative_error, + } + for row in result.diagnostics + ], ) - result = stage(frame) + +def test_uc_calibration_compiles_and_moves_weighted_count_towards_fact() -> None: + frame = _frame() + solved = _solve( + frame, _registry(), UKNationalSolveDoctrine(epochs=200, learning_rate=0.05) + ) before = 20.0 - after = float(result.weights_for("household").values[:2].sum()) + after = float(solved.frame.weights_for("household").values[:2].sum()) assert abs(after - 30.0) < abs(before - 30.0) - assert stage.manifest["activated_reference_count"] == 1 - assert stage.manifest["resolved_reference_count"] == 1 - assert stage.manifest["matrix_target_count"] == 1 - assert stage.diagnostics[0]["target"] == 30.0 - assert result.weights_for("household").kind is WeightKind.CALIBRATED - assert len(result.mass_log) == 1 - mass_change = stage.manifest["weights"]["calibration_mass_change"] + manifest = solved.manifest + assert manifest["activated_reference_count"] == 1 + assert manifest["resolved_reference_count"] == 1 + assert manifest["matrix_target_count"] == 1 + assert solved.diagnostics[0]["target"] == 30.0 + assert solved.frame.weights_for("household").kind is WeightKind.CALIBRATED + assert len(solved.frame.mass_log) == 1 + mass_change = manifest["weights"]["calibration_mass_change"] assert mass_change["entity"] == "household" assert "National doctrine calibration" in mass_change["reason"] - assert stage.manifest["weights"]["household_weight_kind_chain"] == [ + assert manifest["weights"]["household_weight_kind_chain"] == [ {"stage": "staging", "kind": "design"}, {"stage": "national_calibration", "kind": "calibrated"}, ] - assert stage.manifest["weights"]["mass_log_records_before_calibration"] == 0 - assert stage.manifest["weights"]["mass_log_records"] == 1 - assert stage.manifest["solve"]["n_targets"] == 1 - assert stage.manifest["solve"]["n_households"] == 4 + assert manifest["weights"]["mass_log_records_before_calibration"] == 0 + assert manifest["weights"]["mass_log_records"] == 1 + assert manifest["solve"]["n_targets"] == 1 + assert manifest["solve"]["n_households"] == 4 + assert manifest["parameters"]["doctrine"]["target_weight_rule"] == "family_equal" -def test_stage_forwards_solver_progress_and_complete_lifecycle() -> None: - progress: list[dict[str, object]] = [] - stages: list[tuple[str, str, dict[str, object]]] = [] - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - progress_callback=progress.append, - stage_callback=lambda stage_id, status, details: stages.append( - (stage_id, status, dict(details)) - ), - ) - - stage(_frame()) - - assert progress - assert all(event["kind"] == "calibration_epoch" for event in progress) - assert [(stage_id, status) for stage_id, status, _details in stages] == [ - ("calibration_input_validation", "started"), - ("calibration_input_validation", "completed"), - ("measure_resolution", "started"), - ("measure_resolution", "completed"), - ("target_materialization", "started"), - ("target_materialization", "completed"), - ("solver_preparation", "started"), - ("solver_preparation", "completed"), - ("solver_execution", "started"), - ("solver_execution", "completed"), - ("calibration_result_validation", "started"), - ("calibration_result_validation", "completed"), - ("calibration_evidence_construction", "started"), - ("calibration_evidence_construction", "completed"), - ] - assert stages[-1][2]["diagnostic_count"] == 1 - assert all(details["elapsed_seconds"] >= 0 for _, _, details in stages) - - -def test_uc_calibration_stage_accepts_benunit_grain_reference_on_nested_frame() -> None: +def test_uc_calibration_accepts_benunit_grain_reference_on_nested_frame() -> None: frame = _nested_frame() - stage = UKNationalCalibrationStage( - _registry(value=60.0), - band_edge_registry=_registry(value=60.0), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) + solved = _solve(frame, _registry(value=60.0), UKNationalSolveDoctrine(epochs=5)) - result = stage(frame) - - assert stage.manifest["activated_reference_count"] == 1 - assert stage.manifest["resolved_reference_count"] == 1 - assert stage.manifest["matrix_target_count"] == 1 - assert stage.diagnostics[0]["target"] == 60.0 - assert stage.diagnostics[0]["estimate"] == pytest.approx(60.0) - validate_uk_national_frame(result) + assert solved.manifest["activated_reference_count"] == 1 + assert solved.manifest["resolved_reference_count"] == 1 + assert solved.manifest["matrix_target_count"] == 1 + assert solved.diagnostics[0]["target"] == 60.0 + assert solved.diagnostics[0]["estimate"] == pytest.approx(60.0) + validate_uk_national_frame(solved.frame) -def test_stage_measure_resolver_injects_columns_then_restores_pristine_output() -> None: +def test_measure_resolver_injects_columns_then_restores_pristine_output() -> None: frame = _frame_without_uc_column() resolver = StubMeasureResolver() original_columns = { entity: set(frame.table(entity).columns) for entity in frame.entities } - stage = UKNationalCalibrationStage( + solved = _solve( + frame, _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - measure_resolver=resolver, + UKNationalSolveDoctrine(epochs=5), + resolver_factory=lambda **kwargs: resolver, ) - result = stage(frame) - assert resolver.calls == [("benunit", "universal_credit")] - assert stage.manifest["measure_resolution"]["provider"] == {"provider": "stub_uc"} - assert stage.manifest["measure_resolution"]["attached"] == { - "benunit.universal_credit": "stub_uc" - } + # The seam manifest's block: which measure came from which provider, the + # rounds and the provider's receipt; the materialisation report is its + # own field. + resolution = solved.manifest["measure_resolution"] + assert resolution["provider"] == {"provider": "stub_uc"} + assert resolution["attached"] == {"benunit.universal_credit": "stub_uc"} + assert solved.manifest["target_materialization"]["prepared_count"] == 1 for entity in frame.entities: - assert set(result.table(entity).columns) == original_columns[entity] - assert "universal_credit" not in result.table("benunit") - validate_uk_national_frame(result) + assert set(solved.frame.table(entity).columns) == original_columns[entity] + assert "universal_credit" not in solved.frame.table("benunit") + validate_uk_national_frame(solved.frame) -def test_stage_manifest_omits_measure_resolution_without_resolver() -> None: - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) +def test_manifest_omits_measure_resolution_without_resolver() -> None: + solved = _solve(_frame(), _registry(), UKNationalSolveDoctrine(epochs=5)) - stage(_frame()) + assert "measure_resolution" not in solved.manifest + assert solved.problem.bindings["measure_resolution"]["mode"] == "frame_only" - assert "measure_resolution" not in stage.manifest +def test_materialization_threads_band_edge_registry(monkeypatch) -> None: + from microcosm.build.uk_runtime import ledger_targets -def test_stage_threads_band_edge_registry_to_materialization(monkeypatch) -> None: captured = [] - real_materialize = national_calibration.materialize_uk_ledger_targets + real_materialize = ledger_targets.materialize_uk_ledger_targets def capture_materialize(*args, **kwargs): captured.append(kwargs) return real_materialize(*args, **kwargs) monkeypatch.setattr( - national_calibration, - "materialize_uk_ledger_targets", - capture_materialize, + ledger_targets, "materialize_uk_ledger_targets", capture_materialize ) sentinel = TargetRegistry([], country="uk") - stage = UKNationalCalibrationStage( + materialize_uk_national_rows( + _frame(), _registry(), period=2025, - doctrine=UKNationalSolveDoctrine(epochs=1), band_edge_registry=sentinel, + resolver_factory=None, ) - stage(_frame()) - assert captured[-1]["band_edge_registry"] is sentinel - # The parameter is required, never defaulted: a stage cannot tell a + # The parameter is required, never defaulted: the route cannot tell a # pruned registry from a full one (#803 review finding 1). with pytest.raises(TypeError, match="band_edge_registry"): - UKNationalCalibrationStage( - _registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=1), + materialize_uk_national_rows( + _frame(), _registry(), period=2025, resolver_factory=None ) -def test_activated_unresolvable_compiled_reference_aborts_loudly() -> None: - stage = UKNationalCalibrationStage( - UKLedgerTargetCompilation( - registry=TargetRegistry([], country="uk"), - unsupported=( - { - "name": "dwp.uc.households", - "period": "2025", - "reason": "did not match a Ledger fact selector", - }, - ), - ), - band_edge_registry=TargetRegistry([], country="uk"), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=1), - ) - - with pytest.raises(RuntimeError, match="did not match a Ledger fact selector"): - stage(_frame()) - - def test_chronicle_184_uc_and_obr_references_compile_fail_closed() -> None: from microcosm.build.country_spec import load_country_spec from microcosm.build.ledger_targets import compile_ledger_target_references @@ -640,7 +648,7 @@ def test_chronicle_184_uc_and_obr_references_compile_fail_closed() -> None: assert {spec.name for spec in registry.specs} == {"obr.universal_credit_in_cap"} -def test_packaged_binding_classes_materialize_through_national_stage() -> None: +def test_packaged_binding_classes_materialize_through_the_national_route() -> None: selected_names = ( "dwp.uc.households", "obr.esa", @@ -670,25 +678,23 @@ def test_packaged_binding_classes_materialize_through_national_stage() -> None: assert headline.metadata["ledger_member_fact_count"] == "120" resolver = StubCrosstabResolver() resolver.contract_targets = _uk_contract_targets() - stage = UKNationalCalibrationStage( - registry, - band_edge_registry=registry, - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=1, learning_rate=0.01), - measure_resolver=resolver, - ) - input_frame = _materialization_binding_frame() original_columns = { entity: set(input_frame.table(entity).columns) for entity in input_frame.entities } - result = stage(input_frame) + solved = _solve( + input_frame, + registry, + UKNationalSolveDoctrine(epochs=1, learning_rate=0.01), + resolver_factory=lambda **kwargs: resolver, + ) + result = solved.frame - assert stage.manifest["activated_reference_count"] == len(selected_names) - assert stage.manifest["resolved_reference_count"] == len(selected_names) - assert stage.manifest["matrix_target_count"] == len(selected_names) + assert solved.manifest["activated_reference_count"] == len(selected_names) + assert solved.manifest["resolved_reference_count"] == len(selected_names) + assert solved.manifest["matrix_target_count"] == len(selected_names) # The staged frame stays writer-clean: no prepared scratch column survives # onto the returned tables (adjudicated lifecycle; slash-named scratch # crashes the HDFStore staging writer). Original input columns — including @@ -724,7 +730,7 @@ def test_packaged_binding_classes_materialize_through_national_stage() -> None: assert adapter.tables[entity][measure].tolist() == expected -def test_packaged_materialization_skip_aborts_national_stage() -> None: +def test_packaged_materialization_skip_aborts_the_national_route() -> None: from microcosm.build.ledger_targets import compile_ledger_target_references reference = _reference_by_name("hmrc.salary_sacrifice.it_relief_basic_rate") @@ -733,27 +739,20 @@ def test_packaged_materialization_skip_aborts_national_stage() -> None: [reference], country="uk", ) - stage = UKNationalCalibrationStage( - registry, - band_edge_registry=registry, - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=1), - ) - with pytest.raises(RuntimeError, match="could not materialize every"): - stage(_materialization_binding_frame(include_counterfactual_delta=False)) + materialize_uk_national_rows( + _materialization_binding_frame(include_counterfactual_delta=False), + registry, + period=2025, + band_edge_registry=registry, + resolver_factory=None, + ) def test_calibration_preserves_entity_ids_and_national_integrity() -> None: frame = _frame() - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - - result = stage(frame) + solved = _solve(frame, _registry(), UKNationalSolveDoctrine(epochs=5)) + result = solved.frame for entity in frame.entities: id_column = f"{entity}_id" @@ -761,80 +760,12 @@ def test_calibration_preserves_entity_ids_and_national_integrity() -> None: validate_uk_national_frame(result) -def test_checkpoint_metadata_round_trips_calibration_evidence() -> None: - frame = _frame() - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - - staged = stage(frame) - metadata = json.loads(json.dumps(stage.checkpoint_metadata())) - - resumed = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - resumed.resume_from_checkpoint(metadata, staged) - - assert resumed.manifest == stage.manifest - assert resumed.diagnostics == stage.diagnostics - assert resumed.output_content_identity == metadata["output_content_identity"] - - drifted = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - with pytest.raises(RuntimeError, match="drifted record"): - drifted.resume_from_checkpoint(metadata, frame) - - empty = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - with pytest.raises(RuntimeError, match="calibration counts"): - empty.resume_from_checkpoint({}, staged) - - missing_count = dict(metadata) - missing_count["calibration"] = { - key: value - for key, value in metadata["calibration"].items() - if key != "activated_reference_count" - } - with pytest.raises(RuntimeError, match="calibration counts"): - empty.resume_from_checkpoint(missing_count, staged) - - unrun = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) - with pytest.raises(RuntimeError, match="has not run"): - unrun.checkpoint_metadata() - - def test_prepared_slash_columns_are_not_returned_to_the_writer(tmp_path) -> None: pytest.importorskip("tables") - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - ) + solved = _solve(_frame(), _registry(), UKNationalSolveDoctrine(epochs=5)) - result = stage(_frame()) - - assert "dwp/uc/households" not in result.table("benunit") - write_uk_national_frame(result, tmp_path / "staging.h5") + assert "dwp/uc/households" not in solved.frame.table("benunit") + write_uk_national_frame(solved.frame, tmp_path / "staging.h5") def test_national_calibration_mass_reason_is_canonical() -> None: @@ -848,8 +779,10 @@ def test_national_calibration_mass_reason_is_canonical() -> None: national_calibration_mass_reason([]) -def test_post_solve_fence_requires_calibrated_kind_and_mass_record() -> None: +def test_evidence_block_requires_calibrated_kind_and_the_calibration_record() -> None: + solved = _solve(_frame(), _registry(), UKNationalSolveDoctrine(epochs=5)) before = _frame() + doctrine = solved.manifest["parameters"]["doctrine"] uncalibrated = Frame( {entity: before.table(entity) for entity in before.entities}, before.schema, @@ -858,23 +791,35 @@ def test_post_solve_fence_requires_calibrated_kind_and_mass_record() -> None: mass_log=before.mass_log, metadata=before.metadata, ) - with pytest.raises(RuntimeError, match="not 'calibrated'"): - _post_solve_calibration_record(before, uncalibrated, before_count=0) + national_calibration_manifest( + solved.result, uncalibrated, problem=solved.problem, doctrine=doctrine + ) calibrated_without_record = Frame( {entity: before.table(entity) for entity in before.entities}, before.schema, - {"household": Weights(np.full(4, 10.0), WeightKind.CALIBRATED)}, + {"household": Weights(solved.result.weights, WeightKind.CALIBRATED)}, before.strata, mass_log=before.mass_log, metadata=before.metadata, ) - with pytest.raises(RuntimeError, match="exactly one mass record"): - _post_solve_calibration_record( - before, + with pytest.raises(RuntimeError, match="append one mass record"): + national_calibration_manifest( + solved.result, calibrated_without_record, - before_count=0, + problem=solved.problem, + doctrine=doctrine, + ) + + foreign_record = calibrated_without_record.with_weights( + "household", + Weights(solved.result.weights, WeightKind.CALIBRATED), + mass=MassChange(factor=1.0, reason="a resample"), + ) + with pytest.raises(RuntimeError, match="not the calibration record"): + national_calibration_manifest( + solved.result, foreign_record, problem=solved.problem, doctrine=doctrine ) @@ -985,45 +930,35 @@ def test_national_doctrine_rejects_tampered_bounds() -> None: ("rule", "expected"), [("uniform", None), ("family_equal", [1.0])], ) -def test_doctrine_target_weight_rule_reaches_the_solver( - monkeypatch, rule, expected -) -> None: +def test_doctrine_target_weight_rule_reaches_the_solver(rule, expected) -> None: """The declared rule must reach calibrate(), not just the manifest echo. First-armed-run finding (2026-08-23): the doctrine declared a target_weight_rule the stage never passed to the kernel, so the solve - silently ran uniform whatever the doctrine said. The doctrine vector now - travels explicitly; "uniform" maps to None — the kernel's own default — - so the shipped identity is unchanged under the default rule. + silently ran uniform whatever the doctrine said. On the graph the + doctrine vector is bound into the ordered problem, which the shared + dense node hands to calibrate(); "uniform" is bound as the explicit + all-ones vector (the kernel's own equal weighting written out). """ - from microcosm.calibrate import calibrate as real_calibrate - - captured: dict[str, object] = {} - - def capturing_calibrate(*args, **kwargs): - captured["target_loss_weights"] = kwargs["target_loss_weights"] - return real_calibrate(*args, **kwargs) - - monkeypatch.setattr( - "microcosm.build.uk_runtime.national_calibration.calibrate", - capturing_calibrate, - ) - stage = UKNationalCalibrationStage( - _registry(), + payload = encode_uk_national_problem( + _frame(), + national_registry=_registry(), band_edge_registry=_registry(), period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5, target_weight_rule=rule), + doctrine={ + "target_weight_rule": rule, + "target_loss_cap": 10.0, + "max_weight_ratio": 10.0, + "l0_lambda": 0.0, + "mass_rule": "free", + "scale_rule": "default_target_loss_scales", + }, + resolver_factory=None, ) - - stage(_frame()) - - weights = captured["target_loss_weights"] - if expected is None: - assert weights is None - else: - assert weights is not None - assert weights.tolist() == expected + bindings = decode_problem(payload).bindings + assert bindings["target_weight_rule"] == rule + assert bindings["target_loss_weights"] == ([1.0] if expected is None else expected) def test_measure_resolution_never_touches_the_source_frame() -> None: @@ -1156,38 +1091,3 @@ def test_packaged_uc_headline_refuses_incomplete_month_cell_fixture(defect) -> N with pytest.raises(ValueError, match="monthly window"): compile_ledger_target_references(facts, [reference], country="uk") - - -def test_stage_attributes_failure_to_the_operation_actually_executing( - monkeypatch: pytest.MonkeyPatch, -) -> None: - stages: list[tuple[str, str]] = [] - stage = UKNationalCalibrationStage( - _registry(), - band_edge_registry=_registry(), - period=2025, - doctrine=UKNationalSolveDoctrine(epochs=5), - stage_callback=lambda stage_id, status, details: stages.append( - (stage_id, status) - ), - ) - - def fail_materialization(*args, **kwargs): - raise RuntimeError("materialization failed") - - monkeypatch.setattr( - "microcosm.build.uk_runtime.national_calibration.materialize_uk_ledger_targets", - fail_materialization, - ) - - with pytest.raises(RuntimeError, match="materialization failed"): - stage(_frame()) - - assert stages == [ - ("calibration_input_validation", "started"), - ("calibration_input_validation", "completed"), - ("measure_resolution", "started"), - ("measure_resolution", "completed"), - ("target_materialization", "started"), - ("target_materialization", "failed"), - ] diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_national_graph.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_national_graph.py new file mode 100644 index 000000000..110b0ba1b --- /dev/null +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_national_graph.py @@ -0,0 +1,498 @@ +"""The national release role on the graph: kernels, composition, evidence. + +These tests stand on the calibration seam suite's synthetic frame, sidecar +and register (``test_support.microcosm_build.uk_calibration_run``): the same +inputs the seam solved, now bound as a spine checkpoint and solved through +``uk.full.national_targets`` → ``uk.full.national_problem`` → +``uk.full.dense`` → ``uk.full.calibrated`` → ``uk.full.gates.calibrated``. +The register materialises from the frame's own columns (no engine), the +seam's ``measure_resolver=None`` route. +""" + +from __future__ import annotations + +import json +from types import SimpleNamespace + +import numpy as np +import pytest + +from microcosm.build.uk_runtime import calibration_run, full_targets, graph_national +from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS, +) +from microcosm.build.uk_runtime.graph_build import ( + UKBoundSpineKernel, + bound_spine_graph, +) +from microcosm.build.uk_runtime.graph_calibration import register_uk_calibration_kernels +from microcosm.build.uk_runtime.graph_national import ( + NATIONAL_GATES_NODE, + NATIONAL_PROBLEM_NODE, + NATIONAL_TARGETS_NODE, + UKNationalBuildConfig, + register_uk_national_kernels, + uk_national_graph, +) +from microcosm.build.uk_runtime.national_calibration import ( + CalibrationFrameAdapter, + national_calibration_mass_reason, +) +from microcosm.build.uk_runtime.national_doctrine import ( + UKNationalSolveDoctrine, + uk_doctrine_with_overrides, + uk_national_target_loss_weights, +) +from microcosm.build.uk_runtime.national_frame import write_uk_national_frame +from microcosm.calibrate import calibrate +from microcosm.calibrate.artifacts import decode_problem +from microcosm.frame import WeightKind +from microcosm.graph import ContentStore, KernelRegistry, compile_graph, run_graph +from test_support.microcosm_build import uk_calibration_run as seam +from test_support.microcosm_build.uk_calibration_run import ( # noqa: F401 + _cgt_projection, + _signing_key, +) + + +@pytest.fixture(autouse=True) +def _admin_totals(monkeypatch): + monkeypatch.setattr( + calibration_run, + "uk_aggregate_admin_totals", + lambda frame, manifest: (seam._admin_anchor_values(), []), + ) + + +def _patch_target_inputs(monkeypatch, registry): + """Stand in for the pinned Chronicle artifact: the fixture register.""" + artifact = SimpleNamespace( + facts=None, + facts_sha256="1" * 64, + manifest_sha256="2" * 64, + provenance=lambda: { + "facts_sha256": "1" * 64, + "manifest_sha256": "2" * 64, + "artifact_id": "synthetic-national-fixture", + }, + ) + monkeypatch.setattr( + full_targets, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact + ) + monkeypatch.setattr( + full_targets, + "require_committed_uk_chronicle_feed_pin", + lambda facts_sha256, **kwargs: SimpleNamespace( + to_dict=lambda: {"facts_sha256": "1" * 64, "manifest_sha256": "2" * 64} + ), + ) + monkeypatch.setattr( + full_targets, + "compile_uk_target_registry", + lambda facts, target_period: SimpleNamespace(registry=registry, unsupported=()), + ) + monkeypatch.setattr( + full_targets, "load_uk_calibration_measure_exclusions", lambda p: () + ) + monkeypatch.setattr( + full_targets, + "apply_uk_calibration_measure_exclusions", + lambda reg, exclusions, now=None: (reg, {}), + ) + monkeypatch.setattr( + full_targets, "_ledger_provenance", lambda artifact: {"artifact_id": "fixture"} + ) + return artifact + + +def _bound_inputs(tmp_path, frame): + input_h5 = tmp_path / "spine.h5" + write_uk_national_frame(frame, input_h5) + sidecar_path, report_path, _sidecar = seam._bound_checkpoint(tmp_path, frame) + ledger = tmp_path / "ledger" + ledger.mkdir() + (ledger / "consumer_facts.jsonl").write_text("{}\n") + return { + "uk_spine": input_h5, + "uk_spine_evidence": sidecar_path, + "uk_spine_gates": report_path, + "uk_ledger_facts": ledger, + } + + +def _config(**overrides): + doctrine, receipt = uk_doctrine_with_overrides(epochs=5, **overrides) + return UKNationalBuildConfig( + calibration_year=2025, + time_period="2023", + source_year=2023, + doctrine=doctrine, + doctrine_overrides=receipt, + ) + + +def _registry_for_run(): + kernels = KernelRegistry() + kernels.register(UKBoundSpineKernel()) + register_uk_calibration_kernels(kernels) + register_uk_national_kernels(kernels, resolver_factory=None) + return kernels + + +def _run(tmp_path, monkeypatch, config=None, *, endpoint=None): + pytest.importorskip("tables") + frame = seam._frame() + registry = seam._registry() + _patch_target_inputs(monkeypatch, registry) + sources = _bound_inputs(tmp_path, frame) + national = uk_national_graph( + config or _config(), + spine=bound_spine_graph(frame), + spine_population="uk.full.spine_checkpoint", + review_date="2026-09-29", + ) + graph = national.graph + if endpoint is not None: + compiled = compile_graph(graph) + needed, pending = {endpoint}, [endpoint] + while pending: + for parent in compiled.predecessors[pending.pop()]: + if parent not in needed: + needed.add(parent) + pending.append(parent) + from dataclasses import replace + + graph = replace( + graph, nodes=tuple(node for node in graph.nodes if node.id in needed) + ) + store = ContentStore(tmp_path / "store") + manifest = run_graph( + compile_graph(graph), + sources=sources, + store=store, + kernels=_registry_for_run(), + ) + return national, manifest, store, frame, registry + + +def _payload(manifest, store, node, artifact): + return store.load_bytes(manifest.nodes[node].opaque_artifacts[artifact]) + + +def test_national_graph_composes_the_bound_checkpoint_line(): + frame = seam._frame() + national = uk_national_graph( + _config(), + spine=bound_spine_graph(frame), + spine_population="uk.full.spine_checkpoint", + review_date="2026-09-29", + ) + compiled = compile_graph(national.graph) + assert list(compiled.order) == [ + "uk.full.spine_checkpoint", + NATIONAL_TARGETS_NODE, + NATIONAL_PROBLEM_NODE, + "uk.full.dense", + "uk.full.calibrated", + NATIONAL_GATES_NODE, + ] + dense = national.graph.node("uk.full.dense") + # The national doctrine drives the shared solve node; its admission is the + # bound spine's provenance, not a preflight battery. + assert dense.params == { + "epochs": 5, + "learning_rate": 0.02, + "seed": 0, + "admission": "bound_spine", + } + assert [item.name for item in dense.artifact_inputs] == [ + "problem", + "spine_provenance", + ] + problem = national.graph.node(NATIONAL_PROBLEM_NODE) + assert problem.params["target_weight_rule"] == "family_equal" + assert problem.params["target_loss_cap"] == 10.0 + assert problem.params["max_weight_ratio"] == 10.0 + assert {source.name for source in national.graph.sources} == { + "uk_spine", + "uk_spine_evidence", + "uk_spine_gates", + "uk_ledger_facts", + } + inventory = national.operation_inventory() + assert inventory["release_role"] == "national" + assert inventory["configuration"]["doctrine"]["epochs"] == 5 + assert inventory["configuration"]["doctrine_overrides"] == { + "epochs": {"default": 1500, "effective": 5} + } + assert national.population == "uk.full.calibrated" + + +def test_national_graph_refuses_a_population_without_bound_provenance(): + frame = seam._frame() + spine = bound_spine_graph(frame) + from dataclasses import replace + + unbound = replace( + spine, + nodes=tuple(replace(node, artifact_outputs=()) for node in spine.nodes), + ) + with pytest.raises(ValueError, match="bound spine checkpoint population"): + uk_national_graph( + _config(), + spine=unbound, + spine_population="uk.full.spine_checkpoint", + review_date="2026-09-29", + ) + + +def test_national_problem_binds_the_seam_doctrine(tmp_path, monkeypatch): + national, manifest, store, frame, registry = _run( + tmp_path, monkeypatch, endpoint=NATIONAL_PROBLEM_NODE + ) + targets = json.loads(_payload(manifest, store, NATIONAL_TARGETS_NODE, "registry")) + assert targets["kind"] == "uk_national_target_registry" + assert targets["calibration_year"] == 2025 + assert targets["register_completeness"]["approved_reference_count"] == 1 + assert targets["chronicle_feed_pin"] == { + "facts_sha256": "1" * 64, + "manifest_sha256": "2" * 64, + } + problem = decode_problem( + _payload(manifest, store, NATIONAL_PROBLEM_NODE, "problem") + ) + assert problem.entity_ids == tuple(frame.table("household")["household_id"]) + row_name = registry.specs[0].to_target().row_name + assert problem.problem.names == (row_name,) + bindings = problem.bindings + families = [spec.family for spec in registry.specs] + assert bindings["release_role"] == "national" + assert bindings["mass_reason"] == national_calibration_mass_reason(families) + assert ( + bindings["target_loss_weights"] + == uk_national_target_loss_weights(families, rule="family_equal").tolist() + ) + assert bindings["target_weight_rule"] == "family_equal" + assert bindings["target_loss_cap"] == 10.0 + assert bindings["max_weight_ratio"] == 10.0 + assert bindings["mass_rule"] == "free" + # The dense node solves under free mass, the default scales and no L0 + # penalty; the doctrine the problem binds is one it honours. + from microcosm.build.uk_runtime.graph_calibration import ( + refuse_unhonoured_solve_doctrine, + ) + + refuse_unhonoured_solve_doctrine(bindings) + assert bindings["bound_families"] == ["national/dwp_universal_credit"] + assert bindings["register_sha256"] == registry.version + assert bindings["measure_resolution"]["mode"] == "frame_only" + assert bindings["measure_resolution"]["target_materialization"] == { + "prepared_count": 1, + "skipped_count": 0, + "skipped": [], + } + assert problem.target_metadata[0]["materialization"] == "uk_national_measure" + assert problem.target_metadata[0]["geography_level"] == "country" + # The matrix is the seam's: the register materialised on the adapter and + # compiled row for row. + adapter = CalibrationFrameAdapter(frame) + from microcosm.build.uk_runtime.ledger_targets import materialize_uk_ledger_targets + from microcosm.calibrate import build_constraint_matrix + + materialize_uk_ledger_targets( + adapter, registry, period=2025, band_edge_registry=registry + ) + expected = build_constraint_matrix( + adapter.prepared_frame(), registry.to_target_set(), "household" + ) + assert np.array_equal(problem.problem.matrix.toarray(), expected.matrix.toarray()) + assert np.array_equal(problem.problem.target_vector, expected.target_vector) + + +def test_national_solve_is_the_seam_solve(tmp_path, monkeypatch): + national, manifest, store, frame, registry = _run( + tmp_path, monkeypatch, endpoint="uk.full.calibrated" + ) + calibrated = manifest.population("uk.full.calibrated") + weights = calibrated.weights_for("household") + assert weights.kind is WeightKind.CALIBRATED + # The seam solved the same register on the same frame with the same + # doctrine: an in-process calibrate call under the national doctrine + # reproduces the graph's weights exactly. + doctrine = UKNationalSolveDoctrine(epochs=5) + adapter = CalibrationFrameAdapter(frame) + from microcosm.build.uk_runtime.ledger_targets import materialize_uk_ledger_targets + + materialize_uk_ledger_targets( + adapter, registry, period=2025, band_edge_registry=registry + ) + families = [spec.family for spec in registry.specs] + result = calibrate( + adapter.prepared_frame(), + registry.to_target_set(), + weight_entity="household", + epochs=doctrine.epochs, + learning_rate=doctrine.learning_rate, + mass=doctrine.mass_rule, + mass_reason=national_calibration_mass_reason(families), + max_weight_ratio=doctrine.max_weight_ratio, + seed=doctrine.seed, + l0_lambda=doctrine.l0_lambda, + target_loss_cap=doctrine.target_loss_cap, + target_loss_weights=uk_national_target_loss_weights( + families, rule=doctrine.target_weight_rule + ), + ) + assert np.array_equal(weights.values, result.weights) + # The calibration mass record is the seam's, appended once. + assert len(calibrated.mass_log) == len(frame.mass_log) + 1 + record = calibrated.mass_log[-1] + assert record.reason == national_calibration_mass_reason(families) + assert record.entity == "household" + assert calibrated.mass_log[:-1] == frame.mass_log + + +def test_national_gates_evaluate_the_seam_scope_and_record_the_evidence( + tmp_path, monkeypatch +): + national, manifest, store, frame, registry = _run(tmp_path, monkeypatch) + report = json.loads(_payload(manifest, store, NATIONAL_GATES_NODE, "gate_report")) + assert report["kind"] == "uk_national_gate_report" + assert report["posture"] == "calibration_seam" + assert report["policy_suffix"] == "calibration_seam_scope" + assert report["scope"] == list(UK_CALIBRATION_GATE_SCOPE) + assert set(report["scope_exclusions"]) == set(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS) + outcomes = {row["id"]: row for row in report["report"]["outcomes"]} + assert set(outcomes) == set(UK_CALIBRATION_GATE_SCOPE) + assert report["blocking"] == [] + assert report["artifact_permitted"] is True + assert manifest.nodes[NATIONAL_GATES_NODE].receipt["outcome"] == "pass" + evidence = json.loads( + _payload(manifest, store, NATIONAL_GATES_NODE, "calibration_evidence") + ) + calibration = evidence["calibration"] + assert calibration["activated_reference_count"] == 1 + assert calibration["resolved_reference_count"] == 1 + assert calibration["matrix_target_count"] == 1 + assert calibration["max_weight_ratio_bound"] == 10.0 + assert calibration["weights"]["household_weight_kind"] == "calibrated" + assert calibration["weights"]["household_weight_kind_chain"] == [ + {"stage": "staging", "kind": "design"}, + {"stage": "national_calibration", "kind": "calibrated"}, + ] + assert calibration["weights"]["mass_log_records_before_calibration"] == 0 + assert calibration["weights"]["mass_log_records"] == 1 + assert calibration["weights"]["calibration_mass_change"]["entity"] == "household" + assert calibration["solve"]["n_targets"] == 1 + assert calibration["solve"]["n_households"] == 4 + assert calibration["parameters"]["doctrine"]["target_weight_rule"] == "family_equal" + assert calibration["parameters"]["doctrine"]["epochs"] == 5 + assert calibration["target_materialization"]["prepared_count"] == 1 + assert "measure_resolution" not in calibration + row_name = registry.specs[0].to_target().row_name + assert evidence["diagnostics"][0]["name"] == row_name + assert evidence["target_geography_levels"] == {row_name: "country"} + assert evidence["register"] == { + "country": "uk", + "version": registry.version, + "compiled_count": 1, + "excluded_count": 0, + "calibrated_count": 1, + } + + +def test_national_problem_refuses_an_empty_register(tmp_path, monkeypatch): + from microcosm.calibrate import TargetRegistry + + pytest.importorskip("tables") + frame = seam._frame() + _patch_target_inputs(monkeypatch, TargetRegistry([], country="uk")) + sources = _bound_inputs(tmp_path, frame) + national = uk_national_graph( + _config(), + spine=bound_spine_graph(frame), + spine_population="uk.full.spine_checkpoint", + review_date="2026-09-29", + ) + with pytest.raises(Exception, match="selects no target"): + run_graph( + compile_graph(national.graph), + sources=sources, + store=ContentStore(tmp_path / "store"), + kernels=_registry_for_run(), + ) + + +def test_dense_kernel_refuses_bound_spine_admission_without_provenance(): + from microcosm.build.uk_runtime.graph_calibration import UKDenseSolveKernel + + context = SimpleNamespace(params={"admission": "bound_spine"}, artifacts={}) + with pytest.raises(ValueError, match="bound spine's provenance"): + UKDenseSolveKernel._admit(context) + with pytest.raises(ValueError, match="Unknown dense calibration admission"): + UKDenseSolveKernel._admit( + SimpleNamespace(params={"admission": "x"}, artifacts={}) + ) + # The dense line's admission is unchanged: absent parameter, preflight required. + with pytest.raises(ValueError, match="source preflight artifact"): + UKDenseSolveKernel._admit(SimpleNamespace(params={}, artifacts={})) + + +def test_graph_national_module_has_no_seam_stage_dependency(): + import inspect + + source = inspect.getsource(graph_national) + assert "UKNationalCalibrationStage" not in source + assert "run_uk_calibration" not in source + + +def test_dense_solve_refuses_a_bound_doctrine_it_cannot_honour(): + """``mass_rule``, ``scale_rule`` and ``l0_lambda`` are recorded by the + national problem as the doctrine the solve ran under; the dense node + refuses any value other than the one it hard-codes (#1057 round 1).""" + from microcosm.build.uk_runtime.graph_calibration import ( + UK_DENSE_SOLVE_FIXED_DOCTRINE, + refuse_unhonoured_solve_doctrine, + ) + + refuse_unhonoured_solve_doctrine({}) + refuse_unhonoured_solve_doctrine(dict(UK_DENSE_SOLVE_FIXED_DOCTRINE)) + refuse_unhonoured_solve_doctrine({"l0_lambda": 0}) + for key, value in ( + ("mass_rule", "pinned"), + ("scale_rule", "unit"), + ("l0_lambda", 0.5), + ): + with pytest.raises(ValueError, match=key): + refuse_unhonoured_solve_doctrine( + {**UK_DENSE_SOLVE_FIXED_DOCTRINE, key: value} + ) + + +def test_national_gate_node_reads_its_scope_from_the_posture(): + from microcosm.build.uk_runtime.rowwise_posture import UK_ROWWISE_NATIONAL_POSTURE + + national = uk_national_graph( + _config(), + spine=bound_spine_graph(seam._frame()), + spine_population="uk.full.spine_checkpoint", + review_date="2026-09-29", + ) + node = national.graph.node(NATIONAL_GATES_NODE) + assert json.loads(node.params["gate_scope"]) == list( + UK_ROWWISE_NATIONAL_POSTURE.gate_scope + ) + assert len(json.loads(node.params["gate_scope"])) == 7 + + +def test_national_run_config_refuses_an_empty_register(): + from microcosm.build.uk_runtime.rowwise_posture import UK_ROWWISE_NATIONAL_POSTURE + + with pytest.raises(ValueError, match="compiled national register"): + graph_national.national_run_config( + posture=UK_ROWWISE_NATIONAL_POSTURE, + config=_config(), + registry_document={"national_registry": {}}, + driver_parameters={}, + ) diff --git a/packages/microcosm-build/tests/engine_free/uk/test_uk_rowwise_national_role.py b/packages/microcosm-build/tests/engine_free/uk/test_uk_rowwise_national_role.py index 76797eb57..2439bbf0c 100644 --- a/packages/microcosm-build/tests/engine_free/uk/test_uk_rowwise_national_role.py +++ b/packages/microcosm-build/tests/engine_free/uk/test_uk_rowwise_national_role.py @@ -1,11 +1,13 @@ -"""The UK rowwise driver's national release role (microcosm#823). - -The national role delegates the build to the calibration seam library -through ``uk_runtime.national_role`` (the graph driver dispatches there -before any graph preparation), so these tests stand on the seam run suite's -synthetic frame, sidecar and register (loaded from that module) and on the -candidate suite's driver loader and fake Hub; the seam's consumers are -patched on ``national_role``. +"""The UK build driver's national release role (microcosm#823) on the graph. + +The national line solves the bound spine checkpoint as bound through +``uk_runtime.graph_national`` and materialises the seam-shaped evidence +(``build_record.json``, the signed calibration-seam gate report, the seam +diagnostics, the frozen registries, the schema-4 national manifest). These +tests run the driver end to end on the calibration seam suite's synthetic +frame and register (loaded from that module): the register materialises from +the frame's own columns (no engine), the Chronicle artifact is stood in for +on ``full_targets``, and the Hub is the candidate suite's fake. """ from __future__ import annotations @@ -17,42 +19,113 @@ from pathlib import Path from types import SimpleNamespace +import numpy as np import pytest from microcosm.build.logbook import load_spool_rows from microcosm.build.staging_v2 import validate_v2_bundle -from microcosm.build.uk_runtime import calibration_run, national_role, rowwise_staging -from microcosm.build.uk_runtime.calibration_run import UK_CALIBRATION_GATE_SCOPE +from microcosm.build.uk_runtime import ( + calibration_run, + full_targets, + graph_national, + national_role, + rowwise_staging, + spine_build, +) +from microcosm.build.uk_runtime.calibration_run import ( + UK_CALIBRATION_GATE_SCOPE, + UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS, +) from microcosm.build.uk_runtime.chronicle_feed import ( - UKChronicleFeedPinError, load_uk_chronicle_feed, require_committed_uk_chronicle_feed_pin, ) -from microcosm.build.uk_runtime.national_frame import write_uk_national_frame +from microcosm.build.uk_runtime.content_identity import uk_frame_content_identity +from microcosm.build.uk_runtime.national_calibration import ( + CalibrationFrameAdapter, + national_calibration_mass_reason, +) +from microcosm.build.uk_runtime.national_doctrine import ( + UKNationalSolveDoctrine, + uk_national_target_loss_weights, +) +from microcosm.build.uk_runtime.national_frame import ( + load_uk_national_frame, + write_uk_national_frame, +) from microcosm.build.uk_runtime.release_identity import UK_NATIONAL_RELEASE_ID -from microcosm.calibrate import TargetRegistry +from microcosm.calibrate import TargetRegistry, calibrate +from microcosm.frame import WeightKind from test_support.microcosm_build import uk_calibration_run as seam from test_support.microcosm_build import uk_rowwise_candidate as candidate +from test_support.microcosm_build.uk_calibration_run import ( # noqa: F401 + _cgt_projection, + _signing_key, +) def _sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() -class _FakeResolver: - """Stands in for the engine-backed resolver; the seam solves the fixture - register from the frame's own columns when the resolver is ``None``.""" - - def __init__(self, **kwargs): - self.kwargs = kwargs +def _bound_spine(tmp_path: Path, frame) -> Path: + """The seam fixture frame as a bound checkpoint: H5, sidecar, gate report.""" + input_h5 = tmp_path / "spine.h5" + write_uk_national_frame(frame, input_h5) + report = { + **calibration_run.uk_spine_checkpoint_gate_digests(), + "blocked_at_phase": None, + "gates": { + gate_id: {"status": "passed", "criticality": "release_blocking"} + for gate_id in calibration_run.UK_SPINE_GATE_SCOPE + }, + } + report_path = input_h5.with_suffix(".spine_gates.json") + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n") + sidecar = { + "schema_version": 2, + "pipeline": "uk-frs-spine", + "stages": ["frs_spine", "was_wealth"], + "stage_records": [ + { + "stage": "was_wealth", + "produced": ["property_wealth"], + "nonzero_share": {"property_wealth": 1.0}, + "seconds": 0.1, + } + ], + "stage_evidence": { + "was_wealth": {"stage": "was_wealth", "support_clip": {"columns": {}}} + }, + "artifact_pins": {"person": "a" * 64}, + "input_artifact_pins": {"was_qrf_donor": {"sha256": "b" * 64}}, + "resource_pins": {"wealth.json": "c" * 64}, + "stage_artifact_pins": {"was_wealth": {"was_qrf_donor": "d" * 64}}, + "declared_seeds": {"was_wealth": {"was_wealth": 0}}, + "rules_engine": {"package": "policyengine-uk", "version": "unavailable"}, + "source_vintages": {"frs": "2024_25"}, + "stochastic_contract_sha256": "e" * 64, + "entity_row_counts": { + entity: int(len(frame.table(entity))) for entity in frame.entities + }, + "household_weight_kind": frame.weights_for("household").kind.value, + "household_weight_total": float(frame.weights_for("household").values.sum()), + "uk_frame_content_identity": uk_frame_content_identity(frame), + "spine_gate_report": {"sha256": _sha(report_path)}, + "fit_weight_records": {}, + } + input_h5.with_suffix(".build.json").write_text( + json.dumps(sidecar, indent=2, sort_keys=True) + "\n" + ) + return input_h5 -def _national_inputs(monkeypatch, tmp_path: Path, *, resolver=None): +def _national_inputs(monkeypatch, tmp_path: Path, *, registry=None): + """The synthetic national build: bound frame, stood-in Chronicle artifact, + no engine (the register materialises from the frame's own columns).""" frame = seam._frame() - input_h5 = tmp_path / "spine.h5" - write_uk_national_frame(frame, input_h5) - seam._write_spine_sidecar(input_h5, frame) - registry = seam._registry() + input_h5 = _bound_spine(tmp_path, frame) + registry = seam._registry() if registry is None else registry ledger_dir = tmp_path / "ledger" ledger_dir.mkdir() (ledger_dir / "consumer_facts.jsonl").write_text("{}\n") @@ -71,30 +144,38 @@ def _national_inputs(monkeypatch, tmp_path: Path, *, resolver=None): to_dict=lambda: {"facts_sha256": "1" * 64, "manifest_sha256": "2" * 64} ) monkeypatch.setattr( - national_role, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact + full_targets, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact ) monkeypatch.setattr( - national_role, + full_targets, "require_committed_uk_chronicle_feed_pin", lambda facts_sha256, **kwargs: pin, ) monkeypatch.setattr( - national_role, + full_targets, "compile_uk_target_registry", lambda facts, target_period: SimpleNamespace(registry=registry, unsupported=()), ) monkeypatch.setattr( - national_role, "load_uk_calibration_measure_exclusions", lambda p: () + full_targets, "load_uk_calibration_measure_exclusions", lambda p: () ) monkeypatch.setattr( - national_role, + full_targets, "apply_uk_calibration_measure_exclusions", - lambda reg, exclusions: (reg, {}), + lambda reg, exclusions, now=None: (reg, {}), + ) + monkeypatch.setattr( + full_targets, + "_ledger_provenance", + lambda artifact: {"artifact_id": "synthetic-national-fixture"}, ) + # No engine: the national problem node takes the seam's frame-only route. + monkeypatch.setattr(graph_national, "UKMeasureResolver", None) + monkeypatch.setattr(spine_build, "_rules_engine", lambda: None) monkeypatch.setattr( - national_role, - "UKMeasureResolver", - (lambda **kwargs: None) if resolver is None else resolver, + spine_build, + "_rules_engine_provenance", + lambda: {"package": "policyengine-uk", "version": "test"}, ) monkeypatch.setattr( calibration_run, @@ -125,116 +206,186 @@ def _argv(input_h5: Path, out: Path, *extra: str) -> list[str]: ] -def _fake_seam_run(calls: list[dict]): - def fake_run(**kwargs): - calls.append(kwargs) - paths = kwargs["paths"] - paths.staging_h5.write_bytes(b"h5") - paths.diagnostics_json.write_text( - json.dumps( - { - "targets": [ - { - "name": "dwp.uc.households", - "target_name": "dwp.uc.households", - "relative_error": 0.05, - } - ] - } - ) - ) - paths.terminal_gate_json.write_text( - json.dumps( - { - "release_id": kwargs["release_id"], - "gates": { - gate: {"status": "passed"} for gate in UK_CALIBRATION_GATE_SCOPE - }, - } - ) - ) - record = { - "build_id": kwargs["build_id"], - "spine_provenance": {"spine_gate_report": {"sha256": "f" * 64}}, - "calibration": { - "weights": {"household_weight_kind": "calibrated"}, - "solve": { - "n_targets": 1, - "n_households": 4, - "initial_loss": 0.5, - "final_loss": 0.01, - "n_nonzero": 4, - }, - "effective_sample_size": 4.0, - "max_weight_ratio": 1.0, - "measure_resolution": None, - }, - } - paths.build_record_json.write_text(json.dumps(record)) - return SimpleNamespace( - build_record_sha256=_sha(paths.build_record_json), build_record=record - ) +def _sums_verify(directory: Path) -> None: + for line in (directory / "sha256sums.txt").read_text().splitlines(): + digest, name = line.split(" ", 1) + assert _sha(directory / name) == digest, name - return fake_run +def _seam_weights(frame, registry, doctrine: UKNationalSolveDoctrine) -> np.ndarray: + """An in-process calibrate call under the national doctrine: the seam's solve.""" + from microcosm.build.uk_runtime.ledger_targets import materialize_uk_ledger_targets -def test_uk_national_role_delegates_to_the_seam_library(monkeypatch, tmp_path): + adapter = CalibrationFrameAdapter(frame) + materialize_uk_ledger_targets( + adapter, registry, period=2025, band_edge_registry=registry + ) + families = [spec.family for spec in registry.specs] + result = calibrate( + adapter.prepared_frame(), + registry.to_target_set(), + weight_entity="household", + epochs=doctrine.epochs, + learning_rate=doctrine.learning_rate, + mass=doctrine.mass_rule, + mass_reason=national_calibration_mass_reason(families), + max_weight_ratio=doctrine.max_weight_ratio, + seed=doctrine.seed, + l0_lambda=doctrine.l0_lambda, + target_loss_cap=doctrine.target_loss_cap, + target_loss_weights=uk_national_target_loss_weights( + families, rule=doctrine.target_weight_rule + ), + ) + return result.weights + + +def test_uk_national_role_builds_through_the_graph_and_stages_locally( + monkeypatch, tmp_path, capsys +): pytest.importorskip("tables") builder = candidate._load_builder_module() - input_h5, registry, artifact, pin = _national_inputs( - monkeypatch, tmp_path, resolver=_FakeResolver - ) - calls: list[dict] = [] - monkeypatch.setattr(national_role, "run_uk_calibration", _fake_seam_run(calls)) + input_h5, registry, _artifact, pin = _national_inputs(monkeypatch, tmp_path) out = tmp_path / "national" - assert builder.main(_argv(input_h5, out, "--no-staging")) == 0 - - call = calls[0] - assert call["build_id"].startswith("uk-frs-calibration-attempt-") - assert call["paths"].input_h5 == input_h5 - assert call["paths"].staging_h5 == out / "microcosm_uk_2024_25.h5" - assert call["paths"].diagnostics_json == out / "calibration_diagnostics.json" - assert call["paths"].build_record_json == out / "build_record.json" assert ( - call["paths"].terminal_gate_json - == out / "microcosm_uk_2024_25.terminal_gates.json" + builder.main(_argv(input_h5, out, "--staging-local-only", "--epochs", "5")) == 0 ) - # The role's doctrine, with no overrides: the seam's own constants. - doctrine = call["doctrine"] - assert (doctrine.epochs, doctrine.learning_rate, doctrine.seed) == (1500, 0.02, 0) - assert doctrine.target_weight_rule == "family_equal" - assert call["doctrine_overrides"] == {} - assert call["release_id"] == UK_NATIONAL_RELEASE_ID - assert call["register_registry"] is registry - assert call["band_edge_registry"] is registry - assert call["calibration_year"] == 2025 - assert call["exclusion_receipt"] == {} - assert call["ledger_artifact"] is artifact - # The seam's direct_h5 resolver: the input file, never a live frame. - resolver = call["measure_resolver"] - assert isinstance(resolver, _FakeResolver) - assert resolver.kwargs["simulation_source"] == input_h5 - assert resolver.kwargs["frame"] is None - assert resolver.kwargs["year"] == 2025 - assert call["source_pins"]["input_h5"]["sha256"] == _sha(input_h5) - assert call["source_pins"]["ledger_facts"]["sha256"] == "1" * 64 - extra = call["run_config_extra"] - assert extra["release_role"] == "national" - assert extra["allow_unpinned_feed"] is False - assert extra["chronicle_feed_pin"] == pin.to_dict() - assert extra["rowwise_driver_parameters"]["release_role"] == "national" - assert extra["rowwise_driver_parameters"]["n_clones"] is None - assert call["staging_delivery"]["mode"] == "disabled" - assert call["staging_finalizer"] is None - assert call["progress_callback"] is None manifest = json.loads((out / builder.MANIFEST_FILENAME).read_text()) + assert json.loads(capsys.readouterr().out) == manifest + run_id = manifest["build_id"] + assert run_id.startswith("uk-frs-calibration-attempt-") + # The seam's output names, from the posture. + dataset = out / "microcosm_uk_2024_25.h5" + gates = out / "microcosm_uk_2024_25.terminal_gates.json" + diagnostics = out / "calibration_diagnostics.json" + for path in ( + dataset, + gates, + diagnostics, + out / "build_record.json", + out / "national_target_registry.json", + out / "national_contract_registry.json", + out / "target_support_manifest.json", + out / "build.json", + out / "graph.json", + out / "operations.json", + out / "build.graph.json", + ): + assert path.is_file(), path.name + assert not (out / "failure.json").exists() + + # The build record the release-cut certifier reads: the seam's schema. + record = json.loads((out / "build_record.json").read_text()) + assert record["schema_version"] == 1 + assert record["build_id"] == run_id + assert record["pipeline"] == "uk-frs-calibration" + run_config = record["run_config"] + assert run_config["pipeline"] == "uk-frs-calibration" + assert run_config["release_id"] == UK_NATIONAL_RELEASE_ID + assert run_config["release_role"] == "national" + assert run_config["register_sha256"] == registry.version + assert run_config["band_edge_register_sha256"] == registry.version + assert run_config["calibration_year"] == 2025 + assert run_config["doctrine_overrides"] == { + "epochs": {"default": 1500, "effective": 5} + } + assert run_config["doctrine"]["target_weight_rule"] == "family_equal" + assert run_config["doctrine"]["seed"] == 0 + assert run_config["allow_unpinned_feed"] is False + assert run_config["chronicle_feed_pin"] == pin.to_dict() + assert run_config["rowwise_driver_parameters"]["n_clones"] is None + assert record["source_pins"]["input_h5"]["sha256"] == _sha(input_h5) + assert record["input_posture"] == { + "tier": "staging_candidate", + "sha256": _sha(input_h5), + "size_bytes": input_h5.stat().st_size, + } + assert record["register"] == { + "country": "uk", + "version": registry.version, + "compiled_count": 1, + "excluded_count": 0, + "calibrated_count": 1, + } + assert record["artifacts"]["staging_h5"]["sha256"] == _sha(dataset) + assert record["artifacts"]["staging_h5"]["path"] == str(dataset) + assert record["artifacts"]["diagnostics_json"]["sha256"] == _sha(diagnostics) + assert record["artifacts"]["terminal_gate_json"]["sha256"] == _sha(gates) + assert record["certification"] == { + "expected_artifact": str(dataset.with_suffix(".release_certification.json")), + "producer": "tools/certify_uk_release_cut.py", + } + provenance = record["spine_provenance"] + assert provenance["spine_gate_report"]["sha256"] == _sha( + input_h5.with_suffix(".spine_gates.json") + ) + assert provenance["stages"] == ["frs_spine", "was_wealth"] + assert provenance["artifact_pins"] == {"person": "a" * 64} + calibration = record["calibration"] + assert calibration["solve"]["n_targets"] == 1 + assert calibration["solve"]["n_households"] == 4 + assert calibration["weights"]["household_weight_kind"] == "calibrated" + assert calibration["parameters"]["doctrine"]["epochs"] == 5 + assert record["gate_summary"] == { + gate_id: "passed" for gate_id in UK_CALIBRATION_GATE_SCOPE + } + assert record["staging_delivery"]["mode"] == "local_only" + assert set(record["graph"]["artifacts"]) >= { + "uk.full.spine_checkpoint", + "uk.full.national_targets", + "uk.full.national_problem", + "uk.full.dense", + "uk.full.gates.calibrated", + "uk.full.national.readback", + } + assert record["graph"]["readback"]["passed"] is True + + # The seam diagnostics with the build block, digest measured from the file. + written = json.loads(diagnostics.read_text()) + assert written["build"]["build_id"] == run_id + assert written["build"]["spine_provenance"] == provenance + assert written["build"]["doctrine"]["epochs"] == 5 + + # The signed calibration-seam gate report. + report = json.loads(gates.read_text()) + assert report["posture"] == "calibration_seam" + assert set(report["scope_exclusions"]) == set(UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS) + assert set(report["gates"]) == set(UK_CALIBRATION_GATE_SCOPE) + assert report["blocked_at_phase"] is None + assert report["release_candidate"] is False + assert report["release_evidence"] == { + "calibration_diagnostics_sha256": _sha(diagnostics) + } + attestation = report["attestation"] + signature = attestation["signature"] + attestation["signature"] = None + key = b"0123456789abcdef0123456789abcdef" + import hmac + + from microcosm.build.gate_battery import _canonical_json_bytes + + assert ( + hmac.new(key, _canonical_json_bytes(report), hashlib.sha256).hexdigest() + == signature + ) + + # The H5: calibrated weights, the seam's mass record. + staged, _ = load_uk_national_frame(dataset) + assert staged.weights_for("household").kind is WeightKind.CALIBRATED + assert len(staged.mass_log) == 1 + assert staged.mass_log[0].reason == national_calibration_mass_reason( + [spec.family for spec in registry.specs] + ) + frozen = TargetRegistry.from_json(out / "national_target_registry.json") + assert frozen.version == registry.version + + # The schema-4 national manifest. assert manifest["schema_version"] == 4 assert manifest["build_kind"] == "uk_national_calibrated_candidate" assert manifest["release_role"] == "national" assert manifest["release_id"] == UK_NATIONAL_RELEASE_ID - assert manifest["build_id"] == call["build_id"] assert set(manifest["outputs"]) == { "dataset", "calibration_diagnostics", @@ -243,20 +394,16 @@ def test_uk_national_role_delegates_to_the_seam_library(monkeypatch, tmp_path): "national_target_registry", "national_contract_registry", } - assert manifest["outputs"]["national_contract_registry"]["sha256"] == _sha( - out / "national_contract_registry.json" - ) - assert manifest["evaluation"] == { - "status": "not_requested", - "note": ( - "no --incumbent-h5: the rule-1 score receipt the release-cut " - "certifier needs was not produced; score the candidate with " - "tools/score_uk_national_candidate.py before certification." - ), - } + assert manifest["outputs"]["dataset"]["sha256"] == _sha(dataset) + assert manifest["outputs"]["dataset"]["path"] == str(dataset) assert manifest["outputs"]["build_record"]["sha256"] == _sha( out / "build_record.json" ) + assert manifest["build_record"]["sha256"] == _sha(out / "build_record.json") + assert manifest["outputs"]["national_contract_registry"]["sha256"] == _sha( + out / "national_contract_registry.json" + ) + assert manifest["evaluation"]["status"] == "not_requested" assert manifest["solve"]["n_targets_by_kind"] == { "national": 1, "local": 0, @@ -271,6 +418,7 @@ def test_uk_national_role_delegates_to_the_seam_library(monkeypatch, tmp_path): assert manifest["fit"]["national_by_family"][0]["family"] == "dwp_universal_credit" assert manifest["gate"]["scope"] == list(UK_CALIBRATION_GATE_SCOPE) assert manifest["gate"]["posture"] == "calibration_seam" + assert set(manifest["gate"]["statuses"]) == set(UK_CALIBRATION_GATE_SCOPE) assert manifest["failing_gate_ids"] == [] assert manifest["releasable"] is False assert manifest["release_posture"]["calibration_seam_gates_passed"] is True @@ -278,84 +426,8 @@ def test_uk_national_role_delegates_to_the_seam_library(monkeypatch, tmp_path): manifest["release_posture"]["shippable_by"] == "tools/certify_uk_release_cut.py" ) assert manifest["staged_dataset"]["status"] == "skipped" - assert manifest["staging_delivery"]["mode"] == "disabled" - frozen = TargetRegistry.from_json(out / "national_target_registry.json") - assert frozen.version == registry.version - - # Explicit solve flags are receipted overrides of the seam doctrine. - calls.clear() - assert ( - builder.main( - _argv( - input_h5, - tmp_path / "override", - "--no-staging", - "--epochs", - "5", - "--target-weight-rule", - "uniform", - "--target-loss-cap", - "4", - ) - ) - == 0 - ) - assert calls[0]["doctrine_overrides"] == { - "epochs": {"default": 1500, "effective": 5}, - "target_loss_cap": {"default": 10.0, "effective": 4.0}, - "target_weight_rule": {"default": "family_equal", "effective": "uniform"}, - } - - -def _sums_verify(directory: Path) -> None: - for line in (directory / "sha256sums.txt").read_text().splitlines(): - digest, name = line.split(" ", 1) - assert _sha(directory / name) == digest, name - - -def test_uk_national_role_builds_the_seam_evidence_and_stages_locally( - monkeypatch, tmp_path, capsys -): - pytest.importorskip("tables") - builder = candidate._load_builder_module() - input_h5, registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) - out = tmp_path / "national" - - assert ( - builder.main(_argv(input_h5, out, "--staging-local-only", "--epochs", "5")) == 0 - ) - - manifest = json.loads((out / builder.MANIFEST_FILENAME).read_text()) - assert json.loads(capsys.readouterr().out) == manifest - run_id = manifest["build_id"] - record = json.loads((out / "build_record.json").read_text()) - assert record["build_id"] == run_id - assert record["pipeline"] == "uk-frs-calibration" - assert record["run_config"]["release_id"] == UK_NATIONAL_RELEASE_ID - assert record["run_config"]["release_role"] == "national" - assert record["run_config"]["doctrine_overrides"] == { - "epochs": {"default": 1500, "effective": 5} - } - assert record["run_config"]["doctrine"]["target_weight_rule"] == "family_equal" - # The bindings the release-cut certifier reads. - dataset = out / "microcosm_uk_2024_25.h5" - gates = out / "microcosm_uk_2024_25.terminal_gates.json" - diagnostics = out / "calibration_diagnostics.json" - assert record["artifacts"]["staging_h5"]["sha256"] == _sha(dataset) - assert record["artifacts"]["diagnostics_json"]["sha256"] == _sha(diagnostics) - assert record["artifacts"]["terminal_gate_json"]["sha256"] == _sha(gates) - assert record["spine_provenance"]["spine_gate_report"]["sha256"] - assert json.loads(diagnostics.read_text())["build"]["build_id"] == run_id - assert record["staging_delivery"]["mode"] == "local_only" - assert record["staging_delivery"] == manifest["staging_delivery"] - assert manifest["outputs"]["dataset"]["sha256"] == _sha(dataset) - assert manifest["outputs"]["build_record"]["sha256"] == _sha( - out / "build_record.json" - ) - assert manifest["build_record"]["sha256"] == _sha(out / "build_record.json") - assert set(manifest["gate"]["statuses"]) == set(UK_CALIBRATION_GATE_SCOPE) - assert manifest["solve"]["n_targets"] == 1 - assert manifest["staged_dataset"]["status"] == "skipped" + assert manifest["staging_delivery"] == record["staging_delivery"] + assert manifest["graph"]["readback"]["passed"] is True _sums_verify(out) bundle = validate_v2_bundle(out / "staging", run_id) @@ -367,16 +439,18 @@ def test_uk_national_role_builds_the_seam_evidence_and_stages_locally( stage_ids = {event["stage_id"] for event in bundle["events"]} assert { "input_pinning", + "input_loading", "target_compilation", "calibration", + "diagnostics", + "target_support_sidecars", "release_check_evaluation", + "candidate_h5_creation", "build_record_creation", "dataset_staging", "complete", } <= stage_ids artifacts = out / "staging" / "runs" / run_id / "artifacts" - assert (artifacts / "fit_summary.json").is_file() - assert (artifacts / "staged_dataset.json").is_file() fit_summary = json.loads((artifacts / "fit_summary.json").read_text()) assert fit_summary["build_kind"] == "uk_national_calibrated_candidate" assert fit_summary["fit_by_family"]["local"] == {} @@ -386,6 +460,79 @@ def test_uk_national_role_builds_the_seam_evidence_and_stages_locally( assert len(rows) == 1 assert rows[0].build_id == run_id assert rows[0].pipeline == "uk-frs-calibration" + assert rows[0].disposition == "iterating" + phases = list(rows[0].phases_reached) + for phase in ( + "input_sidecar_bound", + "targets_bound", + "national_calibration_solved", + "diagnostics_written", + "calibration_gates_evaluated", + "staging_h5_written", + "build_record_written", + "published", + ): + assert phase in phases, phase + + +def test_uk_national_role_solve_is_the_seam_solve(monkeypatch, tmp_path): + """The graph's H5 carries the weights an in-process calibrate call under + the national doctrine produces on the same frame and register.""" + pytest.importorskip("tables") + builder = candidate._load_builder_module() + input_h5, registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) + out = tmp_path / "national" + + assert builder.main(_argv(input_h5, out, "--no-staging", "--epochs", "7")) == 0 + + staged, _ = load_uk_national_frame(out / "microcosm_uk_2024_25.h5") + expected = _seam_weights(seam._frame(), registry, UKNationalSolveDoctrine(epochs=7)) + assert np.array_equal(staged.weights_for("household").values, expected) + + +def test_uk_national_role_records_doctrine_overrides(monkeypatch, tmp_path): + pytest.importorskip("tables") + builder = candidate._load_builder_module() + input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) + out = tmp_path / "override" + + assert ( + builder.main( + _argv( + input_h5, + out, + "--no-staging", + "--epochs", + "5", + "--target-weight-rule", + "uniform", + "--target-loss-cap", + "4", + ) + ) + == 0 + ) + + record = json.loads((out / "build_record.json").read_text()) + assert record["run_config"]["doctrine_overrides"] == { + "epochs": {"default": 1500, "effective": 5}, + "target_loss_cap": {"default": 10.0, "effective": 4.0}, + "target_weight_rule": {"default": "family_equal", "effective": "uniform"}, + } + assert record["run_config"]["doctrine"]["target_weight_rule"] == "uniform" + assert record["run_config"]["doctrine"]["target_loss_cap"] == 4.0 + manifest = json.loads((out / builder.MANIFEST_FILENAME).read_text()) + assert manifest["solve"]["target_weight_rule"] == "uniform" + assert manifest["solve"]["target_weight_rule_override"] == { + "default": "family_equal", + "effective": "uniform", + } + operations = json.loads((out / "operations.json").read_text()) + problem = next( + node for node in operations["nodes"] if node["id"] == "uk.full.national_problem" + ) + assert problem["kernel"] == "uk.full.national_problem@1" + assert operations["configuration"]["doctrine"]["target_loss_cap"] == 4.0 def test_uk_national_role_publishes_telemetry_and_the_bundle_to_the_hub( @@ -483,30 +630,118 @@ def test_uk_national_dry_run_prints_the_plan_and_writes_nothing( assert plan["parameters"]["release_role"] == "national" assert plan["engine"] == "not_run" assert plan["releasable"] is False + # The compiled graph's inventory rides the plan. + assert [node["id"] for node in plan["graph"]["nodes"]] == [ + "uk.full.spine_checkpoint", + "uk.full.national_targets", + "uk.full.national_problem", + "uk.full.dense", + "uk.full.calibrated", + "uk.full.gates.calibrated", + ] assert not out.exists() def test_uk_national_role_marks_the_staging_run_failed_on_a_refusal( monkeypatch, tmp_path ): + """A register that does not compile refuses inside the targets node; the + driver records the failure sidecar, the failed row and the failed run.""" pytest.importorskip("tables") builder = candidate._load_builder_module() input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) monkeypatch.setattr( - national_role, + full_targets, "compile_uk_target_registry", lambda facts, target_period: SimpleNamespace( registry=None, unsupported=("dwp.uc.households",) ), ) out = tmp_path / "national" - with pytest.raises(SystemExit, match="failed to compile"): - builder.main(_argv(input_h5, out, "--staging-local-only")) + assert builder.main(_argv(input_h5, out, "--staging-local-only")) == 1 + failure = json.loads((out / "failure.json").read_text()) + assert "failed to compile" in failure["message"] runs = sorted(path.name for path in (out / "staging" / "runs").iterdir()) assert len(runs) == 1 bundle = validate_v2_bundle(out / "staging", runs[0]) assert bundle["progress"]["status"] == "failed" assert not (out / "build_record.json").exists() + rows = load_spool_rows(out / "logbook-spool") + assert len(rows) == 1 + assert rows[0].disposition == "failed" + assert rows[0].pipeline == "uk-frs-calibration" + + +def test_uk_national_role_refuses_an_occupied_output_directory(monkeypatch, tmp_path): + """The seam refused to overwrite an existing candidate artifact; the graph + driver keeps that refusal before any work, so a second run into the same + ``--out`` leaves the first candidate's bytes under its Logbook row.""" + pytest.importorskip("tables") + builder = candidate._load_builder_module() + input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) + out = tmp_path / "national" + assert builder.main(_argv(input_h5, out, "--no-staging", "--epochs", "5")) == 0 + first = { + name: _sha(out / name) + for name in ("microcosm_uk_2024_25.h5", "build_record.json", "build.json") + } + head = load_spool_rows(out / "logbook-spool")[0].row_digest + + assert ( + builder.main( + _argv( + input_h5, + out, + "--no-staging", + "--epochs", + "5", + "--logbook-prev-row-digest", + head, + ) + ) + == 1 + ) + + failure = json.loads((out / "failure.json").read_text()) + assert failure["error_type"] == "FileExistsError" + assert "refusing to overwrite existing candidate artifact" in failure["message"] + assert {name: _sha(out / name) for name in first} == first + rows = load_spool_rows(out / "logbook-spool") + assert [row.disposition for row in rows] == ["iterating", "failed"] + assert "input_sha_verified" not in rows[1].phases_reached + + +def test_uk_national_role_blocks_on_a_failed_seam_gate(monkeypatch, tmp_path): + """A blocking calibration-seam outcome stops the artifact after the + battery, block included, is on disk: no H5, no build record, a failed row.""" + pytest.importorskip("tables") + builder = candidate._load_builder_module() + input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) + monkeypatch.setattr( + calibration_run, + "uk_aggregate_admin_totals", + lambda frame, manifest: ( + {name: value * 5.0 for name, value in seam._admin_anchor_values().items()}, + [], + ), + ) + out = tmp_path / "blocked" + + assert builder.main(_argv(input_h5, out, "--no-staging", "--epochs", "5")) == 1 + + gates = json.loads((out / "microcosm_uk_2024_25.terminal_gates.json").read_text()) + assert gates["blocked_at_phase"] == "terminal" + assert gates["gates"]["uk_aggregate_admin"]["status"] == "failed" + assert not (out / "microcosm_uk_2024_25.h5").exists() + assert not (out / "build_record.json").exists() + assert (out / "calibration_diagnostics.json").is_file() + assert (out / "target_support_manifest.json").is_file() + completion = json.loads((out / "build.json").read_text()) + assert completion["kind"] == "uk_national_build_refused" + rows = load_spool_rows(out / "logbook-spool") + assert rows[0].disposition == "failed" + assert "candidate_blocked" in rows[0].phases_reached + assert rows[0].gate_verdicts["uk_aggregate_admin"]["verdict"] == "failed" # --- Behaviours re-anchored from the retired seam command's tests (B3) --- @@ -583,7 +818,7 @@ def test_uk_national_role_refuses_a_feed_outside_the_committed_pin( input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) # The fixture stubs the check; this test wants the real one. monkeypatch.setattr( - national_role, + full_targets, "require_committed_uk_chronicle_feed_pin", require_committed_uk_chronicle_feed_pin, ) @@ -594,18 +829,19 @@ def test_uk_national_role_refuses_a_feed_outside_the_committed_pin( manifest_sha256=manifest_sha256, ) monkeypatch.setattr( - national_role, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact + full_targets, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact ) def compile_must_not_run(facts, target_period): raise AssertionError("the register compiled before the feed pin was checked") monkeypatch.setattr( - national_role, "compile_uk_target_registry", compile_must_not_run + full_targets, "compile_uk_target_registry", compile_must_not_run ) out = tmp_path / "national" - with pytest.raises(UKChronicleFeedPinError, match="manifest: loaded"): - builder.main(_argv(input_h5, out, "--staging-local-only")) + assert builder.main(_argv(input_h5, out, "--staging-local-only")) == 1 + failure = json.loads((out / "failure.json").read_text()) + assert "manifest: loaded" in failure["message"] # The refusal happened after telemetry opened: the local run reads failed. runs = sorted(path.name for path in (out / "staging" / "runs").iterdir()) assert ( @@ -618,11 +854,9 @@ def test_uk_national_role_records_an_unpinned_feed_override( ) -> None: pytest.importorskip("tables") builder = candidate._load_builder_module() - input_h5, _registry, _artifact, _pin = _national_inputs( - monkeypatch, tmp_path, resolver=_FakeResolver - ) + input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) monkeypatch.setattr( - national_role, + full_targets, "require_committed_uk_chronicle_feed_pin", require_committed_uk_chronicle_feed_pin, ) @@ -630,18 +864,21 @@ def test_uk_national_role_records_an_unpinned_feed_override( tmp_path / "ledger", facts_sha256="0" * 64, manifest_sha256="c" * 64 ) monkeypatch.setattr( - national_role, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact + full_targets, "load_ledger_consumer_artifact", lambda path, **kwargs: artifact ) - calls: list[dict] = [] - monkeypatch.setattr(national_role, "run_uk_calibration", _fake_seam_run(calls)) out = tmp_path / "national" - assert ( - builder.main(_argv(input_h5, out, "--no-staging", "--allow-unpinned-feed")) == 0 + argv = _argv( + input_h5, out, "--no-staging", "--epochs", "5", "--allow-unpinned-feed" ) - extra = calls[0]["run_config_extra"] - assert extra["allow_unpinned_feed"] is True + argv[argv.index("--ledger-facts-sha256") + 1] = "0" * 64 + argv[argv.index("--ledger-manifest-sha256") + 1] = "c" * 64 + assert builder.main(argv) == 0 + record = json.loads((out / "build_record.json").read_text()) + assert record["run_config"]["allow_unpinned_feed"] is True # The pin the artifact was checked against is the committed one. - assert extra["chronicle_feed_pin"]["facts_sha256"] != "0" * 64 + assert record["run_config"]["chronicle_feed_pin"]["facts_sha256"] != "0" * 64 + operations = json.loads((out / "operations.json").read_text()) + assert operations["configuration"]["allow_unpinned_feed"] is True # --- the end-of-build incumbent evaluation (microcosm#823 E2) ----------------- @@ -762,20 +999,19 @@ def test_uk_national_role_requires_the_incumbent_pair(monkeypatch, tmp_path): pytest.importorskip("tables") builder = candidate._load_builder_module() input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) - with pytest.raises(ValueError, match="must be given together"): + out = tmp_path / "out" + assert ( builder.main( - _argv( - input_h5, - tmp_path / "out", - "--no-staging", - "--incumbent-h5", - str(input_h5), - ) + _argv(input_h5, out, "--no-staging", "--incumbent-h5", str(input_h5)) ) + == 1 + ) + failure = json.loads((out / "failure.json").read_text()) + assert "must be given together" in failure["message"] + assert not (out / "microcosm_uk_2024_25.h5").exists() def test_uk_national_role_refuses_the_incumbent_without_the_scorer() -> None: - def missing(name: str): raise ImportError(name) @@ -789,7 +1025,6 @@ def test_uk_national_role_evaluates_against_the_incumbent( pytest.importorskip("tables") builder = candidate._load_builder_module() input_h5, registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) - monkeypatch.setattr(national_role, "run_uk_calibration", _fake_seam_run([])) calls: list[dict] = [] module = _fake_evaluator( lambda kwargs: _receipt( @@ -804,7 +1039,14 @@ def test_uk_national_role_evaluates_against_the_incumbent( assert ( builder.main( - _argv(input_h5, out, "--no-staging", *_incumbent_argv(input_h5, tmp_path)) + _argv( + input_h5, + out, + "--no-staging", + "--epochs", + "5", + *_incumbent_argv(input_h5, tmp_path), + ) ) == 0 ) @@ -815,8 +1057,10 @@ def test_uk_national_role_evaluates_against_the_incumbent( assert call["incumbent_h5"] == tmp_path / "incumbent.h5" assert call["incumbent_sha256"] == _sha(tmp_path / "incumbent.h5") assert call["incumbent_label"] == "efrs_fixture" - assert call["target_registry"] is registry - assert call["band_edge_registry"] is registry + # The registries the scorer receives are the ones the graph solved and + # froze, by content hash. + assert call["target_registry"].version == registry.version + assert call["band_edge_registry"].version == registry.version assert call["calibration_year"] == 2025 receipt_path = out / "score_vs_incumbent.json" receipt = json.loads(receipt_path.read_text()) @@ -847,7 +1091,6 @@ def test_uk_national_role_records_an_evaluation_error_without_failing( pytest.importorskip("tables") builder = candidate._load_builder_module() input_h5, _registry, _artifact, _pin = _national_inputs(monkeypatch, tmp_path) - monkeypatch.setattr(national_role, "run_uk_calibration", _fake_seam_run([])) def explode(kwargs): raise RuntimeError("the incumbent frame refused to load") @@ -860,7 +1103,14 @@ def explode(kwargs): assert ( builder.main( - _argv(input_h5, out, "--no-staging", *_incumbent_argv(input_h5, tmp_path)) + _argv( + input_h5, + out, + "--no-staging", + "--epochs", + "5", + *_incumbent_argv(input_h5, tmp_path), + ) ) == 0 ) @@ -942,6 +1192,18 @@ def test_uk_national_role_evaluates_after_staging_the_bundle( full_receipt = json.loads((out / "score_vs_incumbent.json").read_text()) assert full_receipt["target_drift"][0]["winner"] == "candidate" assert calls[0]["candidate_sha256"] == manifest["outputs"]["dataset"]["sha256"] + # The completion marker binds the final bytes: the build record gained + # the delivery summary and the manifest its receipts after publication, + # and ``build.json`` was re-issued over both. + completion = json.loads((out / "build.json").read_text()) + for role, name in ( + ("build_record", "build_record.json"), + ("rowwise_candidate_manifest", builder.MANIFEST_FILENAME), + ): + assert completion[role]["sha256"] == _sha(out / name), role + assert completion[role]["size_bytes"] == (out / name).stat().st_size + assert "note" not in completion[role] + assert completion["dataset"]["sha256"] == _sha(out / "microcosm_uk_2024_25.h5") def test_uk_national_dry_run_records_the_incumbent(monkeypatch, tmp_path, capsys): diff --git a/test_support/microcosm_build/uk_calibration_run.py b/test_support/microcosm_build/uk_calibration_run.py index bd32084b8..8ae2235c1 100644 --- a/test_support/microcosm_build/uk_calibration_run.py +++ b/test_support/microcosm_build/uk_calibration_run.py @@ -28,8 +28,6 @@ UK_CALIBRATION_GATE_SCOPE, UK_CALIBRATION_GATE_SCOPE_EXCLUSIONS, UK_SPINE_GATE_SCOPE, - UKCalibrationRunPaths, - run_uk_calibration, ) from microcosm.build.uk_runtime.content_identity import uk_frame_content_identity from microcosm.build.uk_runtime.etb_services import ( @@ -188,16 +186,6 @@ def _registry(): ) -def _paths(tmp_path: Path) -> UKCalibrationRunPaths: - return UKCalibrationRunPaths( - input_h5=tmp_path / "input.h5", - staging_h5=tmp_path / "staged.h5", - diagnostics_json=tmp_path / "diagnostics.json", - build_record_json=tmp_path / "build_record.json", - terminal_gate_json=tmp_path / "terminal_gates.json", - ) - - def _sha(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() diff --git a/test_support/microcosm_build/uk_cgt_observation_period.py b/test_support/microcosm_build/uk_cgt_observation_period.py index e45ef3a9c..3664ebef9 100644 --- a/test_support/microcosm_build/uk_cgt_observation_period.py +++ b/test_support/microcosm_build/uk_cgt_observation_period.py @@ -1,4 +1,4 @@ -"""Dated CGT measurements retain base-year rows through both calibration paths.""" +"""Dated CGT measurements retain base-year rows through both calibration routes.""" # ruff: noqa: F401 @@ -13,8 +13,8 @@ import pytest from microcosm.build.uk_runtime import measure_simulation +from microcosm.build.uk_runtime.graph_national import materialize_uk_national_rows from microcosm.build.uk_runtime.measure_simulation import UKMeasureResolver -from microcosm.build.uk_runtime.national_calibration import UKNationalCalibrationStage from microcosm.build.uk_runtime.national_doctrine import UKNationalSolveDoctrine from microcosm.build.uk_runtime.national_frame import ( load_uk_national_frame, diff --git a/test_support/microcosm_build/uk_full_build_cli.py b/test_support/microcosm_build/uk_full_build_cli.py index 7d43477ed..ec1c4babe 100644 --- a/test_support/microcosm_build/uk_full_build_cli.py +++ b/test_support/microcosm_build/uk_full_build_cli.py @@ -77,6 +77,42 @@ PIN = "0" * 64 +#: Synthetic atomic-area support pins (microcosm#932): the digests the +#: synthetic prepared build binds as its geography assignment and the ones +#: :func:`patch_support_register` registers, so a synthetic release candidate +#: passes the register check the way a real one passes on ``sources.yaml``. +#: They are the ``RELEASE_PINS`` digests. +SYNTHETIC_SUPPORT_PINS = { + "uk_ew_output_area_2021": {"sha256": "c" * 64, "size_bytes": 1828698}, + "uk_scotland_output_area_2022": {"sha256": "d" * 64, "size_bytes": 440439}, + "uk_ni_data_zone_2021": {"sha256": "e" * 64, "size_bytes": 44638}, +} + + +def synthetic_geography_binding() -> dict: + """The request's geography binding as ``_prepare_geography`` shapes it.""" + return { + "assignment": "atomic", + "definition_sha256": "f" * 64, + "support_pins": {k: dict(v) for k, v in SYNTHETIC_SUPPORT_PINS.items()}, + "identity": "geography_household_key", + "stream": ["sha256-u53-v1", "uk-post-clone-atomic-area-v1"], + } + + +def patch_support_register(monkeypatch, pins=None) -> None: + """Register ``pins`` (default the synthetic ones) in place of sources.yaml.""" + from microcosm.build.uk_runtime import country_adapter + + register = { + k: dict(v) + for k, v in (SYNTHETIC_SUPPORT_PINS if pins is None else pins).items() + } + monkeypatch.setattr( + country_adapter, "uk_atomic_support_register", lambda spec=None: register + ) + + STEM = "microcosm_uk_2024_25_local" SUPPORT_ARGUMENTS = ( "--atomic-support-ew", @@ -793,7 +829,7 @@ def prepared(tmp_path, failed=None): graph = add_uk_export_preparation( graph, population=root.id, - bindings={"target_scope": "all"}, + bindings={"target_scope": "all", "geography": synthetic_geography_binding()}, artifact_inputs=( ArtifactInput( "gates", diff --git a/tools/measure_uk_atomic_assignment.py b/tools/measure_uk_atomic_assignment.py index b801b7a84..6de7e0dbf 100644 --- a/tools/measure_uk_atomic_assignment.py +++ b/tools/measure_uk_atomic_assignment.py @@ -475,6 +475,23 @@ def _suppress(value: float, minimum: int) -> float | str: return f"<{minimum}" if 0 < value < minimum else value +def _suppressed_area(row: dict, minimum: int) -> dict: + """One committed area row: counts below the minimum suppressed, z with them. + + ``z`` is a function of ``rows`` and ``expected_rows`` (with the region's + scale), so a published z-score would let a reader recover a suppressed + count exactly; it is blanked wherever ``rows`` is. + """ + suppressed = 0 < row["rows"] < minimum + return { + **row, + "rows": _suppress(row["rows"], minimum), + "ess": _suppress(row["ess"], minimum), + "sources": _suppress(row["sources"], minimum), + "z": None if suppressed else row["z"], + } + + def _peak_rss_bytes() -> int: usage = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss return int(usage if sys.platform == "darwin" else usage * 1024) @@ -552,12 +569,7 @@ def main(argv: list[str] | None = None) -> None: if k_ not in {"region_mix", "geography_binding"} }, "areas": [ - { - **row, - "rows": _suppress(row["rows"], args.minimum_count), - "ess": _suppress(row["ess"], args.minimum_count), - "sources": _suppress(row["sources"], args.minimum_count), - } + _suppressed_area(row, args.minimum_count) for row in rows.to_dict("records") ], } @@ -579,7 +591,8 @@ def main(argv: list[str] | None = None) -> None: "Assignment-level before/after evidence for the UK geography-first " "reordering (step 2): legacy sequential ladder draw versus the " "identity-keyed single-stage draw, design weights, no solve; per-area " - "counts below the minimum are suppressed and no unit records appear." + "counts below the minimum are suppressed together with their " + "z-scores, and no unit records appear." ), "date": dt.date.today().isoformat(), "model_period": model_period, diff --git a/tools/preflight_uk_local_release_candidate.py b/tools/preflight_uk_local_release_candidate.py index fd7269fc8..44edb9b3c 100644 --- a/tools/preflight_uk_local_release_candidate.py +++ b/tools/preflight_uk_local_release_candidate.py @@ -224,6 +224,31 @@ def check_candidate_dir(candidate_dir: Path, *, today: date | None = None) -> li for name in ("spine", "ladder"): if manifest.get("identity", {}).get(name, {}).get("pin_verified") is not True: failures.append(f"identity.{name}.pin_verified is not true.") + # The release line assigns geography with the identity-keyed atomic law + # on the registered supports (microcosm#932): the manifest's binding must + # say so, and its support pins must be the sources.yaml rows, not merely + # the digests the operator pinned. + assignment = (manifest.get("geography") or {}).get("assignment") or {} + if assignment.get("assignment") != "atomic": + failures.append( + f"geography.assignment.assignment is {assignment.get('assignment')!r}, " + "not 'atomic'." + ) + else: + from microcosm.build.uk_runtime import country_adapter + + register = country_adapter.uk_atomic_support_register() + support_pins = assignment.get("support_pins") or {} + for system, expected in register.items(): + pin = support_pins.get(system) or {} + if ( + pin.get("sha256") != expected["sha256"] + or pin.get("size_bytes") != expected["size_bytes"] + ): + failures.append( + f"geography.assignment.support_pins.{system} is not the " + "registered support (sources.yaml)." + ) dataset = manifest.get("outputs", {}).get("dataset", {}) h5_path = Path(str(dataset.get("path", ""))) if not h5_path.is_absolute():