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:
+24
-6
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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:]))
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user