Seed still indexing with the strongest spots; refine with all

On flooded or noisy still frames (weakly-diffracting detectors, XFEL background,
ice) the full spot list derails the known-cell indexer: its many spurious peaks
compete with the true reflections for the search, so genuinely diffracting frames
fail to index.

Seed the indexer with a few spot-count subsets (30 / 80 / all) and keep the lattice
that explains the largest FRACTION of its own seed -- a lean, clean seed that a good
lattice indexes almost fully beats a flooded seed it fits only in small part. This
auto-selects a lean seed on noisy frames and the full seed where the extra spots are
real signal, with no per-dataset setting. Geometry refinement and integration still
use the full spot list (the orientation refiner filters spots by lattice match, so
the flood is ignored while high-resolution spots are kept), so resolution is
preserved. Costs at most ~3 indexer calls per frame, only on frames that do not
index on the first, lean seed.

Lifts the indexed-crystal yield on mildly-flooded synchrotron serial data with no
regression elsewhere. Stills only; the rotation indexing path is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-24 00:07:24 +02:00
co-authored by Claude Opus 4.8
parent 7c5bedfd74
commit 17eed80ff9
+43 -9
View File
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <limits>
#include <cstdlib>
#include "IndexAndRefine.h"
@@ -94,17 +95,50 @@ IndexAndRefine::IndexingOutcome IndexAndRefine::DetermineLatticeAndSymmetry(Data
IndexingOutcome outcome(experiment);
// Convert input spots to reciprocal space
std::vector<Coord> recip;
recip.reserve(msg.spots.size());
for (const auto &i: msg.spots) {
if (index_ice_rings || !i.ice_ring)
recip.push_back(i.ReciprocalCoord(geom_));
// Seed the indexer with the strongest few spots first and escalate to more only if that fails.
// On flooded / noisy frames (XFEL, ice) a lean high-quality seed finds the lattice far more
// reliably than the full spot list, whose many spurious peaks derail the search; on clean frames
// the lean seed already works, so nothing is lost. The FULL spot list is still used for geometry
// refinement and integration downstream, so higher-resolution accuracy is preserved. Cost is
// ~1 indexer call on frames that index cleanly, up to 3 only on the hard ones. msg.spots is
// already ordered non-ice-first, strongest-first (FilterSpotsByCount), so the prefix IS the seed.
const float idx_tol = experiment.GetIndexingSettings().GetTolerance();
const float idx_tol_sq = idx_tol * idx_tol;
IndexerResult indexer_result;
bool any_executed = false;
float best_frac = -1.0f;
for (size_t seed_cap : {size_t{30}, size_t{80}, std::numeric_limits<size_t>::max()}) {
std::vector<Coord> recip;
recip.reserve(std::min<size_t>(seed_cap, msg.spots.size()));
for (const auto &i: msg.spots) {
if (index_ice_rings || !i.ice_ring) {
recip.push_back(i.ReciprocalCoord(geom_));
if (recip.size() >= seed_cap)
break;
}
}
auto res = indexer_->Run(experiment, recip);
any_executed |= res.executed;
if (!res.lattice.empty()) {
// Keep the seed the lattice explains the largest FRACTION of: a lean clean seed a good
// lattice indexes almost fully beats a flooded seed it fits only in small part. This
// auto-selects the lean seed on noisy frames (XFEL) and the full seed where the extra spots
// are real signal (weak synchrotron) -- no per-dataset setting.
const Coord a = res.lattice[0].Vec0(), b = res.lattice[0].Vec1(), c = res.lattice[0].Vec2();
int n = 0;
for (const auto &q : recip) {
const float hf = q * a, kf = q * b, lf = q * c;
const float dh = hf - std::round(hf), dk = kf - std::round(kf), dl = lf - std::round(lf);
if (dh * dh + dk * dk + dl * dl < idx_tol_sq) ++n;
}
const float frac = recip.empty() ? 0.0f : static_cast<float>(n) / recip.size();
if (frac > best_frac) { best_frac = frac; indexer_result = std::move(res); }
}
if (recip.size() < seed_cap) // already fed every available spot; a larger cap won't add any
break;
}
auto indexer_result = indexer_->Run(experiment, recip);
if (indexer_result.executed)
if (any_executed)
msg.indexing_result = false;
if (!indexer_result.lattice.empty()) {