@@ -226,6 +226,124 @@ def test_config_hsp_little_group_k_residual_default():
226226 assert custom .hsp_little_group_k_residual == 3.5e-6
227227
228228
229+ # --------------------------------------------------------------------
230+ # Production consumer regressions — inventory, HSP-star, config parser
231+ # --------------------------------------------------------------------
232+
233+ def test_inventory_rows_carry_residual_and_tolerance_under_custom_tol ():
234+ """update_valley_preserving_operation_inventory serializes residual
235+ evidence with the configured tolerance, and the acceptance/rejection
236+ Boolean agrees with the residual."""
237+ from valleyscope .analysis .valley_little_group import (
238+ update_valley_preserving_operation_inventory ,
239+ )
240+ c3 = np .array ([[0 , - 1 , 0 ], [1 , - 1 , 0 ], [0 , 0 , 1 ]], dtype = int )
241+ payload = {
242+ "detected_operations" : [
243+ {
244+ "operation_id" : 1 , "kind" : "C3" , "order" : 3 ,
245+ "rotation_frac" : c3 ,
246+ "sector_mapping" : {"K_valley" : "K_valley" },
247+ "preserved" : {"K_valley" : True },
248+ },
249+ ],
250+ "hsp_little_group_k_residual_tolerance" : 9e-7 ,
251+ }
252+ k_6digit = np .float64 ([0.333333 , 0.333333 , 0.0 ])
253+ per_valley = update_valley_preserving_operation_inventory (
254+ symmetry_payload = payload ,
255+ kpoint_name = "KM" ,
256+ k_frac = k_6digit ,
257+ valley_names = ["K_valley" ],
258+ )
259+ row = per_valley ["K_valley" ][0 ]
260+ assert row ["little_group_passed" ] is False
261+ assert 9e-7 < row ["hsp_little_group_k_residual_max_abs" ] < 1.1e-6
262+ assert row ["hsp_little_group_k_residual_tolerance" ] == 9e-7
263+ # Default tolerance must accept.
264+ payload2 = {
265+ "detected_operations" : payload ["detected_operations" ],
266+ "hsp_little_group_k_residual_tolerance" : 5e-6 ,
267+ }
268+ per_valley2 = update_valley_preserving_operation_inventory (
269+ symmetry_payload = payload2 ,
270+ kpoint_name = "KM" ,
271+ k_frac = k_6digit ,
272+ valley_names = ["K_valley" ],
273+ )
274+ assert per_valley2 ["K_valley" ][0 ]["little_group_passed" ] is True
275+
276+
277+ def test_hsp_star_conjugation_serializes_lg_tolerance ():
278+ """build_hsp_star_conjugation_report accepts lg_tolerance and serializes
279+ it in the output dict, keeping it distinct from the target-matching
280+ tolerance."""
281+ from valleyscope .analysis .hsp_star_conjugation import (
282+ build_hsp_star_conjugation_report ,
283+ )
284+ id3 = np .eye (3 , dtype = int )
285+ ops = [
286+ {"operation_id" : 0 , "rotation_frac" : id3 , "sector_mapping" : {"K_valley" : "K_valley" }},
287+ ]
288+ report = build_hsp_star_conjugation_report (
289+ kpoint_frac_by_name = {"GammaM" : [0.0 , 0.0 , 0.0 ]},
290+ operations = ops ,
291+ valley_names = ["K_valley" ],
292+ lg_tolerance = 3.5e-6 ,
293+ )
294+ assert report ["lg_tolerance" ] == 3.5e-6
295+ # target-matching tolerance is separate
296+ assert "tolerance" in report
297+ assert report ["tolerance" ] != report ["lg_tolerance" ]
298+
299+
300+ def test_config_parser_rejects_invalid_tolerance (tmp_path ):
301+ """Parser-level rejection of negative, NaN, and infinity values."""
302+ import yaml
303+ from valleyscope .io .config import load_config
304+ base = """
305+ analysis:
306+ kpoints: [GM]
307+ iband: [1, 2]
308+ input:
309+ wavefunction_h5: /nonexistent.h5
310+ output:
311+ directory: /tmp/out
312+ """
313+ for bad_val , label in [(- 1.0 , "negative" ), ("nan" , "NaN" ), ("inf" , "inf" )]:
314+ cfg_path = tmp_path / f"cfg_{ label } .yaml"
315+ cfg_path .write_text (base + f"""
316+ symmetry:
317+ tolerance:
318+ hsp_little_group_k_residual: { bad_val }
319+ """ )
320+ try :
321+ load_config (cfg_path )
322+ pytest .fail (f"config parser should reject { label } tolerance value { bad_val } " )
323+ except (ValueError , SystemExit ):
324+ pass
325+
326+
327+ def test_valley_preserving_little_group_ids_passes_tolerance ():
328+ """The unitary-sewing helper _valley_preserving_little_group_ids
329+ passes tolerance through _little_group_member."""
330+ from valleyscope .workflows .analyze_hsp import (
331+ _valley_preserving_little_group_ids ,
332+ )
333+ c3 = np .array ([[0 , - 1 , 0 ], [1 , - 1 , 0 ], [0 , 0 , 1 ]], dtype = int )
334+ ops = [
335+ {"operation_id" : 0 , "rotation_frac" : np .eye (3 , dtype = int ), "sector_mapping" : {"K_valley" : "K_valley" }},
336+ {"operation_id" : 1 , "rotation_frac" : c3 , "sector_mapping" : {"K_valley" : "K_valley" }},
337+ ]
338+ k = np .float64 ([0.333333 , 0.333333 , 0.0 ])
339+ # With tolerance=9e-7 the C3 operation is rejected
340+ ids_strict = _valley_preserving_little_group_ids (ops , k , "K_valley" , tolerance = 9e-7 )
341+ assert ids_strict == [0 ], f"Expected only identity, got { ids_strict } "
342+ # Default accepts both
343+ ids_default = _valley_preserving_little_group_ids (ops , k , "K_valley" )
344+ assert ids_default == [0 , 1 ], f"Expected both ops, got { ids_default } "
345+
346+
229347def test_fractional_operation_matches_cartesian_column_convention_for_nonorthogonal_lattice ():
230348 lattice = np .array (
231349 [
0 commit comments