Merge fc-trial: the stills geometry trial indexes its hypotheses side by side; the tilt fits run beside the pre-scan (decision-identical)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-23 12:50:54 +02:00
commit 3a7bb28ed2
3 files changed
+200 -69

No files matched your search

+9 -2
View File
@@ -871,6 +871,10 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
saturation = std::async(std::launch::async, [&] { return SampleSaturation(start_image, images_to_process); });
if (full && config_.refine_geometry.has_value() && !config_.detector_distance_unknown)
RefineStillsGeometry(start_image, end_image, images_to_process, observer);
// The detector tilt's fits read only the frames just fitted and the geometry they left: started now, they
// run beside the pre-scan and the geometry trial, and are used where neither moved the geometry.
if (full)
StartStillsTiltAhead();
// The geometry pre-pass integrates only the first part of the sweep (pass 2 reads it all), and on a
// rotation sweep the decisions before pass 2 read only that part too (PrepassEnd).
@@ -890,13 +894,16 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
// The stills geometry refinement above ran from the file's centre and distance, within a few pixels and
// millimetres of them; where the frames have just moved either, it is run again from where they are.
bool refitted = false;
if (full && !experiment_.IsRotationIndexing() && config_.beam_center_check && !config_.live
&& DecideStillsGeometry(start_image, images_to_process) && config_.refine_geometry.has_value())
&& DecideStillsGeometry(start_image, images_to_process) && config_.refine_geometry.has_value()) {
RefineStillsGeometry(start_image, end_image, images_to_process, observer);
refitted = true;
}
// The detector tilt, now that the centre is settled: a centre error on frames indexed at it is exactly a
// tilt's pattern shift, so a tilt fitted before the centre is decided takes up the centre's error.
if (full)
DecideStillsTilt();
DecideStillsTilt(refitted);
// The geometry pre-pass integrates at the radius the run started with; the width the pre-scan just
// measured is applied to the canonical pass instead (RunAllPasses, between the two). See
+25 -3
View File
@@ -1418,8 +1418,13 @@ class Rugnux {
int64_t on_lattice = 0; // ... of which on the lattice
};
std::vector<DataMessage> FindStillsSampleSpots(const std::vector<int> &ordinals, int start_image);
StillsTrial IndexStillsSample(const DiffractionExperiment &x, IndexerThreadPool &pool,
const std::vector<DataMessage> &frames);
// A geometry hypothesis and the frames it is indexed on.
struct StillsHypothesis {
DiffractionExperiment x;
const std::vector<DataMessage> *frames;
};
// Every hypothesis indexed on its frames, all frames of all hypotheses side by side.
std::vector<StillsTrial> IndexStillsSample(const std::vector<StillsHypothesis> &hypotheses, IndexerThreadPool &pool);
// Returns whether the run's geometry was moved off the file's.
bool DecideStillsGeometry(int start_image, int images_to_process);
// Stills: the detector tilt as a hypothesis, refined or held, kept where the frames it was not fitted to
@@ -1431,7 +1436,24 @@ class Rugnux {
std::vector<ModuleShift> modules;
};
std::optional<StillsTiltBundle> stills_tilt_bundle_;
void DecideStillsTilt();
// The tilt hypothesis tried on a bundle: the held-out residuals with the tilt held and refined, the tilted fit
// on every frame, and the geometry and cell the fits started from.
struct StillsTiltTrial {
DiffractionGeometry start;
UnitCell cell;
gemmi::CrystalSystem crystal_system;
double held = 0, refined = 0;
GeometryRefinerResult tilted;
};
// The same fits started as soon as the first RefineStillsGeometry made the bundle, beside the pre-scan and the
// geometry trial; DecideStillsTilt takes them where nothing in between moved the geometry or the bundle.
static StillsTiltTrial TryStillsTilt(const StillsTiltBundle &bundle, const DiffractionGeometry &start,
const UnitCell &cell, GeometryRefinerSettings st);
GeometryRefinerSettings StillsTiltSettings() const;
std::shared_future<StillsTiltTrial> stills_tilt_ahead_;
void StartStillsTiltAhead();
// bundle_refitted: RefineStillsGeometry ran again after StartStillsTiltAhead.
void DecideStillsTilt(bool bundle_refitted);
// Stills: the cell the user gave (-C / reference), kept apart from the one the geometry refinement
// writes into experiment_. See RefineStillsGeometry.
std::optional<UnitCell> stills_user_cell_;
+166 -64
View File
@@ -115,6 +115,13 @@ GeomRefineFrame ToRefineFrame(const DataMessage &msg, int ordinal) {
return f;
}
bool SameGeometry(const DiffractionGeometry &a, const DiffractionGeometry &b) {
return a.GetBeamX_pxl() == b.GetBeamX_pxl() && a.GetBeamY_pxl() == b.GetBeamY_pxl()
&& a.GetDetectorDistance_mm() == b.GetDetectorDistance_mm() && a.GetPixelSize_mm() == b.GetPixelSize_mm()
&& a.GetWavelength_A() == b.GetWavelength_A() && a.GetPoniRot1_rad() == b.GetPoniRot1_rad()
&& a.GetPoniRot2_rad() == b.GetPoniRot2_rad() && a.GetPoniRot3_rad() == b.GetPoniRot3_rad();
}
// Spots piled up at one radius - an ice or other powder ring - are not this crystal's lattice. They are
// found by their radius in millimetres about (beam_x, beam_y), not by a d-spacing: a d computed at a
// distance that may be wrong misses the rings, and then they are most of the spots a frame has.
@@ -554,23 +561,10 @@ void Rugnux::RefineStillsGeometry(int start_image, int end_image, int images_to_
// back to where it was given, and refitted with the tilt on every frame they gave a worse merge than held. Where the tilt is real it bends the pattern in a way nothing
// else in the fit can (the keystone); where it is not, the gain on frames the fit has not seen is nothing and the
// tilt found is too small to matter either way, so no threshold has to say how large a tilt must be.
void Rugnux::DecideStillsTilt() {
if (!stills_tilt_bundle_)
return;
Logger logger("Rugnux");
TimingMark mark(logger, "stills tilt decision");
const StillsTiltBundle bundle = std::move(*stills_tilt_bundle_);
stills_tilt_bundle_.reset();
const DiffractionGeometry start = experiment_.GetDiffractionGeometry();
const UnitCell cell = experiment_.GetUnitCell().value();
GeometryRefinerSettings st;
st.crystal_system = experiment_.GetCrystalSystem();
st.num_threads = config_.nthreads;
st.refine_distance = !config_.detector_distance_given;
st.cell_reg_coeff = 0.0;
// The fits read only what they are given, so they can run beside the steps before the decision (StartStillsTiltAhead).
Rugnux::StillsTiltTrial Rugnux::TryStillsTilt(const StillsTiltBundle &bundle, const DiffractionGeometry &start,
const UnitCell &cell, GeometryRefinerSettings st) {
st.modules = bundle.modules;
std::vector<GeomRefineFrame> half[2];
for (size_t i = 0; i < bundle.frames.size(); i++)
half[i % 2].push_back(bundle.frames[i]);
@@ -604,14 +598,58 @@ void Rugnux::DecideStillsTilt() {
sf.refine_tilt = true;
return RefineGlobalGeometry(start, cell, bundle.frames, sf);
});
const double held = (held_fits[0].get() + held_fits[1].get()) / 2.0;
const double refined = (tilt_fits[0].get() + tilt_fits[1].get()) / 2.0;
const GeometryRefinerResult r = tilted.get();
const bool keep = r.ok && refined < held;
StillsTiltTrial t{start, cell, st.crystal_system};
t.held = (held_fits[0].get() + held_fits[1].get()) / 2.0;
t.refined = (tilt_fits[0].get() + tilt_fits[1].get()) / 2.0;
t.tilted = tilted.get();
return t;
}
GeometryRefinerSettings Rugnux::StillsTiltSettings() const {
GeometryRefinerSettings st;
st.crystal_system = experiment_.GetCrystalSystem();
st.num_threads = config_.nthreads;
st.refine_distance = !config_.detector_distance_given;
st.cell_reg_coeff = 0.0;
return st;
}
void Rugnux::StartStillsTiltAhead() {
if (!stills_tilt_bundle_)
return;
stills_tilt_ahead_ = std::async(std::launch::async, TryStillsTilt, *stills_tilt_bundle_,
experiment_.GetDiffractionGeometry(), experiment_.GetUnitCell().value(),
StillsTiltSettings()).share();
}
void Rugnux::DecideStillsTilt(bool bundle_refitted) {
if (!stills_tilt_bundle_)
return;
Logger logger("Rugnux");
TimingMark mark(logger, "stills tilt decision");
std::optional<StillsTiltTrial> ahead;
if (stills_tilt_ahead_.valid()) {
ahead = stills_tilt_ahead_.get();
stills_tilt_ahead_ = {};
}
const StillsTiltBundle bundle = std::move(*stills_tilt_bundle_);
stills_tilt_bundle_.reset();
const DiffractionGeometry start = experiment_.GetDiffractionGeometry();
const UnitCell cell = experiment_.GetUnitCell().value();
const GeometryRefinerSettings st = StillsTiltSettings();
// The fits started ahead are these fits where they started from the same bundle, geometry and cell.
const bool use_ahead = ahead && !bundle_refitted && SameGeometry(ahead->start, start) && ahead->cell == cell
&& ahead->crystal_system == st.crystal_system;
logger.Debug("Detector tilt: the fits started beside the pre-scan are {}", use_ahead ? "used" : "not used");
const StillsTiltTrial t = use_ahead ? std::move(*ahead) : TryStillsTilt(bundle, start, cell, st);
const GeometryRefinerResult &r = t.tilted;
const bool keep = r.ok && t.refined < t.held;
logger.Info("Detector tilt: rot1 {:+.3f} deg, rot2 {:+.3f} deg refined ({:+.3f}, {:+.3f} held); held-out spot "
"residual (capped rms) {:.3f} px held, {:.3f} px refined, {} frames - the tilt is {}",
r.rot1_rad * 180.0 / PI, r.rot2_rad * 180.0 / PI, start.GetPoniRot1_rad() * 180.0 / PI,
start.GetPoniRot2_rad() * 180.0 / PI, held, refined, bundle.frames.size(),
start.GetPoniRot2_rad() * 180.0 / PI, t.held, t.refined, bundle.frames.size(),
keep ? "refined" : "held");
if (!keep)
return;
@@ -670,46 +708,57 @@ std::vector<DataMessage> Rugnux::FindStillsSampleSpots(const std::vector<int> &o
return frames;
}
Rugnux::StillsTrial Rugnux::IndexStillsSample(const DiffractionExperiment &x, IndexerThreadPool &pool,
const std::vector<DataMessage> &frames) {
IndexAndRefine indexer(x, &pool, /*retain_outcomes=*/false);
const DiffractionGeometry geom = x.GetDiffractionGeometry();
std::vector<Rugnux::StillsTrial> Rugnux::IndexStillsSample(const std::vector<StillsHypothesis> &hypotheses,
IndexerThreadPool &pool) {
auto settings = config_.spot_finding;
settings.enable = true;
settings.indexing = true;
std::vector<std::unique_ptr<IndexAndRefine>> indexers;
std::vector<std::pair<size_t, size_t>> items; // (hypothesis, frame)
for (size_t h = 0; h < hypotheses.size(); h++) {
indexers.push_back(std::make_unique<IndexAndRefine>(hypotheses[h].x, &pool, /*retain_outcomes=*/false));
for (size_t i = 0; i < hypotheses[h].frames->size(); i++)
items.emplace_back(h, i);
}
std::mutex m;
StillsTrial total;
std::vector<StillsTrial> total(hypotheses.size());
std::atomic<size_t> next = 0;
auto worker = [&]() {
StillsTrial t;
for (size_t i = next.fetch_add(1); i < frames.size() && !cancelled_; i = next.fetch_add(1)) {
std::vector<StillsTrial> t(hypotheses.size());
for (size_t n = next.fetch_add(1); n < items.size() && !cancelled_; n = next.fetch_add(1)) {
const auto [h, i] = items[n];
const DiffractionGeometry &geom = hypotheses[h].x.GetDiffractionGeometry();
// The spots where the frame put them, at this trial's geometry.
DataMessage msg = frames[i];
DataMessage msg = (*hypotheses[h].frames)[i];
for (auto &s : msg.spots) {
s.d_A = geom.PxlToRes(s.x, s.y);
s.indexed = false;
s.lattice = -1;
}
t.frames++;
if (!indexer.IndexFrameOnly(msg, settings))
t[h].frames++;
if (!indexers[h]->IndexFrameOnly(msg, settings))
continue;
t.indexed++;
t.spots += static_cast<int64_t>(msg.spots.size());
t.on_lattice += msg.spot_count_indexed.value_or(0);
t[h].indexed++;
t[h].spots += static_cast<int64_t>(msg.spots.size());
t[h].on_lattice += msg.spot_count_indexed.value_or(0);
}
indexer.RethrowIndexerError();
std::lock_guard lock(m);
total.frames += t.frames;
total.indexed += t.indexed;
total.spots += t.spots;
total.on_lattice += t.on_lattice;
for (size_t h = 0; h < t.size(); h++) {
total[h].frames += t[h].frames;
total[h].indexed += t[h].indexed;
total[h].spots += t[h].spots;
total[h].on_lattice += t[h].on_lattice;
}
};
std::vector<std::future<void>> futures;
for (int i = 0; i < config_.nthreads; ++i)
futures.push_back(std::async(std::launch::async, worker));
for (auto &f : futures)
f.get();
for (const auto &indexer : indexers)
indexer->RethrowIndexerError();
return total;
}
@@ -785,34 +834,61 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
scan_frames.assign(frames.begin(), frames.begin() + std::min<size_t>(frames.size(), scan_count));
};
find_sample();
IndexerThreadPool pool(experiment_.GetIndexingSettings(), IndexerConstruction::OnFirstUse, gpu_work_beside_);
// As many indexer threads as the image pass has (RunPipeline): the hypotheses are indexed side by side, and
// with the default four the workers waited on the pool.
IndexingSettings pool_settings = experiment_.GetIndexingSettings();
pool_settings.IndexingThreads(std::clamp(config_.nthreads, 1, 8));
IndexerThreadPool pool(pool_settings, IndexerConstruction::OnFirstUse, gpu_work_beside_);
// A hypothesis is indexed once: the searches below meet the same one more than once - the decided centre
// is the file's whenever the file's is kept - and it gives the same count every time.
std::map<std::tuple<float, float, float, const std::vector<DataMessage> *>, int64_t> tried;
const auto trial = [&](float x, float y, float distance, const std::vector<DataMessage> &on) {
const auto key = std::make_tuple(x, y, distance, &on);
if (const auto it = tried.find(key); it != tried.end())
return it->second;
DiffractionExperiment e = experiment_;
place(e, x, y, distance);
if (stills_user_cell_)
e.SetUnitCell(*stills_user_cell_); // the grid asks the user's cell, not the one refined at the file's distance
// The distance scan's frames are indexed with a wider acceptance: the scan only has to find the peak, and
// it looks at centres a few pixels off and at distances up to half a step off, either of which leaves the
// strict tolerance with nothing on a large cell at a long distance (a 92 A cell at 199 mm indexed on no
// centre 3 px off). Everything after the scan is counted at the run's own tolerance.
if (&on == &scan_frames) {
IndexingSettings wide = e.GetIndexingSettings();
wide.Tolerance(SCAN_TOLERANCE);
e.ImportIndexingSettings(wide);
// is the file's whenever the file's is kept - and it gives the same count every time. The hypotheses a
// search asks are indexed together (try_all) - a hypothesis' count does not depend on what is indexed
// beside it - and then read one by one, in the order the search compares them.
using TrialKey = std::tuple<float, float, float, const std::vector<DataMessage> *>;
std::map<TrialKey, int64_t> tried;
// Hypotheses indexed in one go: enough to keep every worker and the indexer pool busy on 8 frames each,
// few enough that their experiments and indexers stay small.
constexpr size_t HYPOTHESES_AT_ONCE = 64;
const auto try_all = [&](const std::vector<TrialKey> &keys) {
std::vector<TrialKey> todo;
for (const auto &key : keys)
if (!tried.contains(key) && std::find(todo.begin(), todo.end(), key) == todo.end())
todo.push_back(key);
for (size_t c0 = 0; c0 < todo.size(); c0 += HYPOTHESES_AT_ONCE) {
const size_t c1 = std::min(todo.size(), c0 + HYPOTHESES_AT_ONCE);
std::vector<StillsHypothesis> hypotheses;
for (size_t k = c0; k < c1; k++) {
const auto &[x, y, distance, on] = todo[k];
DiffractionExperiment e = experiment_;
place(e, x, y, distance);
if (stills_user_cell_)
e.SetUnitCell(*stills_user_cell_); // the grid asks the user's cell, not the one refined at the file's distance
// The distance scan's frames are indexed with a wider acceptance: the scan only has to find the peak, and
// it looks at centres a few pixels off and at distances up to half a step off, either of which leaves the
// strict tolerance with nothing on a large cell at a long distance (a 92 A cell at 199 mm indexed on no
// centre 3 px off). Everything after the scan is counted at the run's own tolerance.
if (on == &scan_frames) {
IndexingSettings wide = e.GetIndexingSettings();
wide.Tolerance(SCAN_TOLERANCE);
e.ImportIndexingSettings(wide);
}
hypotheses.push_back(StillsHypothesis{std::move(e), on});
}
const std::vector<StillsTrial> results = IndexStillsSample(hypotheses, pool);
for (size_t k = c0; k < c1; k++) {
const auto &[x, y, distance, on] = todo[k];
const StillsTrial &t = results[k - c0];
logger.Debug("Stills geometry trial: centre ({:.2f},{:.2f}) distance {:.2f} mm - {}/{} frames indexed, "
"{} of their {} spots on the lattice", x, y, distance, t.indexed, t.frames,
t.on_lattice, t.spots);
tried[todo[k]] = t.on_lattice;
}
}
const StillsTrial t = IndexStillsSample(e, pool, on);
logger.Debug("Stills geometry trial: centre ({:.2f},{:.2f}) distance {:.2f} mm - {}/{} frames indexed, "
"{} of their {} spots on the lattice", x, y, distance, t.indexed, t.frames,
t.on_lattice, t.spots);
tried[key] = t.on_lattice;
return t.on_lattice;
};
const auto trial = [&](float x, float y, float distance, const std::vector<DataMessage> &on) {
const TrialKey key{x, y, distance, &on};
try_all({key});
return tried.at(key);
};
// The centres: the file's, the measured one and the point-symmetry capture's - two estimators of the
@@ -826,6 +902,10 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
float x = file_x, y = file_y;
int64_t score_here = 0;
const auto decide_centre = [&](float distance) {
std::vector<TrialKey> keys{{file_x, file_y, distance, &frames}};
for (const auto &[cx, cy] : candidates)
keys.emplace_back(cx, cy, distance, &frames);
try_all(keys);
const int64_t file_score = trial(file_x, file_y, distance, frames);
int64_t best = file_score;
float best_x = file_x, best_y = file_y;
@@ -867,6 +947,10 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
float best_distance = file_distance, best_x = x, best_y = y;
int64_t best = score_here;
const auto fine = [&](float about, float cx, float cy) {
std::vector<TrialKey> keys;
for (const float f : {0.98f, 0.99f, 1.0f, 1.01f, 1.02f})
keys.emplace_back(cx, cy, about * f, &frames);
try_all(keys);
for (const float f : {0.98f, 0.99f, 1.0f, 1.01f, 1.02f}) {
const int64_t score = trial(cx, cy, about * f, frames);
if (score > best) {
@@ -893,6 +977,11 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
centres.emplace_back(file_x, file_y);
// The local maxima of every centre's scan, strongest first; a distance is looked at once.
std::vector<std::pair<int64_t, size_t>> maxima;
std::vector<TrialKey> keys;
for (const auto &[cx, cy] : centres)
for (const float d : grid)
keys.emplace_back(cx, cy, d, &scan_frames);
try_all(keys);
for (const auto &[cx, cy] : centres) {
std::vector<int64_t> scan(grid.size());
for (size_t i = 0; i < grid.size(); i++)
@@ -909,6 +998,12 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
float grid_best = file_distance, grid_x = x, grid_y = y;
int64_t grid_score = -1;
keys.clear();
for (const size_t p : peaks)
for (size_t i = (p > 0 ? p - 1 : 0); i <= std::min(grid.size() - 1, p + 1); i++)
for (const auto &[cx, cy] : centres)
keys.emplace_back(cx, cy, grid[i], &strongest);
try_all(keys);
for (const size_t p : peaks)
for (size_t i = (p > 0 ? p - 1 : 0); i <= std::min(grid.size() - 1, p + 1); i++)
for (const auto &[cx, cy] : centres) {
@@ -977,6 +1072,12 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
int64_t g_best = trial(x, y, distance, strongest);
for (const auto &[step, reach] : {std::pair{3.0f, 4}, std::pair{1.0f, 2}}) {
const float cx0 = gx, cy0 = gy;
std::vector<TrialKey> keys;
for (int i = -reach; i <= reach; i++)
for (int j = -reach; j <= reach; j++)
if (i != 0 || j != 0)
keys.emplace_back(cx0 + step * i, cy0 + step * j, distance, &strongest);
try_all(keys);
for (int i = -reach; i <= reach; i++)
for (int j = -reach; j <= reach; j++) {
if (i == 0 && j == 0)
@@ -989,6 +1090,7 @@ bool Rugnux::DecideStillsGeometry(int start_image, int images_to_process) {
}
}
}
try_all({{x, y, distance, &frames}, {gx, gy, distance, &frames}});
const int64_t here = trial(x, y, distance, frames), there = trial(gx, gy, distance, frames);
logger.Info("Stills beam centre grid at {:.2f} mm: ({:.2f},{:.2f}) puts {} sampled spots on the lattice, "
"({:.2f},{:.2f}) {}", distance, gx, gy, there, x, y, here);