symmetry: a zone whose absences were never measurable must not outrank a dead one

sum_u is a sum of max(0, E^2) over a zone's predicted-absent reflections, in
units of their control class's mean, so it is EXACTLY zero when every one of
them merged non-positive. The Beta tail then diverges and the clamp that caught
it made each such reflection worth about 690 nats.

That was harmless while the value only had to clear a bound of 20. Since the
absence evidence became the primary sort key, summed across zones with no
minimum count per zone, it has ranked the candidates - and it ranked them
backwards. A zone of two absences that were never measurable scores 1378 nats
where a genuine zone of six absences at 1% of its own row scores 22, so the
candidate claiming a screw on an UNMEASURED row beat the one whose rows are
actually dead, by sixty times. The comment on the ranking claimed an extra
condition "LOSES the zone's evidence when the row is not dead"; an unmeasurable
row could not lose, it won outright.

No measurement places a merged intensity at exactly zero, so sum_u is floored at
a thousandth of the control mean per absent reflection. The constant is an order
of magnitude below the precision any real merge reaches - a thousandth of the
row mean needs I/sigma ~ 1000 against that row, where ISa tops out near 40 - so
it can only remove the singularity, never suppress evidence a measurement could
have produced. Measured across the realistic range it changes no genuine zone at
all (22.0, 34.7 and 65.4 nats, unchanged to four figures) and takes the
unmeasurable ones to 13 and 36, below and around the bound respectively.

AbsenceEvidence moves to the header, where the comments already named it, so the
ordering it has to satisfy can be tested directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-08-31 07:17:26 +02:00
co-authored by Claude Opus 5
parent e0ff51e2a0
commit 8d719c1383
3 changed files with 63 additions and 9 deletions
+24
View File
@@ -490,3 +490,27 @@ TEST_CASE("SearchSpaceGroup does not promote triclinic data on a pseudo-orthorho
CHECK(result.best_space_group->number == 1);
CHECK(result.point_group_order == 1);
}
// 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
// was harmless while the number only had to clear a bound; it is now summed across zones and ranks the
// candidates, so it made a candidate claiming a screw on an UNMEASURED row beat one whose rows are
// actually dead. The evidence is scored through the same entry point for both kinds of absence.
TEST_CASE("AbsenceEvidence does not reward a zone that was never measurable", "[SearchSpaceGroup]") {
// 2 absences that all merged non-positive, against a control of 8...
const double unmeasurable = AbsenceEvidence(0.0, 2, 8);
// ...against a genuinely dead zone: 6 absences at 1% of their row's mean, same control.
const double genuine = AbsenceEvidence(0.06, 6, 8);
CHECK(std::isfinite(unmeasurable));
CHECK(unmeasurable < genuine); // the ordering that was inverted
CHECK(unmeasurable < 20.0); // and it does not clear min_screw_absence_evidence
// The floor is far below any real measurement, so a genuine zone is untouched by it.
CHECK(genuine == Catch::Approx(22.0).margin(0.2));
CHECK(AbsenceEvidence(0.22, 22, 8) == Catch::Approx(65.4).margin(0.3));
// More dead reflections still means more evidence, which is the property the sum relies on.
CHECK(AbsenceEvidence(0.0, 6, 8) > AbsenceEvidence(0.0, 2, 8));
}