diff --git a/common/GridScanAnalysisSettings.cpp b/common/GridScanAnalysisSettings.cpp index b90f98b33..3752f6609 100644 --- a/common/GridScanAnalysisSettings.cpp +++ b/common/GridScanAnalysisSettings.cpp @@ -8,6 +8,11 @@ GridScanAnalysisSettings &GridScanAnalysisSettings::ProteinScoreThreshold(float return *this; } +GridScanAnalysisSettings &GridScanAnalysisSettings::GrowScoreThreshold(float input) { + grow_score_threshold = input; + return *this; +} + GridScanAnalysisSettings &GridScanAnalysisSettings::MinBlobCells(int64_t input) { min_blob_cells = input; return *this; @@ -18,7 +23,7 @@ GridScanAnalysisSettings &GridScanAnalysisSettings::DecisiveSingleCellScore(floa return *this; } -GridScanAnalysisSettings &GridScanAnalysisSettings::MaxCrystals(int64_t input) { +GridScanAnalysisSettings &GridScanAnalysisSettings::MaxCrystals(std::optional input) { max_crystals = input; return *this; } @@ -32,6 +37,10 @@ float GridScanAnalysisSettings::GetProteinScoreThreshold() const { return protein_score_threshold; } +float GridScanAnalysisSettings::GetGrowScoreThreshold() const { + return grow_score_threshold; +} + int64_t GridScanAnalysisSettings::GetMinBlobCells() const { return min_blob_cells; } @@ -40,7 +49,7 @@ float GridScanAnalysisSettings::GetDecisiveSingleCellScore() const { return decisive_single_cell_score; } -int64_t GridScanAnalysisSettings::GetMaxCrystals() const { +std::optional GridScanAnalysisSettings::GetMaxCrystals() const { return max_crystals; } diff --git a/common/GridScanAnalysisSettings.h b/common/GridScanAnalysisSettings.h index 3221e8e31..773b26c0b 100644 --- a/common/GridScanAnalysisSettings.h +++ b/common/GridScanAnalysisSettings.h @@ -4,41 +4,46 @@ #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) is to take this object as one argument - +// 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. // -// 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. +// 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; - // 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. + // 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. 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; + // ...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 @@ -50,14 +55,16 @@ class GridScanAnalysisSettings { bool indexing = true; public: GridScanAnalysisSettings& ProteinScoreThreshold(float input); + GridScanAnalysisSettings& GrowScoreThreshold(float input); GridScanAnalysisSettings& MinBlobCells(int64_t input); GridScanAnalysisSettings& DecisiveSingleCellScore(float input); - GridScanAnalysisSettings& MaxCrystals(int64_t 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]] int64_t GetMaxCrystals() const; + [[nodiscard]] std::optional GetMaxCrystals() const; [[nodiscard]] bool IsIndexing() const; }; diff --git a/common/GridScanResult.h b/common/GridScanResult.h index 99c8e3035..5e7ac9a6e 100644 --- a/common/GridScanResult.h +++ b/common/GridScanResult.h @@ -3,51 +3,40 @@ #pragma once -// PLACEHOLDER - TO BE REPLACED AT INTEGRATION. The real GridScanCrystal/GridScanResult live in -// image_analysis/grid_scan_analysis/GridScanResult.h, alongside the analysis that owns the ranking, -// clustering and crystal selection producing them. This file carries only the shape the rest of the -// system needs, so that the /result/scan payload, the CBOR END block and the HDF5 master could land -// without waiting for it; the field list has been checked against the real header and agrees with it. -// -// To integrate: delete this file and its line in common/CMakeLists.txt, and point the two includes - -// common/ScanResult.h and common/JFJochMessages.h - at the real header. Nothing else refers to these -// two structs. The grid_scan_crystal / grid_scan_result schemas in broker/jfjoch_api.yaml are the -// same placeholder and go the same way. - #include #include #include -// One crystal picked out of a grid scan. Positions are in the grid's own frame: nx/ny are fractional -// grid coordinates and x_um/y_um the same point as a signed offset along the two grid axes, so a -// consumer can address the crystal either by grid step or by stage motion without redoing the -// arithmetic. +// One crystal found in a grid scan. Positions are in the display grid of GridScanSettings - +// column 0 is the lowest x, row 0 the lowest y, whatever direction the stage actually moved in - +// so they match the per-image positions the scan writes with GetXContainer_m/GetYContainer_m. +// +// The shape is an ORIENTED extent, not a bounding box: a needle lying at 45 degrees has a bounding +// box of its own length in both directions and would read as a compact blob, which is the one case +// the axes exist to expose. major_um >= minor_um always, and angle_deg points along major_um. struct GridScanCrystal { - float nx = 0.0f; // centre, fractional grid coordinate along the fast grid axis - float ny = 0.0f; // centre, fractional grid coordinate along the slow grid axis - float x_um = 0.0f; // centre, signed offset along the fast grid axis - float y_um = 0.0f; // centre, signed offset along the slow grid axis - int64_t image_number = -1; // the grid point at the centre, as an image ordinal - float major_um = 0.0f; // extent along the crystal's own major principal axis - float minor_um = 0.0f; // extent along its minor principal axis - // Direction of the major axis from the +x grid axis, counter-clockwise. An AXIS, not a direction: - // it lives in [0, 180) and wraps, so 179 deg and 0 deg are adjacent. Averaging two such angles - // arithmetically across the wrap turns two nearly parallel needles into a right angle - combine - // them on the doubled angle (mean of 2*theta, halved) or not at all. - float angle_deg = 0.0f; - float score = 0.0f; - float ice_score = 0.0f; - float res_A = NAN; // NaN where no resolution was measured in the blob - int64_t n_images = 0; // grid points that fell in this crystal + float nx = 0, ny = 0; // centre, grid coords, fractional, 0-based + float x_um = 0, y_um = 0; // centre, signed offset from centre of cell (0,0), along grid axes + int64_t image_number = -1; // nearest COLLECTED image, for the DAQ to address + float major_um = 0, minor_um = 0; // extent along the crystal's own principal axes + // Angle of the major axis from the +x grid axis, counter-clockwise. This is an AXIS, so it lives + // in [0,180) and 179 is adjacent to 0 - anything averaging or comparing angles must know that. + // Arbitrary when major_um and minor_um are close; let that ratio say so rather than trusting it. + float angle_deg = 0; + float score = 0, ice_score = 0; // 0-1, the patch MEAN + float peak_score = 0; // 0-1, the patch peak - what a one-cell admission was decided on + float res_A = NAN; // robust best resolution in the blob, NaN if none was measured + int64_t n_images = 0; // grid points that fell in this crystal }; -// The crystals a raster found, sorted by score descending. Today the analysis returns zero or one, but -// N is the design intent: nothing may assume at most one entry. +// Sorted by score, strongest first, so crystals[0] is the one to collect. Today the list usually +// holds nothing or one entry; more than one is the point of the design, not an error. +// +// The extents are MEASURED - the beam is not removed. It is reported here so a consumer can do that +// itself, and reversibly: removing an anisotropic beam is a subtraction of covariance matrices +// followed by re-diagonalisation, NOT a per-axis quadrature removal, which is silently wrong for any +// crystal not aligned with the grid. struct GridScanResult { std::vector crystals; - - // The beam the extents above were measured with, along the grid axes. Those extents still contain - // it, so this says what a consumer has to take back out. - float beam_size_x_um = 0.0f; - float beam_size_y_um = 0.0f; + float beam_size_x_um = 0, beam_size_y_um = 0; }; diff --git a/image_analysis/IceScore.h b/image_analysis/IceScore.h index a3cdc522a..036508edd 100644 --- a/image_analysis/IceScore.h +++ b/image_analysis/IceScore.h @@ -9,7 +9,7 @@ #include "../common/SpotToSave.h" // Is there CRYSTALLINE ICE on this image? A detection score in [0,1] that saturates: a loop buried in -// ice and one carrying a single detectable ring both come out near 1. Unlike ice_ring_score - which is +// ice and one carrying a single detectable ring both come out near 1. Unlike ice_ring_ratio - which is // a ratio, unbounded, and answers "how strong is the worst ring" - this answers only "is ice present", // and it is the number to threshold. // diff --git a/image_analysis/grid_scan_analysis/AnalyzeGridScan.cpp b/image_analysis/grid_scan_analysis/AnalyzeGridScan.cpp index fc140701d..14e919de3 100644 --- a/image_analysis/grid_scan_analysis/AnalyzeGridScan.cpp +++ b/image_analysis/grid_scan_analysis/AnalyzeGridScan.cpp @@ -17,7 +17,7 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan, const GridScanSettings &grid, float beam_size_x_um, float beam_size_y_um, - const GridScanAnalysisParameters ¶ms) { + const GridScanAnalysisSettings &settings) { const int64_t nx = grid.GetGridSizeX_step(); const int64_t ny = grid.GetGridSizeY_step(); const float step_x = fabsf(grid.GetGridStepX_um()); @@ -47,7 +47,7 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan, // background that never reaches the seed level produces nothing. std::vector grown(nx * ny); for (int64_t i = 0; i < nx * ny; i++) - grown[i] = (protein[i] > params.grow_score_threshold) ? 1 : 0; + grown[i] = (protein[i] > settings.GetGrowScoreThreshold()) ? 1 : 0; // Labelled with no size cut of its own: whether a patch is big enough is no longer a plain // floor - a small patch survives on the strength of its diffraction - and that test needs the @@ -72,7 +72,7 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan, // whatever its shape, so nothing about it is worth measuring. bool has_seed = false; for (int64_t i: cell) - has_seed = has_seed || protein[i] > params.protein_score_threshold; + has_seed = has_seed || protein[i] > settings.GetProteinScoreThreshold(); if (!has_seed) continue; @@ -176,8 +176,8 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan, // patch no seed would have admitted, because the patch has already been discarded above // unless it holds a seed cell - and a seed cell is by definition the strongest kind there // is, so the peak of a grown patch is the peak of its seeds. - if (static_cast(cell.size()) < params.min_blob_cells - && peak_protein < params.decisive_protein_score) + if (static_cast(cell.size()) < settings.GetMinBlobCells() + && peak_protein < settings.GetDecisiveSingleCellScore()) continue; GridScanCrystal crystal; @@ -228,9 +228,10 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan, // Kept to the best few only where a caller asked for that; the sort above is what makes the // ones it keeps the right ones. - if (params.max_crystals > 0 - && result.crystals.size() > static_cast(params.max_crystals)) - result.crystals.resize(params.max_crystals); + // Unset means no cap: a crystal found and then dropped is information the caller cannot recover. + if (const auto cap = settings.GetMaxCrystals(); + cap.has_value() && result.crystals.size() > static_cast(*cap)) + result.crystals.resize(*cap); return result; } diff --git a/image_analysis/grid_scan_analysis/AnalyzeGridScan.h b/image_analysis/grid_scan_analysis/AnalyzeGridScan.h index c8f15041e..6d9529b33 100644 --- a/image_analysis/grid_scan_analysis/AnalyzeGridScan.h +++ b/image_analysis/grid_scan_analysis/AnalyzeGridScan.h @@ -5,7 +5,8 @@ #include "../../common/GridScanSettings.h" #include "../../common/ScanResult.h" -#include "GridScanResult.h" +#include "../../common/GridScanResult.h" +#include "../../common/GridScanAnalysisSettings.h" // Finds the crystals in a completed grid scan: the per-image protein score is scattered back onto // the raster, the map is thresholded and labelled, and each blob is reported as one crystal. @@ -23,49 +24,8 @@ // axes - and a needle at an arbitrary angle is exactly the case this whole design exists for. // beam_size_x_um/beam_size_y_um are only copied into the result, so it says what the sizes contain. -// A cell counts as protein above this. The per-image score saturates, so this only has to separate -// "something diffracted here" from "nothing did". -constexpr float PROTEIN_SCORE_THRESHOLD_DEFAULT = 0.5f; - -// Two cells can be the two ends of a single hit lying on a cell boundary; three is the smallest -// patch that is a shape rather than a coincidence - unless the diffraction in it is decisive, see -// below. -constexpr int64_t MIN_BLOB_CELLS_DEFAULT = 3; - -// 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 reaching only this level and never the seed level is -// not a patch at all, so no lowering of this can turn a weak background into a crystal. -constexpr float GROW_SCORE_THRESHOLD_DEFAULT = 0.35f; - -// A patch smaller than the minimum is kept anyway when its BEST cell scores at least this, so one -// cell is enough where that cell is clearly protein while a weak patch still has to be a shape. -// The bar is the peak and not the patch mean, because a two-cell patch with one strong cell and one -// marginal one is exactly the case this exists for and the mean would average the evidence away. -// Measured over 67 labelled rasters the two populations do not overlap at all: no water raster -// reaches 0.15 and no ice raster reaches 0.50, while the weakest protein raster peaks at 0.67. 0.6 -// is the middle of that gap. -constexpr float DECISIVE_PROTEIN_SCORE_DEFAULT = 0.6f; - -// How many crystals are returned, best first; 0 is all of them, which is the default because a -// crystal that was found and then dropped is information the caller cannot get back. -constexpr int64_t MAX_CRYSTALS_DEFAULT = 0; - -// What counts as a crystal. Grouped rather than passed loose so that the GridScanAnalysisSettings -// class in common/ can adopt it whole when it lands: these are the members and the defaults it -// should carry, and AnalyzeGridScan then takes that class in place of this struct with no other -// change here or at any call site. -struct GridScanAnalysisParameters { - float protein_score_threshold = PROTEIN_SCORE_THRESHOLD_DEFAULT; - float grow_score_threshold = GROW_SCORE_THRESHOLD_DEFAULT; - int64_t min_blob_cells = MIN_BLOB_CELLS_DEFAULT; - float decisive_protein_score = DECISIVE_PROTEIN_SCORE_DEFAULT; - int64_t max_crystals = MAX_CRYSTALS_DEFAULT; -}; - GridScanResult AnalyzeGridScan(const ScanResult &scan, const GridScanSettings &grid, float beam_size_x_um, float beam_size_y_um, - const GridScanAnalysisParameters ¶ms = {}); + const GridScanAnalysisSettings &settings = {}); diff --git a/image_analysis/grid_scan_analysis/CMakeLists.txt b/image_analysis/grid_scan_analysis/CMakeLists.txt index edc3ae5c5..c02192da2 100644 --- a/image_analysis/grid_scan_analysis/CMakeLists.txt +++ b/image_analysis/grid_scan_analysis/CMakeLists.txt @@ -1,2 +1,2 @@ -ADD_LIBRARY(JFJochGridScanAnalysis STATIC AnalyzeGridScan.cpp AnalyzeGridScan.h GridScanResult.h) +ADD_LIBRARY(JFJochGridScanAnalysis STATIC AnalyzeGridScan.cpp AnalyzeGridScan.h) TARGET_LINK_LIBRARIES(JFJochGridScanAnalysis JFJochCommon) diff --git a/image_analysis/grid_scan_analysis/GridScanResult.h b/image_analysis/grid_scan_analysis/GridScanResult.h deleted file mode 100644 index 1259f4846..000000000 --- a/image_analysis/grid_scan_analysis/GridScanResult.h +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include -#include -#include - -// One crystal found in a grid scan. Positions are in the display grid of GridScanSettings - -// column 0 is the lowest x, row 0 the lowest y, whatever direction the stage actually moved in - -// so they match the per-image positions the scan writes with GetXContainer_m/GetYContainer_m. -struct GridScanCrystal { - float nx = 0, ny = 0; // centre, grid coords, fractional, 0-based - float x_um = 0, y_um = 0; // centre, signed offset from centre of cell (0,0), along grid axes - int64_t image_number = -1; // nearest COLLECTED image, for the DAQ to address - // Extent along the crystal's own principal axes. major_um >= minor_um always, and - // angle_deg points along major_um - so a consumer can draw a major_um by minor_um frame - // rotated by angle_deg without checking which of the two is the longer. - float major_um = 0, minor_um = 0; - // Major axis from the +x grid axis, counter-clockwise. This is an AXIS, not a direction, so it - // lives in [0,180) and wraps there: 179 deg is adjacent to 0 deg, and code comparing two angles - // has to fold the difference into [0,90]. On a round blob the axis is arbitrary and the value is - // whatever the numerics produced - major_um/minor_um near 1 is what says so. - float angle_deg = 0; - float score = 0, ice_score = 0; // 0-1, the patch MEAN - // The best protein score in the patch. The mean above ranks the crystals; this says how strong - // the evidence in the patch ever gets, and it is what admits a patch too small to be a shape - // (AnalyzeGridScan). Reported so that admission can be checked against the number that decided it. - float peak_score = 0; - float res_A = NAN; // robust best resolution in the blob, NaN if none was measured - int64_t n_images = 0; -}; - -// Crystals found in one completed grid scan, sorted by score descending: element 0 is the one to -// collect. Empty when the raster hit nothing. -struct GridScanResult { - std::vector crystals; - - // The beam the sizes above were measured with, along the grid axes. The crystal extents still - // contain it (see AnalyzeGridScan), so this says what a consumer has to take back out. - float beam_size_x_um = 0, beam_size_y_um = 0; -}; diff --git a/rugnux/RasterReport.h b/rugnux/RasterReport.h index 6aa9e8da3..16e5de6c1 100644 --- a/rugnux/RasterReport.h +++ b/rugnux/RasterReport.h @@ -9,7 +9,8 @@ #include "../common/Logger.h" #include "../common/ScanResult.h" #include "../image_analysis/grid_scan_analysis/AnalyzeGridScan.h" -#include "../image_analysis/grid_scan_analysis/GridScanResult.h" +#include "../common/GridScanResult.h" +#include "../common/GridScanAnalysisSettings.h" #include "ResultReport.h" // RunProvenance // _raster_report.txt and _raster.json - what a grid scan found, written by @@ -22,7 +23,7 @@ // runs apart. The beam size is here because it is the one setting a wrong value silently rotates the // answer with - see AnalyzeGridScan. struct RasterSettings { - GridScanAnalysisParameters analysis; + GridScanAnalysisSettings analysis; float beam_size_x_um = 0, beam_size_y_um = 0; // Where the beam size came from: "FILE" (incident_beam_size), "COMMAND_LINE" (--beam-size) or // "NOT_STATED" (neither, and the sizes above are zero). diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index e7f2a2351..13bc8b824 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -125,7 +125,7 @@ void print_usage() { std::cout << " --raster-grow-score Once a patch has a cell above --raster-protein-threshold it is grown out to this score (default: 0.35), so a crystal is not broken in two by one cell that fell just under the higher bar. A patch that never reaches the higher bar is not a patch at all, so lowering this cannot invent one" << std::endl; std::cout << " --raster-min-cells Cells a connected patch must have before it is reported as a crystal (default: 3). A smaller patch is still reported when its best cell reaches --raster-decisive-score" << std::endl; std::cout << " --raster-decisive-score Protein score at which one cell is evidence enough on its own, so a patch below --raster-min-cells is still reported (default: 0.60). Over 67 labelled rasters no water raster reaches 0.15 and no ice raster 0.50, while the weakest protein raster peaks at 0.67" << std::endl; - std::cout << " --raster-max-crystals Report at most this many crystals, the best-scoring ones (default: 0, all of them)" << std::endl; + std::cout << " --raster-max-crystals Report at most this many crystals, the best-scoring ones (default: no cap - a crystal found and then dropped is information that cannot be recovered)" << std::endl; std::cout << std::endl; std::cout << " Calibration (--mode calibration)" << std::endl; @@ -721,7 +721,7 @@ static int RunRugnux(int argc, char **argv) { // once the dataset is open, and --beam-size overrides it there. std::optional beam_size_x_um, beam_size_y_um; // --beam-size // What counts as a crystal (--raster-*). The defaults are the ones AnalyzeGridScan states. - GridScanAnalysisParameters raster_analysis; + GridScanAnalysisSettings raster_analysis; bool rotation_indexing = false; bool force_still = false; // --force-still: process a rotation dataset as stills (indexing + scaling) bool two_pass_rotation = true; @@ -1155,24 +1155,24 @@ static int RunRugnux(int argc, char **argv) { break; } case OPT_RASTER_PROTEIN_THRESHOLD: - raster_analysis.protein_score_threshold = - parse_number_arg(optarg, "--raster-protein-threshold", logger, 0.0f, 1.0f); + raster_analysis.ProteinScoreThreshold( + parse_number_arg(optarg, "--raster-protein-threshold", logger, 0.0f, 1.0f)); break; case OPT_RASTER_MIN_CELLS: - raster_analysis.min_blob_cells = - parse_number_arg(optarg, "--raster-min-cells", logger, 1); + raster_analysis.MinBlobCells( + parse_number_arg(optarg, "--raster-min-cells", logger, 1)); break; case OPT_RASTER_GROW_SCORE: - raster_analysis.grow_score_threshold = - parse_number_arg(optarg, "--raster-grow-score", logger, 0.0f, 1.0f); + raster_analysis.GrowScoreThreshold( + parse_number_arg(optarg, "--raster-grow-score", logger, 0.0f, 1.0f)); break; case OPT_RASTER_DECISIVE_SCORE: - raster_analysis.decisive_protein_score = - parse_number_arg(optarg, "--raster-decisive-score", logger, 0.0f, 1.0f); + raster_analysis.DecisiveSingleCellScore( + parse_number_arg(optarg, "--raster-decisive-score", logger, 0.0f, 1.0f)); break; case OPT_RASTER_MAX_CRYSTALS: - raster_analysis.max_crystals = - parse_number_arg(optarg, "--raster-max-crystals", logger, 0); + raster_analysis.MaxCrystals( + parse_number_arg(optarg, "--raster-max-crystals", logger, 1)); break; case OPT_NO_MERGE: run_scaling = false; diff --git a/viewer/CMakeLists.txt b/viewer/CMakeLists.txt index d8b72df09..6f227c432 100644 --- a/viewer/CMakeLists.txt +++ b/viewer/CMakeLists.txt @@ -104,7 +104,6 @@ ADD_EXECUTABLE(jfjoch_viewer jfjoch_viewer.cpp JFJochViewerWindow.cpp JFJochView image_viewer/JFJochGridScanImage.cpp image_viewer/JFJochGridScanImage.h image_viewer/GridScanComposite.h - GridScanDataStub.h widgets/PowderCalibrationWidget.cpp widgets/PowderCalibrationWidget.h ${APP_RESOURCES} diff --git a/viewer/GridScanDataStub.h b/viewer/GridScanDataStub.h deleted file mode 100644 index 634515962..000000000 --- a/viewer/GridScanDataStub.h +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute -// SPDX-License-Identifier: GPL-3.0-only - -#pragma once - -#include - -#include "../reader/JFJochReaderDataset.h" - -// =========================================================================================== -// STUB - DELETE THIS FILE AT INTEGRATION. -// -// The composite grid-scan map is drawn from the per-image protein and ice scores and from the -// crystal list of the whole raster. None of the three is on JFJochReaderDataset yet; they arrive -// with a separate change, through reader/. Until then this header is the one place in the viewer -// that names them, so the widget and the dataset-info panel are already written against the real -// shape of the data. -// -// At integration: drop this file and its two includes (image_viewer/JFJochGridScanImage.h and -// JFJochViewerDatasetInfo.cpp), take GridScanCrystal from the reader instead, and replace the three -// accessors below with `ds.protein_score`, `ds.ice_score` and `ds.grid_scan_result` at their call -// sites in JFJochViewerDatasetInfo.cpp. Nothing else has to change: both data paths (HTTP and HDF5) -// fill the same JFJochReaderDataset, so both light up at once. -// =========================================================================================== - -// One crystal found in a grid scan. Position and extent are in the grid's own frame; the extent is -// along the crystal's own principal axes, which is why angle_deg is needed to draw it. -struct GridScanCrystal { - float nx = 0; // centre, fractional grid coordinates, 0-based - float ny = 0; - float x_um = 0; // centre, signed offset along the grid axes - float y_um = 0; - int64_t image_number = -1; - float major_um = 0; // extent along the crystal's own principal axes - float minor_um = 0; - float angle_deg = 0; // major axis from +x grid axis, counter-clockwise, in [0, 180) - float score = 0; - float ice_score = 0; - float res_A = 0; - int64_t n_images = 0; -}; - -inline const std::vector &GridScanProteinScore(const JFJochReaderDataset &) { - static const std::vector none; - return none; -} - -inline const std::vector &GridScanIceScore(const JFJochReaderDataset &) { - static const std::vector none; - return none; -} - -inline const std::vector &GridScanCrystals(const JFJochReaderDataset &) { - static const std::vector none; - return none; -} diff --git a/viewer/JFJochHttpReader.cpp b/viewer/JFJochHttpReader.cpp index 0a9db1022..3e398b211 100644 --- a/viewer/JFJochHttpReader.cpp +++ b/viewer/JFJochHttpReader.cpp @@ -268,7 +268,7 @@ std::shared_ptr JFJochHttpReader::UpdateDataset_i() { dataset->bkg_estimate = GetPlot_i("bkg_estimate"); dataset->spindle_blind_fraction = GetPlot_i("spindle_blind_fraction"); - dataset->ice_ring_ratio = GetPlot_i("ice_ring_score"); + dataset->ice_ring_ratio = GetPlot_i("ice_ring_ratio"); dataset->spot_count = GetPlot_i("spot_count"); dataset->spot_count_ice_rings = GetPlot_i("spot_count_ice"); dataset->spot_count_low_res = GetPlot_i("spot_count_low_res"); diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index 7e69d34dd..dc3adfcea 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -6,7 +6,6 @@ #include #include "JFJochViewerDatasetInfo.h" -#include "GridScanDataStub.h" // STUB - see the note in that file namespace { // Stable colour per run by its position in the run list (Original is index 0 -> blue), so a @@ -248,7 +247,7 @@ std::vector JFJochViewerDatasetInfo::ExtractMetric(const JFJochReaderData else if (val == 13) data = ds.indexing_lattice_count; else if (val == 14) data = ds.ice_ring_ratio; else if (val == 15) data = ds.spindle_blind_fraction; - else if (val == kCompositeMetric) data = GridScanProteinScore(ds); + else if (val == kCompositeMetric) data = ds.protein_score; else if (val >= 100) { const int roi_index = (val - 100) / 4; if (val % 4 == 0) { @@ -321,8 +320,8 @@ void JFJochViewerDatasetInfo::UpdatePlot() { if (dataset->experiment.GetGridScan()) { if (val == kCompositeMetric) - grid_scan_image->loadComposite(dataset->bkg_estimate, GridScanProteinScore(*dataset), - GridScanIceScore(*dataset), GridScanCrystals(*dataset), + grid_scan_image->loadComposite(dataset->bkg_estimate, dataset->protein_score, + dataset->ice_score, dataset->grid_crystals, dataset->experiment.GetGridScan().value()); else grid_scan_image->loadData(data, dataset->experiment.GetGridScan().value(), one_over_d2); diff --git a/viewer/image_viewer/JFJochGridScanImage.h b/viewer/image_viewer/JFJochGridScanImage.h index 2af6f44eb..a0c4d0a9d 100644 --- a/viewer/image_viewer/JFJochGridScanImage.h +++ b/viewer/image_viewer/JFJochGridScanImage.h @@ -5,7 +5,7 @@ #include "JFJochImage.h" #include "GridScanComposite.h" -#include "../GridScanDataStub.h" // STUB: GridScanCrystal - see the note in that file +#include "../../common/GridScanResult.h" #include "../../reader/JFJochReaderDataset.h" Q_DECLARE_METATYPE(GridScanSettings)