Files
Jungfraujoch/tests/RotationIndexerTest.cpp
T
leonarski_fandClaude Opus 5 6319d5c600 indexing: tell an indexer that failed apart from one that found nothing
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
2026-08-29 15:30:28 +02:00

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);
}