Files
Jungfraujoch/common/GridScanAnalysisSettings.h
T
leonarski_fandClaude Opus 5 98132d0f83 analysis: every analysis method carries its own settings, and a raster's indexing is one of them
AnalysisSettings had begun collecting per-method parameters - the calibrant was already in
it, and the grid thresholds were about to be. That makes the structure every method reads
grow whenever any one method gains a knob, and it puts a field in front of readers for whom
it means nothing. So: AnalysisSettings keeps what all methods share, which for now is the
mode, and each method gets a class of its own bound the same way.

GridScanAnalysisSettings holds the protein-score threshold, the minimum cells per crystal,
the decisive single-cell score, the maximum crystals reported and the indexing switch.
CalibrationSettings holds the calibrant and the ring source. Both sit on
DiffractionExperiment outside the per-run dataset member, both have an Import/Get pair, and
both have their own endpoint - /config/grid_scan_analysis and /config/calibration - which is
how every other settings group in this API is already reached.

Grid indexing is no longer fixed in the stages table. It was turned off there on cost
grounds, and that reasoning does not hold: a raster runs at up to 100 Hz, which the FFT
indexer keeps up with, and a fixed-target serial experiment with a known cell wants ffbidx on
every cell, where a raster that indexes is most of the measurement. So it is a setting, and
DEFAULTS ON. It is additive rather than a change of answer - blobs are still found on the
protein score, so indexing alters nothing about which cells are called crystals and only adds
what was found in them, including the per-cell lattice count, which is the cheapest
multi-lattice or cracked-crystal signal there is.

That makes indexing the one stage a mode does not decide. AnalysisModeStages still carries a
value for it, but only as the setting's default, and DiffractionExperiment::GetAnalysisStages
- which is what every gate reads - substitutes the configured one. The table row is marked so
nobody reads it as the mode's answer.

The calibration knobs stay coupled to the mode but the rule now lives with them:
CalibrationSettings::ApplyToAzimuthalIntegration moves azimuthal integration onto the CPU and
supplies sectors where fewer than four were asked for, carrying the reason with it - the FPGA
integration core holds 2048 bins in total, so 32 sectors leave 64 q bins, which cannot locate
a ring. Stated there because it will otherwise read as an FPGA defect to be fixed back onto
that path, and it is not one: the core is sized for a detector at full rate, and a calibration
exposure is a few images at a few Hz. Both imports apply it, so the order the mode and the
calibration settings are set in does not matter.

CalibrationMethod moves from image_analysis/geom_refinement/PowderCalibration.h into
common/CalibrationSettings.h, which that header now includes. One enum, so the setting and the
code consuming it are not two vocabularies; every existing user sees it unchanged.

The grid thresholds have one home and it is this class. The raster work owns AnalyzeGridScan's
parameter surface and carries PROTEIN_SCORE_THRESHOLD_DEFAULT / MIN_BLOB_CELLS_DEFAULT beside
that header today; the header here states the signature that replaces them, so the two do not
become competing defaults. The beam size deliberately stays a separate argument to
AnalyzeGridScan: it is measured, not configured.

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

64 lines
3.7 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstdint>
// Settings for AnalysisMode::Grid and nothing else. Per-method by the same rule as
// CalibrationSettings: AnalysisSettings carries what every method shares, each method carries its
// own knobs.
//
// THIS IS THE SINGLE HOME FOR THESE VALUES. AnalyzeGridScan
// (image_analysis/grid_scan_analysis/AnalyzeGridScan.h) is to take this object as one argument -
//
// GridScanResult AnalyzeGridScan(const ScanResult &scan, const GridScanSettings &grid,
// float beam_size_x_um, float beam_size_y_um,
// const GridScanAnalysisSettings &settings = {});
//
// - rather than loose parameters carrying defaults of their own, so that a knob has one default and
// one place where it is documented. PROTEIN_SCORE_THRESHOLD_DEFAULT and MIN_BLOB_CELLS_DEFAULT are
// replaced by the two members below and must not be reintroduced beside that header.
//
// The beam size stays a separate argument on purpose: it is measured (from the file, or --beam-size),
// not configured, and rugnux reports where it came from beside the crystals it sized.
class GridScanAnalysisSettings {
// A cell counts as protein above this. The per-image protein score saturates, so this only has
// to separate "something diffracted here" from "nothing did".
float protein_score_threshold = 0.5f;
// How many cells above that threshold make a shape rather than a coincidence. Two cells can be
// the two ends of a single hit lying on a cell boundary; three is the smallest patch that is not.
int64_t min_blob_cells = 3;
// ...unless one cell on its own is decisive. The rule above is about coincidences, and a lone
// cell scoring near the top of a saturating score is not one - a crystal smaller than the grid
// step lights exactly one cell, and refusing it would lose precisely the samples a fine raster is
// run to find. Well above protein_score_threshold on purpose: this admits the obvious case, it
// does not lower the general threshold by the back door.
float decisive_single_cell_score = 0.9f;
// Most crystals reported. A raster over a loop full of shards can label dozens of blobs, and past
// the first few the list is no longer a ranking anyone acts on - the DAQ collects from the top of
// it. Crystals are sorted by score, so this keeps the best.
int64_t max_crystals = 10;
// Whether each raster cell is indexed as well as scored.
//
// On by default. It is affordable - a raster runs at up to 100 Hz, which the FFT indexer keeps up
// with - and it is additive: the blobs are still found on the protein score, so indexing changes
// nothing about which cells are called crystals and only adds what was found in them. The lattice
// count per cell is the cheapest multi-lattice or cracked-crystal signal there is, and on a
// fixed-target serial experiment with a known cell (-C, ffbidx) a raster that indexes is most of
// the measurement. Turn it off for a very large raster where the GPU is the constraint.
bool indexing = true;
public:
GridScanAnalysisSettings& ProteinScoreThreshold(float input);
GridScanAnalysisSettings& MinBlobCells(int64_t input);
GridScanAnalysisSettings& DecisiveSingleCellScore(float input);
GridScanAnalysisSettings& MaxCrystals(int64_t input);
GridScanAnalysisSettings& Indexing(bool input);
[[nodiscard]] float GetProteinScoreThreshold() const;
[[nodiscard]] int64_t GetMinBlobCells() const;
[[nodiscard]] float GetDecisiveSingleCellScore() const;
[[nodiscard]] int64_t GetMaxCrystals() const;
[[nodiscard]] bool IsIndexing() const;
};