battery: same-absence groups of one Laue class pass as indistinguishable_alternative

sgequiv keyed space groups on their proper+improper rotation matrices, so groups with
identical merged intensities but different point groups (I41md vs I-42d, Pc vs P2/c,
Pna21 vs Pnma, P2 vs P2/m) were called a "REAL difference", contradicting its own
docstring, and score.py compared point groups literally on the XDS arms. A run that
wrote one member of such a pair and listed the other in SPACE_GROUP_ALTERNATIVES failed
as sym_other / sym_over.

sgequiv now keys each group, in every setting gemmi tabulates, on its Laue rotations
(operations plus their negatives), centring and absences; describe() separates the new
different-point-group pairs from the old same-point-group ones, and enantiomorph
handling is unchanged. score.py passes a row, on both arms, when some setting of the
reference has the same signature as our group in our own setting: verdict pass, cause
indistinguishable_alternative, the reason saying whether rugnux listed the reference.

Pass, not unscored: unlike an unmeasured screw axis, everything was measured and the
answer is as right as the data allow; only a model or a strong anomalous signal decides.
The separate cause keeps such rows countable. Being listed in SPACE_GROUP_ALTERNATIVES
alone is deliberately not enough: that list also carries the screw alternatives of an
undetermined axis, whose absences differ, and those stay under the screw rule. The
open arm's same-point-group UNDECIDABLE pass now also requires the setting check.

results.json gains sg_alternatives (rugnux's SPACE_GROUP_ALTERNATIVES). KDP joins the
ci tier. Re-scoring the open and in-house arms of the two latest full runs changes one
verdict in each: kdp_x10sa_20keV fail/sym_other -> pass/indistinguishable_alternative.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
2026-10-09 16:27:23 +02:00
co-authored by Claude Opus 5.5
parent f914a116aa
commit d28c8b8bb3
5 changed files with 140 additions and 22 deletions
+24 -6
View File
@@ -23,8 +23,9 @@ run, is the lattice right, is the symmetry right, and on the XDS arms, is the me
XDS's. The lattice test compares Niggli-reduced primitive cells: the primitive volume ratio
must be 0.95-1.05 and the reduced edges must agree within 2%. On the open arm the space group is
scored with `sgequiv`. On the XDS arms only the point group is scored, because XDS never tests a
screw axis. A no-crystal control (`"expect": "no_lattice"`) passes only if no lattice is reported.
`score.py` has the details.
screw axis. On both, a group with the reference's Laue class and absences passes (below). A
no-crystal control (`"expect": "no_lattice"`) passes only if no lattice is reported. `score.py`
has the details.
**A question the data could not answer is `unscored`, not a failure.** Two of these. The hand of an
enantiomorphic pair (P4_1 vs P4_3) is not in the intensities at all, and `sgequiv` already counts it
@@ -38,6 +39,20 @@ among the ones it offered, and the only axis the two groups differ on is one it
called wrongly where the row WAS measured meets none of them and stays a `sym_screw` failure - this
rule excuses an unanswerable question, never a wrong answer.
**A group the intensities cannot separate from the reference passes, as
`indistinguishable_alternative`.** Two space groups of one Laue class with exactly the same
systematic absences give the same merged intensities: acentric vs centric (P2 vs P2/m, Pc vs P2/c,
Pna21 vs Pnma) and the two acentric groups of one class (I41md vs I-42d). Only a model, or an
anomalous signal strong enough to break Friedel's law, tells them apart. rugnux must still write
one, and lists the others in `SPACE_GROUP_ALTERNATIVES`. `sgequiv` keys each group, in every
setting, on its Laue rotations (the operations and their negatives), its centring and its absences;
the row passes when some setting of the reference agrees with our group in our own setting, on
both arms, and its reason says whether rugnux listed the reference. This is a pass rather than
`unscored` because, unlike an unmeasured screw, everything was measured and the answer is as right
as the data allow; the separate cause keeps it countable. Being listed in
`SPACE_GROUP_ALTERNATIVES` is not by itself enough, because that list also carries the screw
alternatives of an undetermined axis, whose absences differ - those stay under the rule above.
**A row may accept more than one reference.** Where the deposition and our reduction disagree,
the disagreement is real, and no test available to us settles it - a knife-edge - the row lists
the other acceptable answer in `ref_alternatives` and passes if our answer matches any of its
@@ -229,7 +244,7 @@ R=build/rugnux/rugnux
$B run --rugnux $R # full battery: open + inhouse
$B run --rugnux $R --tier smoke # 21 sets covering readers, symmetry, known hard cases
$B run --rugnux $R --tier ci # 25 sets every recent release passed (CI, below)
$B run --rugnux $R --tier ci # 26 sets every recent release passed (CI, below)
$B run --rugnux $R --only 5reo,lyso_ref # named sets
$B run --rugnux $R --arm private # the private arm, on its own
```
@@ -254,13 +269,15 @@ R-free have a reference. `$B table RUN` prints it again for any finished run.
- **smoke** covers the readers, the symmetry classes and some known hard cases; several of its
sets fail on purpose.
- **ci** is the routine regression check: 25 sets that passed with the same space group and steady
- **ci** is the routine regression check: 26 sets that passed with the same space group and steady
d_min and ISa in every full run of rc174 and rc175, with no warning that questions the lattice or
the group (supercell, harmonic, twinning, untested centring). It spans HDF5, miniCBF, cbf.gz,
marCCD (both formats) and SMV; every crystal system, centred and high-symmetry lattices, small
molecules, anomalous and long-wavelength data, pink beam, a twinnable P31 and one large set
(8qaw, about 2 minutes) - about 10 minutes of rugnux in all on the RTX 5080. A set that starts
failing for a known, accepted reason leaves the tier; one that is merely slow stays.
failing for a known, accepted reason leaves the tier; one that is merely slow stays. KDP joined
later than the rest: it passes as an `indistinguishable_alternative` (I41md for I-42d), and its
twinning warning is most likely a false alarm from four heavy atoms in the cell.
**Exit status.** `run` exits with status 1 when a ci-tier set in the run failed - a wrong lattice
or space group, a crash, a timeout or a missing input - and names those sets. This holds in any
@@ -448,10 +465,11 @@ was run `--unforced`), so its forced XDS-arm rows are left out the same way.
|---|---|
| `set`, `arm`, `tags`, `input`, `cmd` | the set's id, arm, population tags, input file, and the exact command that ran |
| `verdict` | `pass`, `fail`, `unscored` (no reference, or a question the data could not answer) or `not_run` (input missing) |
| `cause` | why it failed: `crash`, `reader`, `indexing`, `timeout`, `lattice_halved`, `lattice_doubled`, `lattice_other`, `sym_under`, `sym_over`, `sym_screw`, `sym_other`, `false_lattice`; or `no_reference` / `no_input` / `reference_problem` / `screw_undetermined`; or, on a pass, `accepted_alternative` |
| `cause` | why it failed: `crash`, `reader`, `indexing`, `timeout`, `lattice_halved`, `lattice_doubled`, `lattice_other`, `sym_under`, `sym_over`, `sym_screw`, `sym_other`, `false_lattice`; or `no_reference` / `no_input` / `reference_problem` / `screw_undetermined`; or, on a pass, `accepted_alternative` or `indistinguishable_alternative` |
| `accepted_alt`, `accepted_alt_why` | the alternative reference the answer matched and the reason the row accepts it, on a row that passed that way; null otherwise |
| `reason` | one line for a human |
| `sgno`, `sg`, `pg` / `sgno_ref`, `sg_ref`, `pg_ref` | space group number and name, and point group: ours (the data's own determination) / the reference's |
| `sg_alternatives` | rugnux's `SPACE_GROUP_ALTERNATIVES` as written (`\|`-separated), else null |
| `sg_label` | the space group rugnux reported where `--model` put the model's enantiomorph on it, else null |
| `sg_relation` | open arm: how the two groups relate (`sgequiv`) |
| `cell`, `cell_ref`, `cell_dev_pct`, `volume_ratio` | cells, the largest edge deviation in % (over axis orders), and the primitive volume ratio |
+1 -1
View File
@@ -43,7 +43,7 @@
{"id": "citricacid_x10sa_20keV", "input": "citricacid_x10sa_20keV/citricacid_1_005_master.h5", "wavelength": 0.61993, "cod": "5000063", "tags": ["h5", "monoclinic", "small-molecule"], "ref": {"sgno": 4, "cell": [11.454, 5.579, 12.67, 90.0, 111.587, 90.0], "anomalous": true, "isa": 20.44, "completeness": 82.0, "r_meas": 0.041, "cc_half": 0.997, "multiplicity": 3.1, "dmin_low": 2.02, "r_meas_low": 0.05, "cc_half_high": 0.997, "dmin": 0.68, "dmin_rule": "xds_range", "dmin_xds": 0.68, "dmax": 50.0}, "ref_override": {"sg": "P 1 21/c 1", "sgno": 14}, "ref_override_why": "anhydrous citric acid, P 21/a (P 21/c in the standard setting); COD 5000063 - J. P. Glusker, J. A. Minkin, A. L. Patterson, Acta Cryst. B25 (1969) 1066-1072, doi:10.1107/S0567740869003542. XDS reports only Sohncke groups (P 21)"},
{"id": "hepes_x10sa_20keV", "input": "hepes_x10sa_20keV/hepes_1_004_master.h5", "wavelength": 0.61993, "cod": "2224210", "tags": ["h5", "orthorhombic", "small-molecule"], "ref": {"sgno": 19, "cell": [8.348, 9.575, 27.068, 90.0, 90.0, 90.0], "anomalous": true, "isa": 28.49, "completeness": 91.8, "r_meas": 0.031, "cc_half": 0.999, "multiplicity": 5.66, "dmin_low": 2.0, "r_meas_low": 0.037, "cc_half_high": 0.998, "dmin": 0.68, "dmin_rule": "xds_range", "dmin_xds": 0.68, "dmax": 50.0}, "ref_override": {"sg": "P b c a", "sgno": 61}, "ref_override_why": "HEPES, P b c a; COD 2224210 (100 K, 8.341 9.567 27.066) - P. Sledz, T. Minor, M. Chruszcz, Acta Cryst. E65 (2009) o3027-o3028, doi:10.1107/S1600536809042512. XDS reports only Sohncke groups (P 21 21 21)"},
{"id": "yag_x10sa_20keV", "input": "yag_x10sa_20keV/yag_2_004_master.h5", "wavelength": 0.61993, "cod": "2003066", "tags": ["h5", "cubic", "small-molecule"], "ref": {"sgno": 211, "cell": [11.997, 11.997, 11.997, 90.0, 90.0, 90.0], "anomalous": true, "isa": 3.18, "completeness": 99.0, "r_meas": 0.432, "cc_half": 0.947, "multiplicity": 28.44, "dmin_low": 1.99, "r_meas_low": 0.436, "cc_half_high": 0.885, "dmin": 0.68, "dmin_rule": "xds_range", "dmin_xds": 0.68, "dmax": 50.0}, "ref_override": {"sg": "I a -3 d", "sgno": 230}, "ref_override_why": "Y3Al5O12 garnet, I a -3 d; COD 2003066 - A. Nakatsuka, A. Yoshiasa, T. Yamanaka, Acta Cryst. B55 (1999) 266-272, doi:10.1107/S0108768198012567. XDS reports only Sohncke groups (I 4 3 2)", "tiers": {"ci": "small molecule, cubic Ia-3d, 0.66 A"}},
{"id": "kdp_x10sa_20keV", "input": "kdp_x10sa_20keV/kdp_1_003_master.h5", "wavelength": 0.61993, "cod": "9009999", "tags": ["h5", "tetragonal", "small-molecule"], "ref": {"sgno": 98, "cell": [7.425, 7.425, 6.924, 90.0, 90.0, 90.0], "anomalous": true, "isa": 4.1, "completeness": 99.2, "r_meas": 0.241, "cc_half": 0.969, "multiplicity": 6.14, "dmin_low": 2.06, "r_meas_low": 0.172, "cc_half_high": 0.911, "dmin": 0.74, "dmin_rule": "xds_range", "dmin_xds": 0.74, "dmax": 50.0}, "ref_override": {"sg": "I -4 2 d", "sgno": 122}, "ref_override_why": "KH2PO4 (KDP), tetragonal paraelectric phase, I -4 2 d; COD 9009999 - R. J. Nelmes, G. M. Meyer, J. E. Tibballs, J. Phys. C 15 (1982) 59-75, doi:10.1088/0022-3719/15/1/005. XDS reports only Sohncke groups (I 41 2 2)"},
{"id": "kdp_x10sa_20keV", "input": "kdp_x10sa_20keV/kdp_1_003_master.h5", "wavelength": 0.61993, "cod": "9009999", "tags": ["h5", "tetragonal", "small-molecule"], "ref": {"sgno": 98, "cell": [7.425, 7.425, 6.924, 90.0, 90.0, 90.0], "anomalous": true, "isa": 4.1, "completeness": 99.2, "r_meas": 0.241, "cc_half": 0.969, "multiplicity": 6.14, "dmin_low": 2.06, "r_meas_low": 0.172, "cc_half_high": 0.911, "dmin": 0.74, "dmin_rule": "xds_range", "dmin_xds": 0.74, "dmax": 50.0}, "ref_override": {"sg": "I -4 2 d", "sgno": 122}, "ref_override_why": "KH2PO4 (KDP), tetragonal paraelectric phase, I -4 2 d; COD 9009999 - R. J. Nelmes, G. M. Meyer, J. E. Tibballs, J. Phys. C 15 (1982) 59-75, doi:10.1088/0022-3719/15/1/005. XDS reports only Sohncke groups (I 41 2 2)", "tiers": {"ci": "small molecule, tetragonal I-42d; I41md has the same absences"}},
{"id": "lcystine_x10sa_20keV", "input": "lcystine_x10sa_20keV/lcystine_2_004_master.h5", "wavelength": 0.61993, "cod": "1513328", "tags": ["h5", "hexagonal", "small-molecule"], "ref_override": {"sg": "P 61 2 2", "sgno": 178, "cell": [5.422, 5.422, 56.275, 90.0, 90.0, 120.0]}, "ref_override_why": "hexagonal L-cystine, P 61 2 2; COD 5000005 - B. M. Oughton and P. M. Harrison, Acta Cryst. 12 (1959) 396-404, doi:10.1107/S0365110X59001177. No XDS reference: XDS indexes under 10% of the spots of either sweep, so the row is scored on lattice and symmetry only"},
{"id": "lcystine_x10sa_25keV", "input": "lcystine_x10sa_25keV/lcystine_2_003_master.h5", "wavelength": 0.49594, "cod": "1513328", "tags": ["h5", "hexagonal", "small-molecule"], "ref_override": {"sg": "P 61 2 2", "sgno": 178, "cell": [5.422, 5.422, 56.275, 90.0, 90.0, 120.0]}, "ref_override_why": "hexagonal L-cystine, P 61 2 2; COD 5000005 - B. M. Oughton and P. M. Harrison, Acta Cryst. 12 (1959) 396-404, doi:10.1107/S0365110X59001177. No XDS reference: XDS indexes under 10% of the spots of either sweep, so the row is scored on lattice and symmetry only"}
]}
+22 -1
View File
@@ -224,6 +224,9 @@ def judge(entry, rep, run_note):
"rmodel": fnum(rep.get("R_MODEL")), "rmodel_shell_scaled": fnum(rep.get("R_MODEL_SHELL_SCALED")),
"radial_misfit": fnum(rep.get("MODEL_RADIAL_MISFIT")),
"sg_label": None, "accepted_alt": None, "accepted_alt_why": None,
# the groups rugnux could not tell from the one it wrote (and, with an undetermined screw,
# the screw alternatives), as it lists them
"sg_alternatives": rep.get("SPACE_GROUP_ALTERNATIVES") or None,
}
# the same merge binned over the reference's range (--report-resolution, XDS arms)
r.update({k: fnum(rep.get(k.upper())) for k in REFRES_KEYS})
@@ -322,15 +325,33 @@ def judge(entry, rep, run_note):
# judged - unless the group was measured by hand (sg_measured).
by_sg = entry["arm"] == "open" or cand.get("sg_measured")
pg_cand = point_group(cand["sgno"])
# Our group in our own setting: does some setting of the reference show the same Laue
# rotations and the same absences? Then intensities could never have told them apart.
same = sgequiv.same_intensities(space_group(r["sg"], r["sgno"]), cand["sgno"])
if by_sg:
rel = sgequiv.describe(cand["sgno"], r["sgno"])
n["sg_relation"] = rel
ok = rel in ("match", "hand only (needs anomalous)", "UNDECIDABLE from intensities")
ok = (rel in ("match", "hand only (needs anomalous)")
or (rel == "UNDECIDABLE from intensities" and same))
else:
ok = r["pg"] == pg_cand
if ok:
return n, None
sg_cand = cand.get("sg") or sg_name(cand["sgno"])
if same:
# A different point group of the same Laue class with exactly the same absences
# (I41md vs I-42d, Pc vs P2/c, Pna21 vs Pnma, P2 vs P2/m): the merged intensities hold
# nothing that separates them, only a model or a strong anomalous signal does. rugnux
# must still write one group and lists the rest in SPACE_GROUP_ALTERNATIVES. Its answer
# is as right as the data allow, so the row passes, with its own cause. Being listed
# alone is NOT enough: the list also carries the screw alternatives of an undetermined
# axis, whose absences differ, and those keep the screw rule below.
offered = {g.number for g in (gemmi.find_spacegroup_by_name(x.strip()) for x in
(rep.get("SPACE_GROUP_ALTERNATIVES") or "").split("|")) if g}
listed = "listed" if cand["sgno"] in offered else "NOT listed"
return n, ("pass", "indistinguishable_alternative",
f"{r['sg']} vs reference {sg_cand}: same Laue class and absences, "
f"undecidable from intensities ({listed} in SPACE_GROUP_ALTERNATIVES)")
order = len(gemmi.find_spacegroup_by_number(r["sgno"]).operations().sym_ops)
order_ref = len(gemmi.find_spacegroup_by_number(cand["sgno"]).operations().sym_ops)
if r["pg"] == pg_cand:
+43 -14
View File
@@ -1,9 +1,17 @@
"""Which space-group differences a diffraction experiment could actually have decided.
A merged dataset sees only (a) which reflections are equivalent - the rotation parts of the
operations in their real orientation - and (b) which are systematically absent. Two space
groups agreeing on both cannot be told apart without solving the structure. Body centring is
the usual culprit: h+k+l=2n already forces h00: h=2n, so a 2_1 along <100> adds nothing.
A merged dataset sees only (a) which reflections are equivalent in intensity - the rotation parts
of the operations together with their negatives (Friedel's law), i.e. the Laue group in its real
orientation - and (b) which are systematically absent. Two space groups agreeing on both cannot be
told apart without solving the structure (or an anomalous signal strong enough to break Friedel's
law). Two families:
- same point group: body centring is the usual culprit - h+k+l=2n already forces h00: h=2n, so a
2_1 along <100> adds nothing (I222 = I212121); and the two hands of an enantiomorphic pair.
- different point groups of one Laue class with the same absences: acentric vs centric (P2 vs
P2/m, Pc vs P2/c, Pna21 vs Pnam) and the two acentric groups of one class (I41md vs I-42d).
The comparison is made per setting: two numbers are related if some setting of the one agrees with
some setting of the other (Pna21 matches Pnma as Pnam, not in the standard settings of both).
"""
import gemmi, collections
@@ -11,37 +19,58 @@ _N = 7
_HKL = [(h, k, l) for h in range(-_N, _N+1) for k in range(-_N, _N+1) for l in range(-_N, _N+1)
if (h, k, l) != (0, 0, 0)]
def _signature(sg):
def signature(sg):
"""What the intensities of a crystal in this space group (a gemmi.SpaceGroup, in its setting)
can show: the Laue rotations, the centring and the absences."""
ops = sg.operations()
return (frozenset(tuple(map(tuple, op.rot)) for op in ops),
rots = [tuple(map(tuple, op.rot)) for op in ops]
return (frozenset(rots) | frozenset(tuple(tuple(-x for x in row) for row in r) for r in rots),
frozenset(tuple(t) for t in ops.cen_ops),
tuple(ops.is_systematically_absent(h) for h in _HKL))
_SIGS = collections.defaultdict(set) # number -> the signatures of its settings
_by_sig = collections.defaultdict(set)
for _n in range(1, 231):
_by_sig[_signature(gemmi.find_spacegroup_by_number(_n))].add(_n)
CLASS = {n: frozenset(s) for s in _by_sig.values() for n in s if len(s) > 1}
for _sg in gemmi.spacegroup_table():
_s = signature(_sg)
_SIGS[_sg.number].add(_s)
_by_sig[_s].add(_sg.number)
CLASS = collections.defaultdict(set)
for _s in _by_sig.values():
for _n in _s:
CLASS[_n] |= _s - {_n}
def indistinguishable(a, b):
"""True if intensities alone could never have separated these two space groups."""
"""True if intensities alone could never have separated these two space groups (numbers), in
some setting of each."""
return a == b or b in CLASS.get(a, ())
def same_intensities(sg, number):
"""True if some setting of space group `number` shows exactly what `sg` (a gemmi.SpaceGroup, in
its own setting) shows: the same Laue rotations and the same absences."""
return signature(sg) in _SIGS[number]
def point_group(n):
return gemmi.find_spacegroup_by_number(n).point_group_hm()
def enantiomorphic(a, b):
"""True if the pair differs only by hand - anomalous signal can break it, absences cannot."""
return (indistinguishable(a, b) and a != b
return (indistinguishable(a, b) and a != b and point_group(a) == point_group(b)
and gemmi.find_spacegroup_by_number(a).is_enantiomorphic())
def describe(a, b):
if a == b: return "match"
if enantiomorphic(a, b): return "hand only (needs anomalous)"
if indistinguishable(a, b): return "UNDECIDABLE from intensities"
if indistinguishable(a, b):
if point_group(a) == point_group(b): return "UNDECIDABLE from intensities"
return "point group UNDECIDABLE from intensities (same Laue class and absences)"
return "REAL difference (absences differ)"
if __name__ == "__main__":
seen = set()
for n, s in sorted(CLASS.items()):
if s in seen: continue
s = frozenset(s | {n})
if len(s) < 2 or s in seen: continue
seen.add(s)
ns = sorted(s)
print(" ", " = ".join("%d %s" % (i, gemmi.find_spacegroup_by_number(i).hm) for i in ns),
" ->", describe(ns[0], ns[1]))
" ->", "; ".join(describe(ns[0], i) for i in ns[1:]))
+50
View File
@@ -141,5 +141,55 @@ class ScrewUndetermined(unittest.TestCase):
self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_screw"))
# Space groups of one Laue class with identical absences: the merged intensities cannot separate
# them. Synthetic cells.
TETRA_I = [7.4, 7.4, 6.9, 90.0, 90.0, 90.0]
MONO = [11.0, 6.5, 11.0, 90.0, 96.0, 90.0]
ORTHO_SMALL = [8.3, 9.6, 27.0, 90.0, 90.0, 90.0]
class IndistinguishableAlternatives(unittest.TestCase):
def judge(self, arm, ref_sg, ref_sgno, cell, sg, sgno, alternatives=""):
e = {"id": "test_same_absences", "arm": arm,
"ref": {"sg": ref_sg, "sgno": ref_sgno, "cell": cell}}
return score.judge(e, report(sg, sgno, cell, SPACE_GROUP_ALTERNATIVES=alternatives), "")
def test_same_absences_pass_on_both_arms(self):
"""Acentric vs acentric (I41md / I-42d), acentric vs centric (Pc / P2/c, Pna21 / Pnma,
P2 / P2/m): a pass with its own cause, whichever arm, whichever way round."""
cases = [("I -4 2 d", 122, TETRA_I, "I 41 m d", 109),
("P 1 2/c 1", 13, MONO, "P 1 c 1", 7),
("P 1 c 1", 7, MONO, "P 1 2/c 1", 13),
("P n m a", 62, ORTHO_SMALL, "P n a m", 62), # same number: plain match
("P n a 21", 33, ORTHO_SMALL, "P n a m", 62),
("P 1 2 1", 3, MONO, "P 1 2/m 1", 10)]
for arm in ("open", "inhouse"):
for ref_sg, ref_sgno, cell, sg, sgno in cases:
r = self.judge(arm, ref_sg, ref_sgno, cell, sg, sgno)
want = (("pass", None) if sgno == ref_sgno
else ("pass", "indistinguishable_alternative"))
self.assertEqual((r["verdict"], r["cause"]), want, (arm, ref_sg, sg))
def test_the_reason_says_whether_rugnux_listed_it(self):
r = self.judge("inhouse", "I -4 2 d", 122, TETRA_I, "I 41 m d", 109, "I -4 2 d")
self.assertIn("listed in SPACE_GROUP_ALTERNATIVES", r["reason"])
self.assertEqual(r["sg_alternatives"], "I -4 2 d")
r = self.judge("inhouse", "I -4 2 d", 122, TETRA_I, "I 41 m d", 109)
self.assertIn("NOT listed", r["reason"])
def test_different_absences_still_fail(self):
"""Same Laue class, different absences: a real difference, listed or not."""
r = self.judge("open", "P 1 21/c 1", 14, MONO, "P 1 21 1", 4, "P 1 21/c 1")
self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_under"))
r = self.judge("open", "I -4 2 d", 122, TETRA_I, "I 41 2 2", 98, "I -4 2 d")
self.assertEqual(r["verdict"], "fail")
r = self.judge("inhouse", "I -4 2 d", 122, TETRA_I, "I 4 m m", 107, "I -4 2 d")
self.assertEqual(r["verdict"], "fail")
def test_different_laue_class_still_fails(self):
r = self.judge("open", "P 4 2 2", 89, [50.0, 50.0, 70.0, 90, 90, 90], "P 4", 75, "P 4 2 2")
self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_under"))
if __name__ == "__main__":
unittest.main()