// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include #include // 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) takes this object rather than loose // parameters, so a knob has one default and one place where it is documented. // // The beam size is deliberately NOT here: 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; // A patch is GROWN out to this score once it has started, so a crystal is not broken in two by a // single cell that fell just under the seed threshold - a real split in the corpus had its // bridging cell at 0.498, two thousandths under it, which is a threshold artefact and not a gap // in a crystal. Growth can never start on its own: a patch that reaches only this level and // never the seed level is discarded, so lowering this cannot turn weak background into a crystal. float grow_score_threshold = 0.35f; // How many cells above the seed 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 - unless the diffraction in it is decisive, below. int64_t min_blob_cells = 3; // ...unless one cell on its own is decisive. 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. The // bar is the patch PEAK, not its mean: a two-cell patch with one strong cell and one marginal // one is the case this exists for, and a mean averages that evidence away. // // 0.6 is measured, not chosen. Over 67 labelled rasters the peak-score populations do not // overlap at all - no water raster reaches 0.15, no ice raster reaches 0.50, and the weakest // confirmed-protein raster peaks at 0.67 - so 0.6 is the middle of the gap. Raising it to 0.9 // drops a real two-cell crystal peaking at 0.751 and costs a loop on this corpus. float decisive_single_cell_score = 0.6f; // Most crystals reported, best first. Unset means no cap, which is the default: a crystal that // was found and then dropped is information the caller cannot get back. Set it where a loop full // of shards would otherwise label dozens of blobs that nobody acts on. std::optional max_crystals; // 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& GrowScoreThreshold(float input); GridScanAnalysisSettings& MinBlobCells(int64_t input); GridScanAnalysisSettings& DecisiveSingleCellScore(float input); GridScanAnalysisSettings& MaxCrystals(std::optional input); GridScanAnalysisSettings& Indexing(bool input); [[nodiscard]] float GetProteinScoreThreshold() const; [[nodiscard]] float GetGrowScoreThreshold() const; [[nodiscard]] int64_t GetMinBlobCells() const; [[nodiscard]] float GetDecisiveSingleCellScore() const; [[nodiscard]] std::optional GetMaxCrystals() const; [[nodiscard]] bool IsIndexing() const; };