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)); +}