Space group: a screw zone's evidence no longer hangs on its largest absence

A screw's predicted-absent class is one axial row - half a dozen to a few dozen
reflections - and its evidence is a SUM over them, so it is decided by its
largest member. The file's own LIMIT comment said so; 7n2s is that limit firing
on real data. Between two scaling passes that differed only in which weak frames
were rejected, one of eight dead 0k0 moved from 14 +- 9 to 99 +- 10 while the
other seven did not move at all, and the zone fell from 30.1 nats to 17.1 and
lost the 2(1) under a bound of 20. That reflection was never measured to the
precision its sigma claimed: its two half-set merges read 198 and 2.5.

The zone's sum is now taken with its single largest member dropped and rescaled
for the trim - divided by n - H_n, the expected sum of the other n-1 under the
null, and multiplied back by n. ScrewZoneEvidence reads the result exactly as
before: same statistic, same floor, same bound, same calibration, with a robust
estimate of the zone's deadness in place of a fragile one. One member only,
whatever the zone holds: a zone with two strong absences is a zone that is not
extinct. On a uniformly dead zone the rescale under-states by 2.2 nats at eight
absences and 3.9 at sixty-four - it only ever refuses, never claims. Glide zones
keep the untrimmed sum: a plane holds hundreds to thousands of reflections and
no single one can carry the verdict.

7n2s -> P 1 21 1 (zone 27.6 nats, set by the seven reflections that did not
move), matching its deposit; a second monoclinic crystal decided six nats under
the bound (7 absent, 1 violation, 13.9 nats) reaches 21.9 and its 2(1) as well.
Unchanged on 7mzt, 7k1l, 11if, 9hs7, 9zlo and four in-house reference sets.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
This commit is contained in:
2026-09-20 18:45:17 +02:00
co-authored by Claude Opus 5
parent 3ca22c07c1
commit bac2d01c44
3 changed files with 82 additions and 4 deletions
+31
View File
@@ -499,6 +499,37 @@ TEST_CASE("SearchSpaceGroup does not promote triclinic data on a pseudo-orthorho
CHECK(result.point_group_order == 1);
}
// A screw zone is a handful of axial reflections and its evidence is a SUM over them, so it is
// decided by its largest member. Measured on a monoclinic crystal whose eight absent 0k0 are dead in
// every run: between two scaling passes that differed only in which weak frames were rejected, ONE
// of the eight moved from 14 +- 9 to 99 +- 10 (its two half-set merges reading 198 and 2.5, so it
// was never measured to the precision its sigma claimed) while the other seven did not move at all -
// and the zone went from 30.1 nats to 17.1 and lost the 2(1) under a bound of 20. Trimming the
// largest member and rescaling for the trim makes the two passes agree.
TEST_CASE("A screw zone's evidence does not hang on its largest absence", "[SearchSpaceGroup]") {
// Seven reflections at a hundredth of their row, and one that moved between the two passes.
const double dead_seven = 7 * 0.01;
const double before = ScrewZoneEvidence(TrimmedZoneSum(dead_seven + 0.03, 0.03, 8), 8);
const double after = ScrewZoneEvidence(TrimmedZoneSum(dead_seven + 0.39, 0.39, 8), 8);
CHECK(before == Catch::Approx(after).margin(0.01)); // the same seven reflections, the same verdict
CHECK(after > 20.0); // and the screw survives the move
// Untrimmed, that one reflection is the whole difference and it crosses the bound.
CHECK(ScrewZoneEvidence(dead_seven + 0.03, 8) > 20.0);
CHECK(ScrewZoneEvidence(dead_seven + 0.39, 8) < 20.0);
// Only ONE member is trimmed, whatever the zone holds: a zone with two strong absences is not a
// zone with a bad reflection in it, it is a zone that is not extinct.
CHECK(ScrewZoneEvidence(TrimmedZoneSum(dead_seven + 0.39 + 0.39, 0.39, 8), 8) < 20.0);
// On a uniform zone the rescale under-states rather than over-states - the safe direction.
CHECK(ScrewZoneEvidence(TrimmedZoneSum(8 * 0.01, 0.01, 8), 8) <
ScrewZoneEvidence(8 * 0.01, 8));
// A class that merged non-positive throughout is unchanged: sum and max are both zero, so the
// floor in ScrewZoneEvidence is what answers, exactly as before.
CHECK(TrimmedZoneSum(0.0, 0.0, 8) == 0.0);
}
// A zone whose predicted absences were never measurable must not outscore a zone that is genuinely
// dead. sum_u is a sum of max(0, E^2)/row_mean, so it is EXACTLY zero when every absent reflection in
// the zone merged non-positive - and the Beta tail then diverges, worth ~690 nats per reflection. That