From 838730d720c3d93fc55b41ecd40af557dd6e8532 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 14 Jul 2026 10:27:22 -0400 Subject: [PATCH] Measure household_weight from typed frame weights in the parity universe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build M's sparse run failed the eCPS parity gate on household_weight: the reference (a finished export) carries the calibrated weight column at 22.6% nonzero share, while the candidate keeps weights as typed pipeline state and us_nonzero_shares read the deliberately absent table column as an all-zero layer. The coverage gate already mirrors the H5 adapter (which materializes the typed vector as household_weight on write); the parity share computation now applies the same mirror, so both gates and the export tell one story. A table column still wins when present. The check entered the parity reference with #399's 158-column expansion — Build J's 65-entry reference predates it, so this was another first-full-scale exercise. Fixes #427. Co-Authored-By: Claude Fable 5 --- .../build/us_runtime/nonzero_shares.py | 16 +++++++++++ .../tests/test_us_nonzero_shares.py | 28 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/packages/populace-build/src/populace/build/us_runtime/nonzero_shares.py b/packages/populace-build/src/populace/build/us_runtime/nonzero_shares.py index 0e3eeea9..443e333a 100644 --- a/packages/populace-build/src/populace/build/us_runtime/nonzero_shares.py +++ b/packages/populace-build/src/populace/build/us_runtime/nonzero_shares.py @@ -82,4 +82,20 @@ def us_nonzero_shares( if requested is not None and column not in requested: continue shares[column] = nonzero_share(table[column]) + + # Frame weights are typed pipeline state, not ordinary table data. The US + # adapter materializes the authoritative vector as ``household_weight`` + # when writing PolicyEngine H5, and the coverage gate mirrors that export + # behavior; mirror it here too so the parity universe measures what is + # persisted instead of reading the deliberately absent table column as an + # all-zero layer (the Build M sparse failure: the reference's finished + # export carries the column, the candidate keeps it as typed state). + if ( + (requested is None or "household_weight" in requested) + and "household_weight" not in shares + and "household" in frame.weighted_entities + ): + shares["household_weight"] = nonzero_share( + pd.Series(frame.weights_for("household").values) + ) return shares diff --git a/packages/populace-build/tests/test_us_nonzero_shares.py b/packages/populace-build/tests/test_us_nonzero_shares.py index b6dab76a..f3ab7aa4 100644 --- a/packages/populace-build/tests/test_us_nonzero_shares.py +++ b/packages/populace-build/tests/test_us_nonzero_shares.py @@ -100,3 +100,31 @@ def test_column_restriction_filters_raw_source_columns(self) -> None: ) assert set(shares) == {"student_loan_interest"} + + +class TestTypedWeightMirror: + def test_household_weight_measures_typed_frame_weights(self) -> None: + """The parity universe measures weights as the export persists them. + + Frame weights are typed pipeline state, not a table column; the H5 + adapter materializes them as ``household_weight`` and the coverage + gate mirrors that. Build M's sparse run failed eCPS parity because + this module read the deliberately absent table column as an all-zero + layer while the reference's finished export carries it — the mirror + keeps the two gates and the export telling one story. + """ + + shares = us_nonzero_shares(_frame(), columns=["household_weight"]) + assert shares["household_weight"] == 1.0 + + def test_table_column_still_wins_when_present(self) -> None: + frame = _frame() + tables = {entity: frame.table(entity).copy() for entity in frame.entities} + tables["household"]["household_weight"] = [0.0, 5.0] + rebuilt = Frame( + tables, + frame.schema, + {entity: frame.weights_for(entity) for entity in frame.weighted_entities}, + ) + shares = us_nonzero_shares(rebuilt, columns=["household_weight"]) + assert shares["household_weight"] == 0.5