Space-group search: take the operator disagreement as a median, not a mean

A merohedral twin mixes EVERY reflection with its twin mate, so it shifts the
whole distribution of |I1-I2|/(I1+I2). A minority of badly measured
reflections shifts only the tail. The mean cannot tell those apart; the median
is blind to the second and just as sensitive to the first.

Measured on real crystals, moving the statistic from the mean to the median
leaves genuine promotions where they are and pushes every twin up:

  genuine tetragonal    1.016 -> 1.013
  genuine lysozyme      1.051 -> 1.067
  genuine tetragonal    1.238 -> 1.231
  twin (-e 1050)        1.272 -> 1.447
  twin (-e 450)         1.280 -> 1.622
  twin (full)           1.441 -> 1.522
  twin (-e 600)         1.427 -> 2.010

The margin around the 1.25 bound widens from 2.7% (genuine 1.238 against twin
1.272 - uncomfortably tight for a decision that cannot be undone downstream)
to 17.5% (1.231 against 1.447). The bound itself does not move.

Rotation battery, 33 crystals in both detection modes: no point group changed
in either (30/33 and 29/33, as before), and only one crystal's numbers move at
all - the one already documented as nondeterministic between repeat runs of
the same binary. The synthetic twin-fraction x multiplicity grid passes
unchanged. So this buys margin, not outcomes.

Found while testing a different hypothesis, which the same measurement refuted:
a tetragonal crystal whose 422 promotion is wrongly refused reads 1.484 by the
mean and 1.472 by the median, i.e. its disagreement is distribution-wide and is
NOT a badly-integrated minority. That crystal's cause is elsewhere and is not
addressed here - see the note below.

  Its indexing-ambiguity operator (-k,-h,-l) lies INSIDE 422 but OUTSIDE 222,
  so the subgroup merge the search is given mixes lattices indexed in the two
  alternative hands. That corrupts exactly the 4-fold relationships and leaves
  the 2-fold ones intact - measured, the 222 step reads 0.917 and the 422 step
  1.484 - and the corruption is indistinguishable from a twin law. Forcing the
  tetragonal group merges the two hands as equivalent and the same data give
  CC1/2 99.2% at multiplicity 10.7, matching XDS. The failure is worse the
  BETTER the frames index (99.9% vs 63.3% for the run that gets it right),
  because indexing more frames picks up more of both hands.

  So no statistic computed on a subgroup merge can arbitrate a promotion whose
  added operators include an indexing-ambiguity operator. Fixing that means
  resolving the ambiguity before the search, or detecting the coincidence and
  deciding another way; the operators needed to detect it are already computed
  (the run warns about them).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-28 11:07:06 +02:00
co-authored by Claude Opus 5
parent ae126c3d5b
commit 11c7cab2e5
3 changed files with 28 additions and 20 deletions
+4 -3
View File
@@ -149,9 +149,9 @@ namespace {
return out;
}
// The statistic the promotion is actually decided on: the mean operator disagreement
// H = <|I1-I2|/(I1+I2)> over the operators the promotion ADDS, divided by the mean over the parent
// group's own operators, measured on the same reflections. Mirrors what SearchSpaceGroup computes
// The statistic the promotion is actually decided on: the operator disagreement
// H = median|I1-I2|/(I1+I2) over the operators the promotion ADDS, divided by the same over the
// parent group's own operators, measured on the same reflections. Mirrors what SearchSpaceGroup computes
// for the sub -> super step, so a test can report the margin the max_operator_h_ratio bound has.
//
// The parent normalisation is the whole design, not a detail. An ABSOLUTE per-operator bound cannot
@@ -172,6 +172,7 @@ namespace {
for (const auto& s : operators) {
if (s.n_pairs < 200) // SearchSpaceGroupOptions::min_pairs_for_h
continue;
// The median, which is what the promotion is gated on (SearchSpaceGroup).
if (parent_ops.count(s.op_triplet_hkl) > 0) { h_parent += s.h_stat; ++n_parent; }
else { h_added += s.h_stat; ++n_added; }
}