From 79ec61c141f71e99f4be1b7abebc14b60204b256 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 8 Oct 2026 20:50:20 +0200 Subject: [PATCH] rugnux: a first pass the schemes cannot index also tries a wedge from the end of the first part Since the decisions before pass 2 read only the first part of a rotation sweep (5b9144410), the first pass samples [start, PrepassEnd) with its two schemes: 'spread' (100 frames over it) and 'wedge' (consecutive frames from the start up to the accumulation cap). Which orientations a sweep passes through first is chance, and a sparse pattern can open on frames that do not show the long axis: on 5nw5 (P212121, 92 x 170 x 390 A, 10-25 spots a frame, 0.1 deg frames) the spread over the first 120 deg returned a C-centred 955 A cell at every density tried (60-200 frames: 0-10/60 validation frames) and the wedge a wrong one (0/60); the FFT never offered the true cell. The run kept the 5/60 cell as a sparse lattice and merged P2_1 at 2.43x the deposited primitive volume, ISa 2.7. The spread over 0-90, 30-150, 60-180 or 0-180 deg indexes 57-59/60, and consecutive frames from 60 deg on 60/60: the second half of the first part carries the lattice. Where the two schemes index no majority of the validation frames, pick_best now also indexes 'end wedge' - the first part read back from its end up to the accumulation cap - and judges it against them as any later scheme is (clearly more frames, or the axis-harmonic arbiter). It stays inside [start, PrepassEnd), so nothing before pass 2 waits for the rest of the sweep. It is not asked on a run the schemes index: offered there unconditionally it only added a lattice for the axis-harmonic arbiter, which took a 3x hexagonal supercell over an I-cubic crystal's cell on a spot count. The measured-centre arm of the beam-centre check takes it the same way, whether its schemes were indexed speculatively or not. Battery subset vs rc175 7c9a6c (GPU): 5nw5 fail -> pass (P212121, vol 0.996, d 7.04, ISa 8.42, R-free(model) 0.462; baseline 7.06 / 7.76 / 0.464); myob_x06da_powder_2 d 2.26 -> 1.47 A (baseline 1.47), ISa 2.11 -> 2.34; p.mtz md5-identical on 8v4o, 9rci, 5ky6, insu_I_x06da_low_isa, nothing_1, nothing_2, 7atg, 9qw8, 5reo. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi --- rugnux/RugnuxFirstPass.cpp | 43 +++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/rugnux/RugnuxFirstPass.cpp b/rugnux/RugnuxFirstPass.cpp index 599a59494..7747fab12 100644 --- a/rugnux/RugnuxFirstPass.cpp +++ b/rugnux/RugnuxFirstPass.cpp @@ -707,13 +707,23 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { {"spread", spread_ordinals}, {"wedge", all_consecutive}, }; + // A third sample of the same frames, asked only when the two above index no majority (see + // pick_best): the wedge read back from the END of the first part. Which orientations a sweep + // passes through first is chance, and the first part can open on frames that see the long axis + // badly - measured on a sparse crystal with a ~390 A axis, the spread and the wedge over its + // first 120 deg returned wrong cells (5/60 and 0/60 validation frames) at every spread density + // tried, while consecutive frames from 60 deg on indexed 60/60. Still inside [start, + // PrepassEnd), so nothing before pass 2 waits for the rest of the sweep. + const std::vector>> end_wedge = { + {"end wedge", {all_consecutive.rbegin(), all_consecutive.rend()}}, + }; // indexer_error: what the indexer threw, if it threw. An empty result with no error is a // verdict on the crystal; an empty result with one is a verdict on the machine. struct FirstPass { std::optional result; int score = -1; std::string name; double vol = 0.0; std::optional indexer_error; }; - // Feed both schemes (single-threaded spot cache), ready to be indexed. + // Feed every scheme (single-threaded spot cache), ready to be indexed. using SchemeIndexers = std::vector>; auto feed_schemes = [&](IndexerThreadPool &pool, const DiffractionExperiment *x = nullptr) { SchemeIndexers ris; @@ -740,8 +750,13 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { // Keep the lattice of the indexed schemes that indexes the most validation frames on the real // per-image path. Scored against the spot lists and settings the run stands on now, which must // be those the schemes were fed under. - auto score_schemes = [&](const SchemeIndexers &ris, IndexAndRefine &idx) -> FirstPass { - FirstPass bp; + // `named` names the indexers (`schemes` unless given) and `bp` is the standing pass they are + // judged against (none unless given). + auto score_schemes = [&](const SchemeIndexers &ris, IndexAndRefine &idx, + const std::vector>> *named = nullptr, + FirstPass bp = {}) -> FirstPass { + if (!named) + named = &schemes; for (size_t i = 0; i < ris.size(); i++) { if (cancelled_) break; @@ -752,7 +767,7 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { continue; RotationIndexerResult result = std::move(*found); int score = count_indexed(idx, result); - const std::string &name = schemes[i].first; + const std::string &name = (*named)[i].first; logger.Info("First-pass scheme '{}': indexes {}/{} validation frames", name, score, static_cast(validation.size())); @@ -919,7 +934,19 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { return bp; }; - // Feed both schemes, index them concurrently, and keep the best (score_schemes). + // Where the schemes index no majority of the validation frames, the end wedge is asked too and + // judged against them as any later scheme is. Only then: on a run the schemes index it would be + // one more lattice for the axis-harmonic arbiter, which is not decided on frame counts. + auto add_end_wedge = [&](FirstPass bp, IndexerThreadPool &pool, IndexAndRefine &idx) -> FirstPass { + if (cancelled_ || bp.score > static_cast(validation.size()) / 2) + return bp; + SchemeIndexers ris; + ris.push_back(feed_scheme(pool, end_wedge[0].second)); + run_indexing({ris[0].get()}); + return score_schemes(ris, idx, &end_wedge, std::move(bp)); + }; + + // Feed the schemes, index them concurrently, and keep the best (score_schemes, add_end_wedge). // alongside: started once the schemes are fed and before they are indexed, to run beside them. auto pick_best = [&](IndexerThreadPool &pool, IndexAndRefine &idx, const std::function &alongside = {}) -> FirstPass { @@ -930,7 +957,7 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { for (const auto &ri : ris) rp.push_back(ri.get()); run_indexing(rp); - return score_schemes(ris, idx); + return add_end_wedge(score_schemes(ris, idx), pool, idx); }; // Powder-ring rescue - and, with it, the lean seed the rotation first pass never had. @@ -1245,7 +1272,7 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { return std::nullopt; }; - // Find the spots of everything the first pass is certain to want in one go - both schemes' + // Find the spots of everything the first pass is certain to want in one go - the schemes' // opening frames and the validation set. A batch of workers costs an analysis engine each, // which is far more than a frame is to analyse, so one batch for the lot beats one per scheme. { @@ -1512,7 +1539,7 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { entries.emplace_back(centre_spec->key, centre_spec->spots); } } - alt = score_schemes(centre_spec->ris, *indexer); + alt = add_end_wedge(score_schemes(centre_spec->ris, *indexer), *indexer_pool, *indexer); } else alt = pick_best(*indexer_pool, *indexer); centre_spec.reset();