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