rugnux: --mode raster reads a stored grid scan and reports the crystals in it

The offline entry point for a raster. It scores every image of a stored grid scan
- spot finding for the per-cell resolution, no indexing, because a raster answers
where the crystal is and not what its lattice is - hands the per-image protein and
ice scores to AnalyzeGridScan, and writes what came back as two files beside the
usual output: <prefix>_raster_report.txt in the idiom of the results report, and
<prefix>_raster.json with the same content typed, so a battery can aggregate a
sweep without scraping prose.

--beam-size states the beam at the sample, defaulting to the file's
incident_beam_size. It matters more than it looks: the reported crystal sizes are
measured and still contain the beam, and taking an anisotropic beam back out is a
subtraction of two covariance matrices, so a beam given as square when it is not
rotates the reported crystal axis. --raster-protein-threshold and
--raster-min-cells expose the two constants AnalyzeGridScan held at file scope, so
a sweep can vary them without a rebuild; they are now defaulted parameters with the
old values, and every existing call is unchanged.

The observer feeding the analysis reads the file's own image number
(DataMessage::original_number), not the loop's ordinal, so -s/-e/--stride cannot
silently shift the grid mapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-09-08 00:01:39 +02:00
co-authored by Claude Opus 5
parent 3be28a99ad
commit 21eca107fa
10 changed files with 659 additions and 19 deletions
@@ -10,21 +10,15 @@
#include "AnalyzeGridScan.h"
namespace {
// 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 = 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.
constexpr int64_t MIN_BLOB_CELLS = 3;
constexpr float NO_VALUE = -1.0f;
}
GridScanResult AnalyzeGridScan(const ScanResult &scan,
const GridScanSettings &grid,
float beam_size_x_um,
float beam_size_y_um) {
float beam_size_y_um,
float protein_score_threshold,
int64_t min_blob_cells) {
const int64_t nx = grid.GetGridSizeX_step();
const int64_t ny = grid.GetGridSizeY_step();
const float step_x = fabsf(grid.GetGridStepX_um());
@@ -49,9 +43,9 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan,
std::vector<uint8_t> above(nx * ny);
for (int64_t i = 0; i < nx * ny; i++)
above[i] = (protein[i] > PROTEIN_SCORE_THRESHOLD) ? 1 : 0;
above[i] = (protein[i] > protein_score_threshold) ? 1 : 0;
const std::vector<int32_t> label = LabelConnectedComponents(above, nx, ny, MIN_BLOB_CELLS);
const std::vector<int32_t> label = LabelConnectedComponents(above, nx, ny, min_blob_cells);
const int32_t n_label = label.empty() ? 0 : *std::max_element(label.begin(), label.end());
GridScanResult result;
@@ -65,7 +59,7 @@ GridScanResult AnalyzeGridScan(const ScanResult &scan,
cell.push_back(i);
}
if (cell.empty())
continue; // label fell below MIN_BLOB_CELLS and was erased
continue; // label fell below min_blob_cells and was erased
const auto n = static_cast<float>(cell.size());