From 4c050b576ceb5cba912e9d5727ecd39d566a676b Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 14 Jul 2026 21:53:08 +0200 Subject: [PATCH] rugnux: de-double spurious rotation supercell in candidate selection The two-pass rotation indexer could keep a doubled-axis supercell over its own primitive sub-cell: when the supercell won candidate (ci) order and the primitive cleared the indexed-fraction hysteresis by less than the 0.05 "prefer-earlier" margin, the supercell survived. On pding4_003 full data this gave P222 on a 65.6x131.3x173 cell instead of P422 on 65.6x65.6x173 - the orthorhombic metric forecloses the 4-fold before the symmetry search ever runs. The FFT peak-finding is correct (the primitive cell is among the candidates, just out-ranked). Add a sub-cell override to the selection loop: adopt a later candidate that is a genuinely smaller cell (> ROT_SUBCELL_VOLUME_RATIO=1.5x smaller volume; a doubling is 2x) indexing at least as many spots (within ROT_SUBCELL_FRAC_SLACK=0.02). That is the signature of a spurious doubling - the primitive always indexes >= its integer multiple, whereas a real superstructure's larger cell indexes MORE (kept by the existing clearly-more branch) and twins share the cell volume (untouched). Full 24-crystal rotation battery (pre-fix vs post-fix): pding4_003 P222->P422 (cell halves to 65.6x65.6x173, R_meas 6.6% CC1/2 99.9% ISa 11.9), pding4_001 holds P422, all 22 other crystals bit-identical in space group. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../rotation_indexer/RotationIndexer.cpp | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/image_analysis/rotation_indexer/RotationIndexer.cpp b/image_analysis/rotation_indexer/RotationIndexer.cpp index 540fe022..355e5b00 100644 --- a/image_analysis/rotation_indexer/RotationIndexer.cpp +++ b/image_analysis/rotation_indexer/RotationIndexer.cpp @@ -12,6 +12,14 @@ #include namespace { + // Sub-cell override thresholds used in candidate selection to undo a spurious axis doubling: + // a later candidate replaces the chosen cell when it is smaller by more than this volume ratio + // (a doubling is 2x, well past 1.5) and indexes within this fraction slack of it. The slack is + // far below the indexed-fraction gap a real superstructure opens between its true cell and its + // sub-cell, so genuine large cells are kept. + constexpr float ROT_SUBCELL_VOLUME_RATIO = 1.5f; + constexpr float ROT_SUBCELL_FRAC_SLACK = 0.02f; + // Re-express a primitive hexagonal/trigonal lattice in the conventional hexagonal setting // (a = b, gamma = 120). The Niggli-reduced primitive cell carries the two equal-length axes // at gamma = 60; replacing b with b - a opens that angle to 120 without changing the lattice. @@ -224,6 +232,7 @@ void RotationIndexer::RunIndexing() { // Assemble and select serially, in candidate order - identical to refining them one by one. float best_frac = -1.0f; + float best_vol = 0.0f; bool have_best = false; size_t best_ci = 0; XtalOptimizerData best_data; @@ -264,8 +273,21 @@ void RotationIndexer::RunIndexing() { // marginally-higher alternative from displacing the primary when both index poorly (e.g. a // twin, where the accumulated-spot fraction is a noisy proxy) - only a decisively better // cell (a superstructure's true cell vs its sublattice) takes over. - if (!have_best || (frac > best_frac + 0.05f && frac > 0.15f)) { + // Displace the current best when the candidate indexes clearly more, OR when it is a + // genuine sub-cell: a meaningfully smaller cell that still indexes at least as many spots. + // The sub-cell branch unmasks a spurious supercell (axis doubling): the primitive cell + // always indexes >= its integer multiple, so a doubled cell that wins ci-order by the + // hysteresis margin is overridden by its own primitive. A real superstructure's true + // (larger) cell indexes MORE than its sub-cell and is kept by the clearly-more branch; + // twin lattices share the cell volume, so this never disturbs twin selection. + const float cand_vol = std::abs(data.latt.CalcVolume()); + const bool clearly_more = frac > best_frac + 0.05f && frac > 0.15f; + const bool smaller_subcell = have_best && frac > 0.15f + && frac >= best_frac - ROT_SUBCELL_FRAC_SLACK + && cand_vol < best_vol / ROT_SUBCELL_VOLUME_RATIO; + if (!have_best || clearly_more || smaller_subcell) { best_frac = frac; + best_vol = cand_vol; have_best = true; best_data = std::move(data); best_sr = sr;