From cd20c2c37872c2b518b80cd0d7934470d23a82a7 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 10 Oct 2026 09:27:50 +0200 Subject: [PATCH] rugnux: the no-crystal test asks its own significance, 1e-9 The final no-crystal test (RejectChanceLattice) used the 0.001 binomial level of the comparisons between candidate lattices. The lattice it tests is the winner of some fifty indexing attempts, each fitted to an accumulated cloud that already holds the long-lived reflections it then re-finds on the validation frames, so that null is optimistic by roughly the number of attempts. The two in-house no-crystal controls passed it by one or two distinct reflections (tails 10^-2.06 and 10^-2.70 against 10^-3). The test now asks 1e-9 (Z ~ 6); the rescue comparisons keep 0.001. With two reflections by chance the controls would now need 15 distinct reflections instead of 4-5. Real lattices are tens of decades past either level (weakest seen: 10^-83). Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi --- rugnux/Rugnux.cpp | 6 +++++- rugnux/Rugnux.h | 16 +++++++++++++++- rugnux/RugnuxFirstPassAccept.cpp | 2 +- tests/RotationScaleWalkTest.cpp | 11 +++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 659039ccc..6e7b86c7a 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -107,13 +107,17 @@ bool ValidationEvidencePrefers(const ValidationSpotEvidence ¤t, const Vali } bool ValidationEvidenceBeatsChance(const ValidationSpotEvidence &e) { + return ValidationEvidenceBeatsChance(e, std::erfc(SPOT_BUDGET_SIGNIFICANCE_Z / std::sqrt(2.0))); +} + +bool ValidationEvidenceBeatsChance(const ValidationSpotEvidence &e, double significance) { const int64_t n = e.reflections + e.reflections_by_chance; const double p = 1.0 / (1.0 + VALIDATION_NULL_DISPLACEMENTS); double tail = 0.0; for (int64_t j = e.reflections; j <= n; j++) tail += std::exp(std::lgamma(n + 1.0) - std::lgamma(j + 1.0) - std::lgamma(n - j + 1.0) + j * std::log(p) + (n - j) * std::log(1.0 - p)); - return tail < std::erfc(SPOT_BUDGET_SIGNIFICANCE_Z / std::sqrt(2.0)); + return tail < significance; } RotationScaleWalk WalkRotationScale(double first_fit, diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 0f2806303..1a49debf0 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -231,8 +231,22 @@ constexpr int VALIDATION_NULL_DISPLACEMENTS = 5; // 1 + VALIDATION_NULL_DISPLACEMENTS arrangements, so each of the reflections the arrangements take // between them falls on the recorded one with probability 1 / (1 + VALIDATION_NULL_DISPLACEMENTS); // the lattice is this crystal's when the recorded arrangement takes so many that the binomial tail -// is below the significance of SPOT_BUDGET_SIGNIFICANCE_Z. +// is below `significance` - by default that of SPOT_BUDGET_SIGNIFICANCE_Z. bool ValidationEvidenceBeatsChance(const ValidationSpotEvidence &e); +bool ValidationEvidenceBeatsChance(const ValidationSpotEvidence &e, double significance); + +// The significance the run's no-crystal test (RejectChanceLattice) asks of the lattice the first +// pass ended on - Z ~ 6 instead of the 0.001 the comparisons between candidate lattices use. The +// binomial null holds for a lattice chosen independently of the validation frames, and this one is +// not: it is the winner of some fifty indexing attempts (schemes, ladder rungs, beam centres, the +// short-axis pass), each fitting the orientation, cell and geometry to an accumulated cloud that +// already holds the long-lived reflections it then re-finds on the validation frames, while the +// displaced arrangements get none of that selection. That makes the 0.001 null optimistic by about +// the number of attempts, ~1.7 decades, before the fit's own freedom is counted. A real crystal's +// lattice, even a weak one on a few frames, clears the test by tens of decades, so the strict level +// costs a crystal nothing; with no crystal it asks about three times the stray reflections the +// selection can supply. +constexpr double NO_CRYSTAL_SIGNIFICANCE = 1e-9; // Whether `candidate` puts a larger share of its validation spots on its lattice, over and above what // a wrong spindle angle puts there, than `current` does - by more than the binomial noise of the two diff --git a/rugnux/RugnuxFirstPassAccept.cpp b/rugnux/RugnuxFirstPassAccept.cpp index a78b2c645..0ef4ec03b 100644 --- a/rugnux/RugnuxFirstPassAccept.cpp +++ b/rugnux/RugnuxFirstPassAccept.cpp @@ -158,7 +158,7 @@ void Rugnux::FirstPassRun::RejectChanceLattice(const FirstPass &best, const Verd fmt::format("bar {:.1f}", bar), static_cast(evidence.reflections) - bar, sd, "binomial on distinct reflections"); } - if (!ValidationEvidenceBeatsChance(evidence)) { + if (!ValidationEvidenceBeatsChance(evidence, NO_CRYSTAL_SIGNIFICANCE)) { { // Name the cell and Bravais class that was rejected. The commonest cause is a metric // symmetry promoted one class too far - the constrained cell then misses every diff --git a/tests/RotationScaleWalkTest.cpp b/tests/RotationScaleWalkTest.cpp index 7fc4e1abb..9b03e33bd 100644 --- a/tests/RotationScaleWalkTest.cpp +++ b/tests/RotationScaleWalkTest.cpp @@ -147,3 +147,14 @@ TEST_CASE("ValidationEvidenceBeatsChance_ExchangeableArrangements", "[RotationSc // As many at the recorded angles as at an average wrong one is no lattice. CHECK_FALSE(ValidationEvidenceBeatsChance({.reflections = 100, .reflections_by_chance = 500})); } + +TEST_CASE("ValidationEvidenceBeatsChance_NoCrystalSignificance", "[RotationScale]") { + // A handful of stray reflections passes 0.001 but not the no-crystal test's own level. + CHECK(ValidationEvidenceBeatsChance({.reflections = 5, .reflections_by_chance = 1})); + CHECK_FALSE(ValidationEvidenceBeatsChance({.reflections = 5, .reflections_by_chance = 1}, NO_CRYSTAL_SIGNIFICANCE)); + // Against two by chance it takes 15 (tail 10^-9.7), and 14 is not enough (10^-8.96). + CHECK(ValidationEvidenceBeatsChance({.reflections = 15, .reflections_by_chance = 2}, NO_CRYSTAL_SIGNIFICANCE)); + CHECK_FALSE(ValidationEvidenceBeatsChance({.reflections = 14, .reflections_by_chance = 2}, NO_CRYSTAL_SIGNIFICANCE)); + // A weak real lattice on a few frames is still tens of decades past it. + CHECK(ValidationEvidenceBeatsChance({.reflections = 115, .reflections_by_chance = 4}, NO_CRYSTAL_SIGNIFICANCE)); +}