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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
This commit is contained in:
2026-10-08 21:37:20 +02:00
co-authored by Claude Opus 5.5
parent 7a3bf6c5cb
commit 79ec61c141
+35 -8
View File
@@ -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<std::pair<std::string, std::vector<int>>> 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<RotationIndexerResult> result; int score = -1; std::string name; double vol = 0.0;
std::optional<std::string> 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<std::unique_ptr<RotationIndexer>>;
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<std::pair<std::string, std::vector<int>>> *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<int>(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<int>(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<void()> &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();