From f51cc35c86da5455f34ac97d00f848a50d83dd4b Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 8 Nov 2026 22:39:52 +0100 Subject: [PATCH] First pass: ask the long-axis hypothesis once per run An axis long enough for the hypothesis is rare, and whether a crystal has one does not change between the passes of a run, yet every first pass - the refined-geometry pass, the probes, the distance-check passes - ran the coarse search again (thau: 0.36 s in Step 6 and 0.06-0.70 s in each later pass, offering nothing in any of them). The first pass that asks now leaves its answer on the run (Rugnux::long_axis_answer_, reset per RunAllPasses): a later pass keeps its standing cell where that pass kept one, and re-indexes straight at the cell it adopted otherwise, without the coarse search. Only the long-axis path changes; within a pass both centres of the beam-centre check are still asked. Rotation first pass, -N 28, 5950X + RTX 5080, machine shared with other jobs (median of 2, s): Step 3 / Step 6 / wall, after the speculation commit thau 1000 images: 1.65 / 0.84 / 15.0 -> 1.58 / 0.67 / 13.9 cytc: 1.50 / / 20.7 -> 1.35 / / 19.5 lyso: 1.40 / 0.65 / 12.7 -> 1.30 / 0.48 / 11.5 _report.txt (minus run-specific lines) and .hkl identical to rc184-port on these three and on a 13-set battery subset (lyso_x06da_ref, thau_x10sa_16keV, thau_x10sa_injection, insu_I_x06da_ref, cytc_x10sa, 6toc, 4bwl, 5vml, kdp_x10sa_20keV, and the long-axis sets 8v4o, 7d98, 4mtk, 4p8r): battery compare 0 changed, every report and .hkl equal. On a crystal with a ~1000 A axis (private) the run ends on the same cell, space group and resolution; its distance-check passes now re-index at the carried cell instead of searching, which moves its merging statistics in the fourth digit. Wall 70 -> 60 s. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK --- docs/CPU_DATA_ANALYSIS_INDEXING.md | 2 +- rugnux/Rugnux.h | 7 +++++ rugnux/RugnuxFirstPass.cpp | 3 +++ rugnux/RugnuxFirstPass.h | 5 ++++ rugnux/RugnuxFirstPassRescues.cpp | 43 ++++++++++++++++++++++-------- rugnux/RugnuxPasses.cpp | 1 + 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/CPU_DATA_ANALYSIS_INDEXING.md b/docs/CPU_DATA_ANALYSIS_INDEXING.md index 4e482dabc..c7a48a0c2 100644 --- a/docs/CPU_DATA_ANALYSIS_INDEXING.md +++ b/docs/CPU_DATA_ANALYSIS_INDEXING.md @@ -121,7 +121,7 @@ For each direction, the FFT magnitude spectrum is computed; peaks correspond to The running-mean background window keeps a **constant width** and is slid inward at the ends of the spectrum rather than truncated there, so a peak within half a window of either end — which is where the longest cells sit — is judged against as much background as any other. Both window bounds stay monotonically non-decreasing in the bin index, so the GPU kernel's running sum is still valid. -The longest basis vector the transform can return is `fft_max_unit_cell_A`, since the histogram is sized from it and its last usable bin *is* that length; the shortest is `fft_min_unit_cell_A` (`rugnux --fft-min-unit-cell`, default 10 Å), below which a candidate is discarded. The defaults are unchanged (500 Å and 10 Å), but the accepted range for the maximum now reaches 1200 Å, and a reference cell given with `-C` moves **both** bounds on its own — up to reach a long axis, down to admit a small-molecule cell — since a cell the search cannot represent cannot be found by it. A de-novo rotation run asks the long end a second time as a **long-axis hypothesis**: the detector separates the rows of an axis of length $L$ while their spacing near the beam, $D\lambda/L$, exceeds the measured spot width, so wherever $D\lambda/(p\,r_{80})$ ($p$ the pixel size, $r_{80}$ the pre-scan's spot width) exceeds the standard bound, a coarse search (3.5 Å histogram) to that length, at most 1200 Å, offers its lattices. Each is re-indexed at full resolution with its cell as the reference, and the one putting the largest share of the pooled validation spots on itself beyond chance replaces the standing lattice where that share is significantly larger (the pooled-spot comparison the first pass uses for every competing lattice) — except an exact superlattice of the standing cell, for which the pooled count is biased and which is left to the supercell probe of the merge. It is asked of every such run, whatever the standing pass indexed: a wrong axis along a fine row still indexes most frames. Its searches are started ahead of the question - the coarse one beside the file centre's schemes, the re-index of what it offers beside the beam-centre check - and the question then reads their answers from the indexer's memo: this changes when they run, not what they find. +The longest basis vector the transform can return is `fft_max_unit_cell_A`, since the histogram is sized from it and its last usable bin *is* that length; the shortest is `fft_min_unit_cell_A` (`rugnux --fft-min-unit-cell`, default 10 Å), below which a candidate is discarded. The defaults are unchanged (500 Å and 10 Å), but the accepted range for the maximum now reaches 1200 Å, and a reference cell given with `-C` moves **both** bounds on its own — up to reach a long axis, down to admit a small-molecule cell — since a cell the search cannot represent cannot be found by it. A de-novo rotation run asks the long end a second time as a **long-axis hypothesis**: the detector separates the rows of an axis of length $L$ while their spacing near the beam, $D\lambda/L$, exceeds the measured spot width, so wherever $D\lambda/(p\,r_{80})$ ($p$ the pixel size, $r_{80}$ the pre-scan's spot width) exceeds the standard bound, a coarse search (3.5 Å histogram) to that length, at most 1200 Å, offers its lattices. Each is re-indexed at full resolution with its cell as the reference, and the one putting the largest share of the pooled validation spots on itself beyond chance replaces the standing lattice where that share is significantly larger (the pooled-spot comparison the first pass uses for every competing lattice) — except an exact superlattice of the standing cell, for which the pooled count is biased and which is left to the supercell probe of the merge. It is asked of every such run, whatever the standing pass indexed: a wrong axis along a fine row still indexes most frames. It is asked **once per run**, by the first pass that reaches it: a later pass (at the refined geometry, or a probe) carries that answer, keeping its standing cell where the first one kept its own and otherwise re-indexing straight at the long-axis cell the first one adopted, without the coarse search. The searches themselves are started ahead of the question - the coarse one beside the file centre's schemes, the re-index of what it offers beside the beam-centre check - and the question then reads their answers from the indexer's memo: this changes when they run, not what they find. Candidate vectors are $\mathbf{v}_d = L_d\,\mathbf{u}_d$. diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index b062da167..ff0c84c5d 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -1552,6 +1552,13 @@ class Rugnux { // keeps the smaller), Niggli-reduced primitive: the cells an indexing scheme found as lattices in their own // right, which DoubledCellHypothesis reads the added class of. std::vector harmonic_offers_; + // The long-axis hypothesis's answer, from the first pass of this run that asked it: the cell it + // adopted, or none where it kept the standing cell. A later pass carries it instead of asking again + // (FirstPassRun::LongAxisHypothesis). + struct LongAxisAnswer { + std::optional adopted; + }; + std::optional long_axis_answer_; // The first pass's spot lists, kept across passes under everything a frame's list depends on // (SpotFindingKey): a probe and the canonical pass after it, or a pass repeating an earlier // pass's rescue, find the spots of the same frames under the same settings again. Shared with the diff --git a/rugnux/RugnuxFirstPass.cpp b/rugnux/RugnuxFirstPass.cpp index 7816abe10..f63d8eb7a 100644 --- a/rugnux/RugnuxFirstPass.cpp +++ b/rugnux/RugnuxFirstPass.cpp @@ -490,6 +490,9 @@ bool Rugnux::FirstPassRotationIndexing(PipelineLocals &p) { // The long-axis hypothesis, at the centre the run now stands on. if (!long_axis_asked) best = run.LongAxisHypothesis(std::move(best)); + // What later passes of the run carry (Rugnux::long_axis_answer_). + if (run.long_axis_asked_here || run.long_axis_adopted) + long_axis_answer_ = LongAxisAnswer{run.long_axis_adopted}; // The end wedge, at the geometry the rescues settled on. best = run.AddEndWedge(std::move(best), *p.indexer_pool, *p.indexer); run.ReleaseSpotEngines(); // no more frames to read; give the cards their memory back diff --git a/rugnux/RugnuxFirstPass.h b/rugnux/RugnuxFirstPass.h index 2e4ddfa2d..5dcf13326 100644 --- a/rugnux/RugnuxFirstPass.h +++ b/rugnux/RugnuxFirstPass.h @@ -363,4 +363,9 @@ struct Rugnux::FirstPassRun { std::vector long_axis_spec_key; void StartLongAxisSearch(std::unique_ptr x, const IndexingSettings &settings, bool own_pool); void JoinLongAxisSpeculation(); + + // Whether this pass asked the long-axis question itself, and the cell it adopted (see + // Rugnux::long_axis_answer_). + bool long_axis_asked_here = false; + std::optional long_axis_adopted; }; diff --git a/rugnux/RugnuxFirstPassRescues.cpp b/rugnux/RugnuxFirstPassRescues.cpp index 5d599e2fe..6e89db80d 100644 --- a/rugnux/RugnuxFirstPassRescues.cpp +++ b/rugnux/RugnuxFirstPassRescues.cpp @@ -201,7 +201,7 @@ void Rugnux::FirstPassRun::StartLongAxisSearch(std::unique_ptr indexing_bound_A)) @@ -274,8 +274,20 @@ auto Rugnux::FirstPassRun::LongAxisHypothesis(FirstPass standing) -> FirstPass { 1000.0f * rx.experiment_.GetPixelSize_mm(), resolvable_A, width_px, indexing_bound_A); return standing; } + // Asked once per run. An axis this long is rare, and whether the crystal has one does not change + // between the passes of a run, so a later pass carries the first answer: it keeps its standing cell + // where that pass kept one, and re-indexes at the cell it adopted otherwise - the coarse search, + // which is what finds the axis, is not run again. std::vector offered; - { + if (rx.long_axis_answer_) { + if (!rx.long_axis_answer_->adopted) { + logger.Info("Long-axis hypothesis: not asked again - an earlier pass of this run asked it and kept " + "the standing cell"); + return standing; + } + offered.push_back(*rx.long_axis_answer_->adopted); + } else { + long_axis_asked_here = true; IndexerThreadPool coarse_pool(LongAxisCoarseSettings(bound_A), IndexerConstruction::OnFirstUse, rx.gpu_work_beside_); const SchemeIndexers cris = FeedSchemes(coarse_pool); @@ -312,7 +324,7 @@ auto Rugnux::FirstPassRun::LongAxisHypothesis(FirstPass standing) -> FirstPass { c = PickBest(*indexer_pool, *indexer); } rx.experiment_.SetUnitCell(std::nullopt); // leave the space-group / cell determination de-novo - counts += fmt::format("; coarse {}", uc_text(coarse)); + counts += fmt::format("; {} {}", rx.long_axis_answer_ ? "carried" : "coarse", uc_text(coarse)); if (!c.result.has_value()) { counts += " re-indexes to nothing"; continue; @@ -351,15 +363,24 @@ auto Rugnux::FirstPassRun::LongAxisHypothesis(FirstPass standing) -> FirstPass { choice.reset(); } const double seconds = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); - logger.Info("Long-axis hypothesis: rows separated up to {:.0f} A ({:.2f} px spots), so the coarse search reached " - "{:.0f} A and offered {} lattice(s) other than the standing one{}; the standing cell puts {} on " - "itself - {}; {:.2f} s", - resolvable_A, width_px, bound_A, offered.size(), counts, - standing.result ? share(at_standing) : std::string("no lattice"), - choice ? "adopting " + cell_text(*constrained[*choice].result) : std::string("keeping the standing cell"), - seconds); - if (choice) + if (rx.long_axis_answer_) + logger.Info("Long-axis hypothesis: an earlier pass of this run adopted a long-axis cell, re-indexed here{}; " + "the standing cell puts {} on itself - {}; {:.2f} s", counts, + standing.result ? share(at_standing) : std::string("no lattice"), + choice ? "adopting " + cell_text(*constrained[*choice].result) : std::string("keeping the standing cell"), + seconds); + else + logger.Info("Long-axis hypothesis: rows separated up to {:.0f} A ({:.2f} px spots), so the coarse search reached " + "{:.0f} A and offered {} lattice(s) other than the standing one{}; the standing cell puts {} on " + "itself - {}; {:.2f} s", + resolvable_A, width_px, bound_A, offered.size(), counts, + standing.result ? share(at_standing) : std::string("no lattice"), + choice ? "adopting " + cell_text(*constrained[*choice].result) : std::string("keeping the standing cell"), + seconds); + if (choice) { + long_axis_adopted = constrained[*choice].result->lattice.GetUnitCell(); return std::move(constrained[*choice]); + } return standing; } diff --git a/rugnux/RugnuxPasses.cpp b/rugnux/RugnuxPasses.cpp index 383a2b5b2..3737a397b 100644 --- a/rugnux/RugnuxPasses.cpp +++ b/rugnux/RugnuxPasses.cpp @@ -365,6 +365,7 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) { // only while this holds one; a second Run() on the same object must not inherit the last one's. quality_guard_pass1_.reset(); harmonic_offers_.clear(); + long_axis_answer_.reset(); // Rotation two-pass geometry post-refinement: the first pass integrates at the header geometry and // post-refines the detector geometry (distance + beam from the observed spot positions, cell scale + axis // from phi_obs); the second pass re-indexes and re-integrates with the refined geometry. Only the second