From 5fe8a967cdcba2b9edcd3d8e1e0a46db06790da7 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 8 Sep 2026 07:59:04 +0200 Subject: [PATCH] grid scan: the five lanes become one, with a single home for the crystal type and its settings Integration of the per-image detection scores, the analysis mode, the grid-scan crystal search, its rugnux entry point and the viewer display. GridScanCrystal/GridScanResult had two definitions - a placeholder in common/ and the real one in image_analysis/ - which is a redefinition in any translation unit reaching both, and tests/RasterReportTest.cpp reaches both. Unified into common/GridScanResult.h, beside ScanResult where the data type belongs, leaving the algorithm in image_analysis/. Same reason UnitCell lives in common while the indexers do not. GridScanAnalysisSettings is now the only home for the search parameters, replacing the loose GridScanAnalysisParameters struct the raster lane carried while the class did not yet exist. Three values changed with the move: - decisive_single_cell_score 0.9 -> 0.6. 0.9 drops a real two-cell crystal peaking at 0.751 and costs a loop on the labelled corpus. 0.6 is the middle of a measured gap: over 67 rasters no water raster peaks above 0.15 and no ice raster above 0.50, while the weakest confirmed-protein raster peaks at 0.67. - max_crystals is std::optional, unset meaning no cap. 0 as a sentinel for "unlimited" reads as "find nothing", which is the opposite of what it did. - grow_score_threshold was missing from the class entirely. The viewer reads protein_score, ice_score and the crystal list from the reader rather than a local stub, and asks the broker for ice_ring_ratio rather than the retired ice_ring_score spelling. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N --- common/GridScanAnalysisSettings.cpp | 13 +++- common/GridScanAnalysisSettings.h | 59 +++++++++-------- common/GridScanResult.h | 65 ++++++++----------- image_analysis/IceScore.h | 2 +- .../grid_scan_analysis/AnalyzeGridScan.cpp | 17 ++--- .../grid_scan_analysis/AnalyzeGridScan.h | 46 +------------ .../grid_scan_analysis/CMakeLists.txt | 2 +- .../grid_scan_analysis/GridScanResult.h | 43 ------------ rugnux/RasterReport.h | 5 +- rugnux/rugnux_cli.cpp | 24 +++---- viewer/CMakeLists.txt | 1 - viewer/GridScanDataStub.h | 56 ---------------- viewer/JFJochHttpReader.cpp | 2 +- viewer/JFJochViewerDatasetInfo.cpp | 7 +- viewer/image_viewer/JFJochGridScanImage.h | 2 +- 15 files changed, 105 insertions(+), 239 deletions(-) delete mode 100644 image_analysis/grid_scan_analysis/GridScanResult.h delete mode 100644 viewer/GridScanDataStub.h 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)