IndexerThreadPool collapsed two different outcomes into the same empty reply. A worker that threw set result = nullptr, and so did a dispatch that never found a free worker; both then became a default-constructed IndexerResult, indistinguishable downstream from an indexer that ran and found no lattice. The first says nothing about the frame at all - the indexer never looked, and it will fail again on the next one - while the second is a real negative result about the crystal. The visible cost was the advice a failed run gave. With the card full, rugnux printed "Indexer thread 0 failed: CUDA (GPU) error" and then ended with "Two-pass rotation indexing found no lattice. Check the beam centre (--beam-x / --beam-y), raise --max-spots ..." - sending the operator to look at geometry that was never wrong, for a machine that was simply out of memory. None of those remedies can help when the frames were not examined. IndexerResult gains an optional error, set by the worker and by the pool's own catch; the remaining nullptr path keeps its meaning of "not attempted" and deliberately carries no error. RotationIndexer records it and exposes GetIndexerError(), and rugnux's first pass branches on it at the throw site, naming the resource failure instead. The error travels as DATA through image_analysis/ rather than as an exception, because RotationIndexer::RunIndexing() is on the online path - IndexAndRefine calls it on a schedule from the broker and the receiver, where dropping a frame is the right failure and killing a live acquisition is not. Only rugnux, which owns the "this run is over" decision, turns it into one. The failure result is byte-identical to the default-constructed one it replaces, so any_executed and the online frame-drop behaviour are unchanged. The new IndexerError category exists because the category is only the display prefix on what() - Category() is read nowhere - and the old line read "Processing failed: Input parameter invalid" for a GPU fault, which is the same defect one layer up. SpotFinderError is the precedent. msg.indexing_result is deliberately left alone: of its three states, "not attempted" is the honest one for a frame the indexer never examined, and asserting false would be the same collapse again. Verified on a squeezed card (246 MB free, -N 4): exit 1, zero dropped frames, the new message, and no mention of --beam-x. On a free card, against the previous binary at -N 6 on the same input, the logs differ only in paths and timing - same space group, cell and merge statistics. Targeted Catch2 cases pass, including three that drive the pool through the online path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
101 lines
3.6 KiB
C++
101 lines
3.6 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
#include <iostream>
|
|
|
|
#include "../image_analysis/rotation_indexer/RotationIndexer.h"
|
|
#include "../image_analysis/bragg_prediction/BraggPrediction.h"
|
|
|
|
TEST_CASE("RotationIndexer") {
|
|
DiffractionExperiment exp_i;
|
|
exp_i.IncidentEnergy_keV(WVL_1A_IN_KEV)
|
|
.BeamX_pxl(1000)
|
|
.BeamY_pxl(1000)
|
|
.PoniRot1_rad(0.01)
|
|
.PoniRot2_rad(0.02)
|
|
.DetectorDistance_mm(200)
|
|
.ImagesPerTrigger(50);
|
|
|
|
IndexingSettings settings;
|
|
#ifdef JFJOCH_USE_CUDA
|
|
settings.Algorithm(IndexingAlgorithmEnum::FFT);
|
|
#elif JFJOCH_USE_FFTW
|
|
settings.Algorithm(IndexingAlgorithmEnum::FFTW);
|
|
#else
|
|
return;
|
|
#endif
|
|
|
|
settings.RotationIndexing(true).RotationIndexingAngularStride_deg(1.0).RotationIndexingMinAngularRange_deg(30.0);
|
|
exp_i.ImportIndexingSettings(settings);
|
|
|
|
// Base lattice (non-pathological)
|
|
CrystalLattice latt_base(40, 50, 80, 90, 90, 90);
|
|
latt_base = latt_base.Multiply(RotMatrix(2.0, Coord(sqrt(3)/3,sqrt(3)/3,sqrt(3)/3)));
|
|
|
|
// Rotation axis: around X with 1 deg per image
|
|
GoniometerAxis axis("omega", 0.0f, 1.0f, Coord(1,0,0), std::nullopt);
|
|
exp_i.Goniometer(axis);
|
|
|
|
BraggPredictionSettings prediction_settings{
|
|
.high_res_A = 1.3,
|
|
.ewald_dist_cutoff = 0.002
|
|
};
|
|
|
|
IndexerThreadPool indexer_thread_pool(exp_i.GetIndexingSettings());
|
|
RotationIndexer indexer(exp_i, indexer_thread_pool);
|
|
|
|
BraggPrediction prediction;
|
|
|
|
int cnt = 0;
|
|
|
|
// Predict reflections for images at 0-30 deg.
|
|
for (int img = 0; img < 50; ++img) {
|
|
std::vector<SpotToSave> spots;
|
|
// For a rotated image, per-image lattice is obtained as Multiply(rot.transpose())
|
|
const float angle_deg = axis.GetAngle_deg(img) + axis.GetWedge_deg() / 2.0f;
|
|
const RotMatrix rot = axis.GetTransformationAngle(angle_deg);
|
|
const CrystalLattice latt_img = latt_base.Multiply(rot.transpose());
|
|
|
|
const auto n = prediction.Calc(exp_i, latt_img, prediction_settings);
|
|
for (int i = 0; i < n; ++i) {
|
|
const auto& r = prediction.GetReflections().at(i);
|
|
SpotToSave s{};
|
|
s.x = r.predicted_x;
|
|
s.y = r.predicted_y;
|
|
s.image = img; // provide image index for rotation-aware refinement
|
|
s.intensity = 1.0f; // minimal positive value
|
|
s.phi = angle_deg;
|
|
s.ice_ring = false;
|
|
s.indexed = true;
|
|
spots.push_back(s);
|
|
}
|
|
|
|
indexer.ProcessImage(img, spots);
|
|
if (img == 30)
|
|
indexer.RunIndexing();
|
|
|
|
auto result = indexer.GetLattice();
|
|
if (result.has_value())
|
|
cnt++;
|
|
}
|
|
|
|
CHECK(cnt == 20);
|
|
// An indexer that ran records no error; only one that threw does. A caller reporting "no lattice"
|
|
// to the user tells the two apart on this.
|
|
CHECK_FALSE(indexer.GetIndexerError().has_value());
|
|
|
|
auto ret = indexer.GetLattice();
|
|
REQUIRE(ret.has_value());
|
|
auto uc = ret->lattice.GetUnitCell();
|
|
auto uc_ref = latt_base.GetUnitCell();
|
|
REQUIRE(std::fabs(uc.a - uc_ref.a) < 0.1);
|
|
REQUIRE(std::fabs(uc.b - uc_ref.b) < 0.1);
|
|
REQUIRE(std::fabs(uc.c - uc_ref.c) < 0.1);
|
|
REQUIRE(std::fabs(uc.alpha - uc_ref.alpha) < 0.1);
|
|
REQUIRE(std::fabs(uc.beta - uc_ref.beta) < 0.1);
|
|
REQUIRE(std::fabs(uc.gamma - uc_ref.gamma) < 0.1);
|
|
CHECK(ret->search_result.centering == 'P');
|
|
CHECK(ret->search_result.system == gemmi::CrystalSystem::Orthorhombic);
|
|
}
|