Files
Jungfraujoch/common/ScanResultGenerator.cpp
T
leonarski_fandClaude Opus 5 0b567061ce
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m42s
Build Packages / build:windows:nocuda (push) Successful in 18m11s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 18m22s
Build Packages / build:windows:cuda (push) Successful in 20m31s
Build Packages / build:viewer-tgz:cpu (push) Successful in 20m57s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m25s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 22m33s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m0s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m37s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m49s
Build Packages / build:rugnux:windows (push) Successful in 11m34s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 23m11s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m23s
Build Packages / build:rpm (rocky9) (push) Successful in 25m25s
Build Packages / build:rpm (rocky8) (push) Successful in 29m21s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m8s
Build Packages / Generate python client (push) Successful in 45s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m12s
Build Packages / XDS test (durin plugin) (push) Successful in 10m58s
Build Packages / DIALS test (push) Successful in 26m15s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 28m3s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m36s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m20s
Build Packages / Unit tests (push) Successful in 1h26m10s
grid scan: the run finds the crystals and the viewer can ask it to
A grid scan is analysed once, from the completed map, and the answer belongs to the run
rather than to whoever happened to call it. rugnux computed it after RunPipeline had
already written the end message, so the crystals reached the raster report and nothing
else - the _process.h5 carried the per-cell scores and no crystal list, and a viewer
re-opening that file had nothing to draw.

Rugnux now accumulates the raster with ScanResultGenerator, the same accumulator the
online receiver fills, runs AnalyzeGridScan at the end message for the same stated reason
the receiver does, sets EndMessage::grid_crystals so the file gets /entry/MX/crystals, and
returns the result on ProcessResult. The CLI consumes that instead of analysing the map a
second time; the raster report and JSON are unchanged, verified against the previous
binary on a real raster (identical crystal, identical report; the ice score differs in the
seventh decimal, which the same binary does run to run).

The accumulator keyed a cell on the message's number, which for rugnux is the ordinal of
the images -s/-e/--stride selected, not the image's place in the raster. It now reads
original_number where there is one, as the writer already does; both receivers set it
equal to number, so nothing online changes.

jfjoch_viewer offers Grid as a fourth mode beside MX, AzInt and Calib, configures the job
with spot finding on and indexing from the experiment's grid-scan settings, and refuses
the mode on a dataset that has no grid scan rather than scoring every image and reporting
nothing. The shared azimuthal section is shown for it, which it needs - the ice score's
radial channel reads that profile.

Verified end to end: Grid, then Analyze dataset, on a stored raster leaves the composite
map on screen with a frame at each crystal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
2026-09-08 12:20:03 +02:00

172 lines
8.1 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <algorithm>
#include <cmath>
#include "ScanResultGenerator.h"
namespace {
template<class T>
T value_or_zero(const std::optional<T>& v) {
return v.value_or(static_cast<T>(0));
}
}
ScanResultGenerator::ScanResultGenerator(const DiffractionExperiment &experiment) {
grid_scan = experiment.GetGridScan();
goniometer_axis = experiment.GetGoniometer();
if (grid_scan)
v.resize(grid_scan->GetNElem());
else
v.resize(experiment.GetImageNum());
file_prefix = experiment.GetFilePrefix();
}
void ScanResultGenerator::Add(const DataMessage &message) {
std::unique_lock ul(m);
// The image's OWN number in the file, which is what a grid position and a goniometer angle are
// both a function of. A receiver numbers images as it collects them, so the two are the same
// there; rugnux numbers the images it SELECTED (-s/-e/--stride) and only original_number says
// where in the run a frame actually sat. Same idiom as the writer (writer/HDF5DataFile.cpp).
const int64_t number = message.original_number.value_or(message.number);
int64_t image_number = number;
if (grid_scan)
image_number = grid_scan->Rearrange(image_number);
if (image_number >= 0 && static_cast<size_t>(image_number) < v.size()) {
if (grid_scan) {
v[image_number].x = grid_scan->GetElementPosX_step(number);
v[image_number].y = grid_scan->GetElementPosY_step(number);
} else if (goniometer_axis) {
v[image_number].angle_deg = goniometer_axis->GetAngle_deg(number);
}
v[image_number].number = number;
v[image_number].pixel_sum = message.pixel_sum;
v[image_number].collection_efficiency = message.image_collection_efficiency.value_or(1.0);
v[image_number].bkg = message.bkg_estimate;
v[image_number].spindle_blind = message.spindle_blind_fraction;
v[image_number].spot_count = message.spot_count;
v[image_number].indexing_solution = message.indexing_result;
v[image_number].indexed_lattice_count = message.indexing_lattice_count;
v[image_number].profile_radius = message.profile_radius;
v[image_number].mosaicity = message.mosaicity_deg;
v[image_number].b_factor = message.b_factor;
v[image_number].uc = message.indexing_unit_cell;
v[image_number].xfel_pulse_id = message.xfel_pulse_id;
v[image_number].err_pixels = message.error_pixel_count;
v[image_number].min_viable_pixel = message.min_viable_pixel_value;
v[image_number].max_viable_pixel = message.max_viable_pixel_value;
v[image_number].sat_pixels = message.saturated_pixel_count;
v[image_number].spot_count_ice = message.spot_count_ice_rings;
v[image_number].spot_count_ice_control = message.spot_count_ice_control;
v[image_number].spot_count_low_res = message.spot_count_low_res;
v[image_number].spot_count_indexed = message.spot_count_indexed;
v[image_number].res = message.resolution_estimate;
v[image_number].integrated_reflections = message.integrated_reflections;
v[image_number].image_scale_factor = message.image_scale_factor;
v[image_number].image_scale_cc = message.image_scale_cc;
v[image_number].ice_ring_ratio = message.ice_ring_ratio;
v[image_number].protein_score = message.protein_score;
v[image_number].ice_score = message.ice_score;
if (message.lattice_type)
v[image_number].niggli_class = message.lattice_type->niggli_class;
}
}
ScanResult ScanResultGenerator::GetResult() const {
std::unique_lock ul(m);
ScanResult ret;
ret.file_prefix = file_prefix;
for (const auto &e: v) {
if (e.number >= 0)
ret.images.push_back(e);
}
return ret;
}
void ScanResultGenerator::FillEndMessage(EndMessage &message) const {
std::unique_lock ul(m);
size_t n = 0;
for (const auto &e: v) {
if (e.number >= 0)
n = std::max(n, static_cast<size_t>(e.number) + 1);
}
if (n == 0)
return;
// The vectors the loop below fills with value_or(NAN) are sized with NAN, not with the
// value-initialised zero: an image that never reached the loop at all - dropped, never
// arrived, numbered outside the run - has no measurement, and zero is a measurement. A
// blind fraction of 0.0 reads as "this frame lost nothing", a mosaicity of 0.0 as a
// perfect crystal, and a resolution estimate of 0.0 as nothing at all. The counts keep
// their zeros, because a count of zero is a thing that can be true.
message.data_collection_efficiency.resize(n);
message.spot_count.resize(n);
message.spot_count_ice_ring.resize(n);
message.spot_count_ice_control.resize(n, NAN);
message.spot_count_low_res.resize(n);
message.spot_count_indexed.resize(n);
message.image_indexed.resize(n);
message.v_bkg_estimate.resize(n, NAN);
message.v_spindle_blind_fraction.resize(n, NAN);
message.profile_radius.resize(n, NAN);
message.mosaicity.resize(n, NAN);
message.bFactor.resize(n, NAN);
message.resolution_estimate.resize(n, NAN);
message.min_viable_pixel_value.resize(n);
message.max_viable_pixel_value.resize(n);
message.saturated_pixel_count.resize(n);
message.error_pixel_count.resize(n);
message.image_scale_factor.resize(n, NAN);
message.image_scale_cc.resize(n, NAN);
message.ice_ring_ratio.resize(n, NAN);
message.v_protein_score.resize(n, NAN);
message.v_ice_score.resize(n, NAN);
message.integrated_reflections.resize(n);
message.niggli_class.resize(n);
message.pixel_sum.resize(n);
message.indexed_lattice_count.resize(n);
for (const auto &e: v) {
if (e.number < 0)
continue;
const auto number = static_cast<size_t>(e.number);
if (number >= n)
continue;
message.data_collection_efficiency[number] = e.collection_efficiency;
message.spot_count[number] = static_cast<int32_t>(value_or_zero(e.spot_count));
message.spot_count_ice_ring[number] = static_cast<int32_t>(value_or_zero(e.spot_count_ice));
message.spot_count_ice_control[number] = e.spot_count_ice_control.value_or(NAN);
message.spot_count_low_res[number] = static_cast<int32_t>(value_or_zero(e.spot_count_low_res));
message.spot_count_indexed[number] = static_cast<int32_t>(value_or_zero(e.spot_count_indexed));
message.image_indexed[number] = static_cast<uint8_t>(e.indexing_solution.value_or(0));
message.v_bkg_estimate[number] = e.bkg.value_or(NAN);
message.v_spindle_blind_fraction[number] = e.spindle_blind.value_or(NAN);
message.profile_radius[number] = e.profile_radius.value_or(NAN);
message.mosaicity[number] = e.mosaicity.value_or(NAN);
message.bFactor[number] = e.b_factor.value_or(NAN);
message.resolution_estimate[number] = e.res.value_or(NAN);
message.min_viable_pixel_value[number] = value_or_zero(e.min_viable_pixel);
message.max_viable_pixel_value[number] = value_or_zero(e.max_viable_pixel);
message.saturated_pixel_count[number] = static_cast<int32_t>(value_or_zero(e.sat_pixels));
message.error_pixel_count[number] = static_cast<int32_t>(value_or_zero(e.err_pixels));
message.image_scale_factor[number] = e.image_scale_factor.value_or(NAN);
message.image_scale_cc[number] = e.image_scale_cc.value_or(NAN);
message.ice_ring_ratio[number] = e.ice_ring_ratio.value_or(NAN);
message.v_protein_score[number] = e.protein_score.value_or(NAN);
message.v_ice_score[number] = e.ice_score.value_or(NAN);
message.integrated_reflections[number] = static_cast<int32_t>(value_or_zero(e.integrated_reflections));
message.niggli_class[number] = static_cast<uint8_t>(value_or_zero(e.niggli_class));
message.pixel_sum[number] = value_or_zero(e.pixel_sum);
message.indexed_lattice_count[number] = static_cast<int32_t>(value_or_zero(e.indexed_lattice_count));
}
}