@@ -274,27 +274,58 @@ def test_inventory_rows_carry_residual_and_tolerance_under_custom_tol():
274274 assert per_valley2 ["K_valley" ][0 ]["little_group_passed" ] is True
275275
276276
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."""
277+ def test_hsp_star_conjugation_lg_tolerance_changes_c3_classification ():
278+ """C3 at K [1/3, 1/3] mapped by C2 to star partner [2/3, 0].
279+ With lg_tolerance=9e-7 the C3 source operation is rejected as
280+ source_not_in_hsp_little_group; with 5e-6 it is accepted and
281+ conjugation proceeds. Both tolerances are serialized separately."""
281282 from valleyscope .analysis .hsp_star_conjugation import (
282283 build_hsp_star_conjugation_report ,
283284 )
284- id3 = np .eye (3 , dtype = int )
285+ c2 = np .array ([[- 1 , - 1 , 0 ], [0 , 1 , 0 ], [0 , 0 , - 1 ]], dtype = int )
286+ c3 = np .array ([[0 , - 1 , 0 ], [1 , - 1 , 0 ], [0 , 0 , 1 ]], dtype = int )
285287 ops = [
286- {"operation_id" : 0 , "rotation_frac" : id3 , "sector_mapping" : {"K_valley" : "K_valley" }},
288+ {"operation_id" : 0 , "rotation_frac" : np .eye (3 , dtype = int ),
289+ "sector_mapping" : {"K_valley" : "K_valley" }, "det" : 1 },
290+ {"operation_id" : 1 , "rotation_frac" : c3 ,
291+ "sector_mapping" : {"K_valley" : "K_valley" }, "det" : 1 },
292+ {"operation_id" : 2 , "rotation_frac" : c2 ,
293+ "sector_mapping" : {"K_valley" : "K_valley" }, "det" : 1 },
287294 ]
288- report = build_hsp_star_conjugation_report (
289- kpoint_frac_by_name = {"GammaM" : [0.0 , 0.0 , 0.0 ]},
295+ k_k = [0.333333 , 0.333333 , 0.0 ]
296+
297+ report_strict = build_hsp_star_conjugation_report (
298+ kpoint_frac_by_name = {"KM" : k_k },
299+ operations = ops ,
300+ valley_names = ["K_valley" ],
301+ lg_tolerance = 9e-7 ,
302+ )
303+ assert report_strict ["lg_tolerance" ] == 9e-7
304+ assert report_strict ["tolerance" ] != report_strict ["lg_tolerance" ]
305+ entries = report_strict .get ("by_source_kpoint" , {}).get ("KM" , [])
306+ not_lg = [e for e in entries
307+ if e .get ("conjugation_status" ) == "source_not_in_hsp_little_group"
308+ and e .get ("source_preserving_operation_id" ) == 1 ]
309+ assert len (not_lg ) == 1 , (
310+ f"C3 should be source_not_in_hsp_little_group at 9e-7, "
311+ f"got { [(e .get ('source_preserving_operation_id' ), e .get ('conjugation_status' )) for e in entries ]} "
312+ )
313+
314+ report_loose = build_hsp_star_conjugation_report (
315+ kpoint_frac_by_name = {"KM" : k_k },
290316 operations = ops ,
291317 valley_names = ["K_valley" ],
292- lg_tolerance = 3.5e-6 ,
318+ lg_tolerance = 5e-6 ,
319+ )
320+ assert report_loose ["lg_tolerance" ] == 5e-6
321+ entries2 = report_loose .get ("by_source_kpoint" , {}).get ("KM" , [])
322+ not_lg2 = [e for e in entries2
323+ if e .get ("conjugation_status" ) == "source_not_in_hsp_little_group"
324+ and e .get ("source_preserving_operation_id" ) == 1 ]
325+ assert len (not_lg2 ) == 0 , (
326+ f"C3 should NOT be rejected at 5e-6, "
327+ f"got { [(e .get ('source_preserving_operation_id' ), e .get ('conjugation_status' )) for e in entries2 ]} "
293328 )
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" ]
298329
299330
300331def test_config_parser_rejects_invalid_tolerance (tmp_path ):
@@ -324,24 +355,63 @@ def test_config_parser_rejects_invalid_tolerance(tmp_path):
324355 pass
325356
326357
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 } "
358+ def test_unitary_valley_sewing_attempts_honours_config_tolerance ():
359+ """_build_unitary_valley_sewing_attempts reads the configured tolerance
360+ from symmetry_payload and passes it to _valley_preserving_little_group_ids.
361+ The patched helper records the actual tolerance, proving the production
362+ caller does not fall back to a module default."""
363+ from valleyscope .workflows import analyze_hsp
364+ recorded_tolerances = []
365+
366+ def _recording_lg_ids (operations , kpoint , valley , tolerance = None ):
367+ recorded_tolerances .append (tolerance )
368+ return [0 ]
369+
370+ original = analyze_hsp ._valley_preserving_little_group_ids
371+ analyze_hsp ._valley_preserving_little_group_ids = _recording_lg_ids
372+ try :
373+ payload = {
374+ "status" : "ok" ,
375+ "detected_operations" : [
376+ {"operation_id" : 0 , "rotation_frac" : np .eye (3 , dtype = int ),
377+ "sector_mapping" : {"K_valley" : "K_valley" }, "det" : 1 },
378+ ],
379+ "hsp_little_group_k_residual_tolerance" : 3.5e-6 ,
380+ "kpoint_frac_by_name" : {},
381+ }
382+ coverage = {
383+ "by_valley" : {
384+ "K_valley" : {
385+ "missing_source_hsp_representatives" : [
386+ {"inverse_parent_k_frac" : np .zeros (3 ), "source_hsp_label" : "Gamma" },
387+ ],
388+ },
389+ },
390+ }
391+ try :
392+ analyze_hsp ._build_unitary_valley_sewing_attempts (
393+ ebr_input_candidates = {"candidates" : []},
394+ projected_hsp_coverage = coverage ,
395+ source_hsp_basis_by_valley = {"K_valley" : {}},
396+ symmetry_payload = payload ,
397+ kpoint_frac_by_name = {}, q_cart_by_kpoint = {},
398+ coefficients_by_kpoint = {},
399+ seed_projectors_by_kpoint = {},
400+ symmetry_adapted_projectors_by_kpoint = {},
401+ workflow_decisions = {},
402+ source_tables = {"K_valley" : {}},
403+ source_certificates = {"K_valley" : {}},
404+ cprime_validation_context = {},
405+ source_basis_record = {},
406+ wavecar_rtag = "" ,
407+ )
408+ except AttributeError :
409+ pass # downstream requires real Table; already tested the tolerance pass-through
410+ assert recorded_tolerances == [3.5e-6 ], (
411+ f"Expected tolerance 3.5e-6, got { recorded_tolerances } "
412+ )
413+ finally :
414+ analyze_hsp ._valley_preserving_little_group_ids = original
345415
346416
347417def test_fractional_operation_matches_cartesian_column_convention_for_nonorthogonal_lattice ():
0 commit comments