Skip to content

Commit 2e4a345

Browse files
committed
fix: close last HSP little-group tolerance gaps and add production regressions
- _build_unitary_valley_sewing_attempts now reads the configured tolerance from symmetry_payload and passes it to every _valley_preserving_little_group_ids call. - build_hsp_star_conjugation_report now uses lg_tolerance in the is_little_group_operation call; lg_tolerance is serialized in the output report separately from the target-matching tolerance. - Production consumer regressions cover inventory serialization, valley_preserving_little_group_ids tolerance propagation, HSP-star lg_tolerance serialization, and config-parser validation of negative/NaN/infinity values.
1 parent 477d03f commit 2e4a345

3 files changed

Lines changed: 127 additions & 2 deletions

File tree

tests/test_symmetry.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
229347
def test_fractional_operation_matches_cartesian_column_convention_for_nonorthogonal_lattice():
230348
lattice = np.array(
231349
[

valleyscope/analysis/hsp_star_conjugation.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,9 @@ def build_hsp_star_conjugation_report(
9797
# Valley-preserving operations that map the kpoint to
9898
# another star member are not valid source operations for
9999
# character derivation at this kpoint.
100-
if not is_little_group_operation(g_rot, source_frac):
100+
if not is_little_group_operation(
101+
g_rot, source_frac, tolerance=lg_tolerance,
102+
):
101103
entries.append(_conjugation_entry(
102104
source_kpoint=source_label,
103105
source_frac=source_frac,
@@ -233,6 +235,7 @@ def build_hsp_star_conjugation_report(
233235
return {
234236
"status": status,
235237
"tolerance": tolerance,
238+
"lg_tolerance": lg_tolerance,
236239
"interpretation": (
237240
"For each HSP-star pair (k0 -> k1 = r k0), conjugate source "
238241
"valley-preserving operations g to target operations h = r g r^-1. "

valleyscope/workflows/analyze_hsp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3472,6 +3472,10 @@ def _build_unitary_valley_sewing_attempts(
34723472
row for row in symmetry_payload.get("detected_operations", [])
34733473
if isinstance(row, dict)
34743474
]
3475+
lg_tolerance = float(symmetry_payload.get(
3476+
"hsp_little_group_k_residual_tolerance",
3477+
DEFAULT_HSP_LITTLE_GROUP_K_RESIDUAL_TOLERANCE,
3478+
))
34753479
missing_targets = []
34763480
coverage_by_valley = projected_hsp_coverage.get("by_valley", {})
34773481
for target_valley, coverage in coverage_by_valley.items():
@@ -3488,7 +3492,7 @@ def _build_unitary_valley_sewing_attempts(
34883492
target_k = missing.get("inverse_parent_k_frac")
34893493
target_hsp = missing.get("source_hsp_label")
34903494
target_ids = _valley_preserving_little_group_ids(
3491-
operations, target_k, target_valley
3495+
operations, target_k, target_valley, tolerance=lg_tolerance,
34923496
)
34933497
classification = classify_projected_subspace_kpoint(
34943498
parent_k_frac=target_k,

0 commit comments

Comments
 (0)