1- """Verify E4 stochastic stage identity stability on an existing UK frame."""
1+ """Verify E4 stochastic stage identity stability on an existing UK frame.
2+
3+ Recomputes every E4 column twice from the pure derivations — once in the
4+ frame's row order, once on row-permuted tables — un-permutes by entity id,
5+ and also compares the original-order recomputation against the columns
6+ stored in the artifact. Exit status is nonzero on any mismatch.
7+ """
28
39from __future__ import annotations
410
511import argparse
612import json
7- from collections .abc import Callable , Sequence
13+ import sys
14+ from collections .abc import Callable , Mapping , Sequence
815from pathlib import Path
916
17+ import numpy as np
18+ import pandas as pd
19+
20+ from microcosm .build .uk_runtime .frs_brma import (
21+ UK_BRMA_DECLARED_SEEDS ,
22+ _benunit_regions ,
23+ _enum_name ,
24+ assign_brma_by_cell ,
25+ collapse_benunit_brma_to_household ,
26+ load_brma_count_resource ,
27+ )
28+ from microcosm .build .uk_runtime .frs_household_draws import derive_frs_household_draws
29+ from microcosm .build .uk_runtime .frs_person_draws import derive_frs_person_draws
30+ from microcosm .build .uk_runtime .frs_take_up import (
31+ aggregate_person_reported_to_benunit ,
32+ derive_frs_take_up ,
33+ )
1034from microcosm .build .uk_runtime .national_build import load_uk_national_frame
35+ from microcosm .build .uk_runtime .national_frame import uk_time_period
1136
1237
1338def identity_stability_receipt (
@@ -45,28 +70,140 @@ def identity_stability_receipt(
4570 }
4671
4772
48- def main () -> None :
49- parser = argparse .ArgumentParser (description = __doc__ )
50- parser .add_argument ("--input-h5" , type = Path , required = True )
51- parser .add_argument ("--output" , type = Path , required = True )
52- args = parser .parse_args ()
53- frame , _provenance = load_uk_national_frame (args .input_h5 )
54- receipt = {
73+ def e4_identity_receipt (
74+ frame ,
75+ * ,
76+ contract ,
77+ count_resource : Mapping [str , object ],
78+ lha_category : Sequence [object ],
79+ permutation_seed : int ,
80+ ) -> dict [str , object ]:
81+ """Recompute every E4 column in original and permuted row order.
82+
83+ Two claims are receipted: a row permutation of the input tables changes
84+ no assignment per entity id, and the original-order recomputation equals
85+ the columns stored in the artifact (re-derivation identity).
86+ """
87+
88+ person = frame .table ("person" )
89+ benunit = frame .table ("benunit" ).copy ()
90+ household = frame .table ("household" )
91+ if len (lha_category ) != len (benunit ):
92+ raise ValueError ("LHA_category materialization must align to benunit rows." )
93+ benunit ["LHA_category" ] = [_enum_name (value ) for value in lha_category ]
94+ benunit ["region" ] = _benunit_regions (person , household , benunit )
95+
96+ def recompute (person_t , benunit_t , household_t ) -> dict [str , pd .DataFrame ]:
97+ anchors = aggregate_person_reported_to_benunit (person_t , benunit_t )
98+ take_up = derive_frs_take_up (benunit_t , anchors = anchors , contract = contract )
99+ take_up .index = benunit_t ["benunit_id" ].to_numpy ()
100+ person_draws = derive_frs_person_draws (person_t , contract = contract )
101+ person_draws .index = person_t ["person_id" ].to_numpy ()
102+ household_draws = derive_frs_household_draws (household_t , contract = contract )
103+ household_draws .index = household_t ["household_id" ].to_numpy ()
104+ seed = UK_BRMA_DECLARED_SEEDS ["brma" ]
105+ benunit_brma = pd .DataFrame (
106+ {
107+ "benunit_id" : benunit_t ["benunit_id" ].to_numpy (),
108+ "brma" : assign_brma_by_cell (
109+ benunit_t , count_resource = count_resource , seed = seed
110+ ),
111+ }
112+ )
113+ household_draws ["brma" ] = collapse_benunit_brma_to_household (
114+ person_t , benunit_brma , household_t , seed = seed
115+ )
116+ return {
117+ "benunit" : take_up ,
118+ "person" : person_draws ,
119+ "household" : household_draws ,
120+ }
121+
122+ original = recompute (person , benunit , household )
123+ rng = np .random .default_rng (permutation_seed )
124+ permuted = recompute (
125+ person .iloc [rng .permutation (len (person ))].reset_index (drop = True ),
126+ benunit .iloc [rng .permutation (len (benunit ))].reset_index (drop = True ),
127+ household .iloc [rng .permutation (len (household ))].reset_index (drop = True ),
128+ )
129+
130+ stored = {
131+ "person" : frame .table ("person" ).set_index ("person_id" ),
132+ "benunit" : frame .table ("benunit" ).set_index ("benunit_id" ),
133+ "household" : frame .table ("household" ).set_index ("household_id" ),
134+ }
135+ permutation_mismatches : dict [str , list [str ]] = {}
136+ stored_mismatches : dict [str , list [str ]] = {}
137+ stored_columns_missing : dict [str , list [str ]] = {}
138+ for entity , values in original .items ():
139+ for column in values .columns :
140+ left = values [column ]
141+ right = permuted [entity ][column ].reindex (left .index )
142+ if not np .array_equal (left .to_numpy (), right .to_numpy ()):
143+ permutation_mismatches .setdefault (entity , []).append (column )
144+ if column not in stored [entity ].columns :
145+ stored_columns_missing .setdefault (entity , []).append (column )
146+ continue
147+ kept = stored [entity ][column ].reindex (left .index )
148+ if not np .array_equal (
149+ left .to_numpy (), kept .to_numpy ().astype (left .to_numpy ().dtype )
150+ ):
151+ stored_mismatches .setdefault (entity , []).append (column )
152+ return {
55153 "check" : "uk_e4_identity_stability" ,
56- "input_h5" : str (args .input_h5 ),
57- "status" : "requires caller-supplied E4 transform in acceptance harness" ,
154+ "permutation_seed" : permutation_seed ,
155+ "identical_under_permutation" : not permutation_mismatches ,
156+ "matches_stored_columns" : not stored_mismatches and not stored_columns_missing ,
157+ "permutation_mismatches" : permutation_mismatches ,
158+ "stored_mismatches" : stored_mismatches ,
159+ "stored_columns_missing" : stored_columns_missing ,
160+ "columns_by_entity" : {
161+ entity : list (values .columns ) for entity , values in original .items ()
162+ },
58163 "entity_row_counts" : {
59164 entity : int (len (frame .table (entity ))) for entity in frame .entities
60165 },
61166 }
167+
168+
169+ def main () -> int :
170+ parser = argparse .ArgumentParser (description = __doc__ )
171+ parser .add_argument ("--input-h5" , type = Path , required = True )
172+ parser .add_argument ("--output" , type = Path , required = True )
173+ parser .add_argument ("--permutation-seed" , type = int , default = 123 )
174+ args = parser .parse_args ()
175+
176+ from microcosm .build .uk_runtime .take_up_contract import load_uk_take_up_contract
177+ from microcosm .frame .adapters .policyengine_uk import PolicyEngineUKEngine
178+
179+ frame , _provenance = load_uk_national_frame (args .input_h5 )
180+ engine = PolicyEngineUKEngine ()
181+ lha_category = engine .materialize (frame , ("LHA_category" ,), uk_time_period (frame ))[
182+ "LHA_category"
183+ ]
184+ receipt = e4_identity_receipt (
185+ frame ,
186+ contract = load_uk_take_up_contract (),
187+ count_resource = load_brma_count_resource (),
188+ lha_category = lha_category ,
189+ permutation_seed = args .permutation_seed ,
190+ )
191+ receipt ["input_h5" ] = str (args .input_h5 )
62192 args .output .write_text (json .dumps (receipt , indent = 2 , sort_keys = True ) + "\n " )
193+ ok = bool (
194+ receipt ["identical_under_permutation" ] and receipt ["matches_stored_columns" ]
195+ )
196+ print (
197+ "identity stability:" ,
198+ "PASS" if ok else f"FAIL ({ args .output } )" ,
199+ )
200+ return 0 if ok else 1
63201
64202
65203def _reverse_rows (frame ):
66204 from microcosm .build .uk_runtime .national_frame import (
67205 uk_household_weight_kind ,
68206 uk_national_frame ,
69- uk_time_period ,
70207 )
71208
72209 return uk_national_frame (
@@ -81,4 +218,4 @@ def _reverse_rows(frame):
81218
82219
83220if __name__ == "__main__" :
84- main ()
221+ sys . exit ( main () )
0 commit comments