Files
Jungfraujoch/common/AnalysisSettings.cpp
T
leonarski_fandClaude Opus 5 a7d3ada3ab analysis: what runs over the images is one stated mode, shared by broker, rugnux and viewer
Until now nothing in the tree said what analysis a run performed. The answer was composed
at each site out of four independent scalars - the detector type, two separate "spot finding
off" switches, an indexing flag and a rotation flag - so what was configured and what
actually ran were different things, and no single place could be read to find out which.

AnalysisMode {None, MXRotation, MXStills, Azint, Grid, PowderCalibration} is that statement,
in common/ because all three programs configure the DiffractionExperiment that carries it.
AnalysisSettings sits on the experiment beside IndexingSettings, outside the DatasetSettings
member, which is the one thing a /start replaces wholesale - so the mode is persistent by
construction rather than by a rule someone has to remember.

The mode does not label a run, it decides it. AnalysisModeStages() is a table - modes as
rows, pipeline stages as columns - and every gate reads that table instead of testing the
mode: spot finding in DiffractionExperiment::IsSpotFindingEnabled, indexing (and with it
prediction and integration, which never run without a lattice) in one gate inside
IndexAndRefine that serves all three front ends, azimuthal integration where the CPU engine
is built. Two rows carry a judgement worth reviewing: powder calibration keeps spot finding,
because --calibration spots fits the pooled spots; grid does not index, because a raster is
thousands of frames and the per-image scoring it ranks on deliberately avoids an indexer that
fires on ice.

There is deliberately no Auto value. GetIndexingAlgorithm() resolves Auto at read time, which
is exactly why an indexing setting cannot be read back off the configuration; removing that
kind of implicitness is the point here, so the mode getter stays a plain accessor. MXStills
is the default because None would silently switch analysis off on every deployment whose
configuration predates the field.

Rotation MX is absent from the OpenAPI schema rather than present and refused: jfjoch_broker
has no rotation analysis path, so the REST and configuration-file routes cannot express it at
all. The shared enum can still carry the value from elsewhere, so CheckAnalysisSettingsOnline
refuses it on both routes with a message naming rugnux. A sweep collected under an MX mode is
not refused - collecting rotation data online is normal and live spot counts are useful - but
it is said out loud in the log, since the mistake worth preventing is the silence about what
was done to it, not the acquisition.

Powder calibration forces azimuthal integration onto the CPU and supplies 32 sectors where
fewer than four were asked for. The FPGA integration core holds 2048 bins in total, so 32
sectors would leave 64 q bins - far too coarse to fit a ring. Frame rate is what this costs
and a calibration exposure does not need it.

The two existing "no analysis" switches, per-dataset dataset_settings.spot_finding and
persistent SpotFindingSettings::enable, are interfaces in too many places to remove now. They
are marked deprecated in the schema and in both headers, and the mode takes precedence over
them: a mode that analyses no spots wins outright, while under a mode that does find spots
they remain the finer control. The precedence is written where it is enforced.

rugnux's ProcessMode is gone, replaced by the shared enum; RugnuxMode stays as the CLI
spelling layer and no existing spelling changes. --mode gains mx_rotation and mx_stills, which
are spellings of -R and --force-still rather than new switches; plain mx still chooses between
them from the goniometer. scale keeps no shared counterpart, since it runs no analysis over
images at all.

The mode reaches the CBOR start message and /entry/MX/analysis_mode in the HDF5 master, so a
written file records which analysis produced it. It is read back as provenance only - what a
stored file was produced by is not what the next run should do.

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

106 lines
4.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "AnalysisSettings.h"
// The mode-to-analysis table. One place, one row per mode, and every gate in the pipeline reads it -
// so what a mode does can be read off here instead of being reconstructed from `if (mode == ...)`
// scattered through the engines.
//
// spots index bragg merge score azint
// None - - - - - -
// MXRotation x x x x x x
// MXStills x x x x x x
// Azint - - - - - x
// Grid x - - - x x
// PowderCalibration x - - - - x
//
// Two rows want a word.
//
// PowderCalibration keeps spot finding. The geometry is fitted either to the ring arcs of the summed
// (q x azimuth) profile or to the POOLED SPOTS (rugnux --calibration rings|spots), and the second of
// those is a spot finder run - so the mode must not decide this; whoever picks the source does.
//
// Grid does not index. A raster is thousands of frames and indexing is the expensive stage, and the
// per-image scoring it ranks grid points on deliberately does not use it (indexing fires on ice, which
// is exactly what the scoring has to see past). The cost of that choice is indexed_lattice_count, which
// is the cheapest multi-lattice / cracked-crystal signal there is and which a raster therefore does not
// get. It is one field in this table either way.
//
// The switch has no default: adding a mode must be a compile error here rather than a silent
// fall-through to whatever the last row happened to be.
AnalysisStages AnalysisModeStages(AnalysisMode mode) {
switch (mode) {
case AnalysisMode::None:
return {false, false, false, false, false, false};
case AnalysisMode::MXRotation:
case AnalysisMode::MXStills:
return {true, true, true, true, true, true};
case AnalysisMode::Azint:
return {false, false, false, false, false, true};
case AnalysisMode::Grid:
return {true, false, false, false, true, true};
case AnalysisMode::PowderCalibration:
return {true, false, false, false, false, true};
}
return {false, false, false, false, false, false};
}
bool AnalysisModeIsMX(AnalysisMode mode) {
switch (mode) {
case AnalysisMode::MXRotation:
case AnalysisMode::MXStills:
return true;
case AnalysisMode::None:
case AnalysisMode::Azint:
case AnalysisMode::Grid:
case AnalysisMode::PowderCalibration:
return false;
}
return false;
}
std::string AnalysisModeName(AnalysisMode mode) {
switch (mode) {
case AnalysisMode::None: return "none";
case AnalysisMode::MXRotation: return "mx_rotation";
case AnalysisMode::MXStills: return "mx_stills";
case AnalysisMode::Azint: return "azint";
case AnalysisMode::Grid: return "grid";
case AnalysisMode::PowderCalibration: return "powder_calibration";
}
return "none";
}
std::optional<AnalysisMode> AnalysisModeFromName(std::string_view name) {
if (name == "none") return AnalysisMode::None;
if (name == "mx_rotation") return AnalysisMode::MXRotation;
if (name == "mx_stills") return AnalysisMode::MXStills;
if (name == "azint") return AnalysisMode::Azint;
if (name == "grid") return AnalysisMode::Grid;
if (name == "powder_calibration") return AnalysisMode::PowderCalibration;
return {};
}
AnalysisSettings &AnalysisSettings::Mode(AnalysisMode input) {
mode = input;
return *this;
}
AnalysisSettings &AnalysisSettings::Calibrant(const std::string &input) {
calibrant = input;
return *this;
}
AnalysisMode AnalysisSettings::GetMode() const {
return mode;
}
const std::string &AnalysisSettings::GetCalibrant() const {
return calibrant;
}
bool AnalysisSettings::IsMX() const {
return AnalysisModeIsMX(mode);
}