From 0f3ccda779cb71f6ce14671214fa6c079b6a015d Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 15 Jul 2026 15:23:43 +0200 Subject: [PATCH] rugnux: fix de-novo rotation indexing adopting a spurious axis-multiple supercell Two-pass rotation indexing scored each first-pass scheme by how many validation frames it indexes, but a spurious axis multiple (2x/3x...) indexes every frame its true sub-cell does, so the count saturates - both schemes reach the same frame total - and the tie fell to whichever scheme ran first. When that first scheme's full-rotation FFT resolves a true axis only as a 2x/3x harmonic of its length (the fundamental can sit far below the harmonic in the full-360 cloud), it commits a multiplied cell and the dataset collapses to P1, even though the other scheme already found the true cell. Add an integer-supercell cross-scheme tie-break: when two schemes tie on frames but their cell volumes differ by a near-integer factor >=2, the larger is the spurious supercell and the smaller true cell is adopted, independent of scheme order. The near-integer test distinguishes a real axis multiplication from a centering coincidence (a rhombohedral H cell vs its C2 sub-cell is 1.5x and is left alone). Validated de-novo across the rotation regression battery: the affected dataset now indexes its correct space group, the override triggers only where an axis was multiplied, and every other dataset is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- rugnux/Rugnux.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 656e0d38..0639162f 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -466,6 +466,7 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { f.get(); int best_score = -1; + double best_vol = 0.0; std::string best_name; std::optional best_result; for (size_t i = 0; i < ris.size(); i++) { @@ -475,13 +476,40 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { if (!result.has_value()) continue; const int score = count_indexed(*result); + const double vol = std::abs(result->lattice.CalcVolume()); const std::string &name = schemes[i].first; logger.Info("First-pass scheme '{}': indexes {}/{} validation frames", name, score, static_cast(validation.size())); - // The first scheme sets the baseline; a later one wins only if it indexes clearly more - // frames (>10%), so a scheme that merely ties the default never displaces it. - if (!best_result.has_value() || static_cast(score) > best_score * 1.1f + 0.5f) { + + // A later scheme wins if it indexes clearly more frames (>10%). + const bool clearly_more = static_cast(score) > best_score * 1.1f + 0.5f; + + // Integer-supercell tie-break. The validation-frame count saturates - a spurious axis + // multiple (2x/3x...) indexes every frame its true cell does, so both schemes reach the same + // frame total and the count alone cannot tell them apart; the default then keeps whichever + // ran first. When the two schemes tie on frames but their cell volumes are related by an + // integer factor >=2, the larger cell is that spurious supercell (one FFT resolved a true + // axis only as a 2x/3x harmonic of its length - the full-rotation 'spread' cloud is prone to + // this) and the smaller is the true reduced cell: take it, regardless of scheme order. + // Requiring a near-integer ratio is what separates a real axis multiplication from a mere + // centering coincidence: a rhombohedral H cell vs its C2 sub-cell is 1.5x (non-integer) and + // is correctly left alone. + bool integer_subcell = false; + if (best_result.has_value() && !clearly_more && vol > 1.0 && best_vol > 1.0) { + const bool tied = static_cast(score) >= best_score * 0.9f - 0.5f; + const double ratio = (vol < best_vol) ? best_vol / vol : vol / best_vol; + const double nearest = std::round(ratio); + const bool integer_multiple = nearest >= 2.0 && std::abs(ratio - nearest) < 0.15; + integer_subcell = tied && integer_multiple && vol < best_vol; + } + + if (!best_result.has_value() || clearly_more || integer_subcell) { + if (integer_subcell) + logger.Info("Scheme '{}' cell (vol {:.0f}) is a {:.0f}x sub-cell of '{}' (vol {:.0f}) at " + "equal frame count - adopting the smaller true cell", name, vol, + std::round(best_vol / vol), best_name, best_vol); best_score = score; + best_vol = vol; best_name = name; best_result = result; }