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
130 lines
5.8 KiB
C++
130 lines
5.8 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "../common/GridScanSettings.h"
|
|
#include "../common/ScanResult.h"
|
|
#include "../image_analysis/grid_scan_analysis/AnalyzeGridScan.h"
|
|
#include "../rugnux/RasterReport.h"
|
|
|
|
// A 5 x 4 raster with one 2 x 2 patch of diffracting cells in it - the smallest scan that has a
|
|
// crystal to report and cells around it that are not one.
|
|
namespace {
|
|
constexpr int64_t GRID_NX = 5;
|
|
constexpr int64_t GRID_NY = 4;
|
|
|
|
GridScanSettings TestGrid() {
|
|
GridScanSettings grid(GRID_NX, 20.0f, 16.0f, /*snake=*/false, /*vertical=*/false);
|
|
grid.ImageNum(GRID_NX * GRID_NY);
|
|
return grid;
|
|
}
|
|
|
|
ScanResult TestScan() {
|
|
ScanResult scan;
|
|
for (int64_t i = 0; i < GRID_NX * GRID_NY; i++) {
|
|
const bool crystal = (i == 6 || i == 7 || i == 11 || i == 12);
|
|
ScanResultElem elem;
|
|
elem.number = i;
|
|
elem.protein_score = crystal ? 0.9f : 0.1f;
|
|
elem.ice_score = 0.05f;
|
|
if (crystal)
|
|
elem.res = 2.0f;
|
|
scan.images.push_back(elem);
|
|
}
|
|
return scan;
|
|
}
|
|
|
|
RasterSettings TestSettings() {
|
|
RasterSettings settings;
|
|
settings.protein_score_threshold = PROTEIN_SCORE_THRESHOLD_DEFAULT;
|
|
settings.min_blob_cells = MIN_BLOB_CELLS_DEFAULT;
|
|
settings.beam_size_x_um = 30.0f;
|
|
settings.beam_size_y_um = 10.0f;
|
|
settings.beam_size_source = "COMMAND_LINE";
|
|
return settings;
|
|
}
|
|
}
|
|
|
|
TEST_CASE("RasterReport_Render", "[Diagnostics]") {
|
|
const GridScanSettings grid = TestGrid();
|
|
const ScanResult scan = TestScan();
|
|
const RasterSettings settings = TestSettings();
|
|
const GridScanResult crystals = AnalyzeGridScan(scan, grid, settings.beam_size_x_um,
|
|
settings.beam_size_y_um,
|
|
settings.protein_score_threshold,
|
|
settings.min_blob_cells);
|
|
REQUIRE(crystals.crystals.size() == 1);
|
|
|
|
const std::string report = RenderRasterReport("test_master.h5", grid, scan, crystals, settings);
|
|
|
|
// The keys are the interface a battery greps; pin the spellings and the values that say what
|
|
// the run saw.
|
|
CHECK(report.find("RASTER_REPORT_VERSION= 1\n") != std::string::npos);
|
|
CHECK(report.find("INPUT_FILE= test_master.h5\n") != std::string::npos);
|
|
CHECK(report.find("GRID_SIZE= 5 4\n") != std::string::npos);
|
|
CHECK(report.find("GRID_STEP_UM= 20.00 16.00\n") != std::string::npos);
|
|
CHECK(report.find("GRID_N_ELEM= 20\n") != std::string::npos);
|
|
CHECK(report.find("BEAM_SIZE_UM= 30.00 10.00\n") != std::string::npos);
|
|
CHECK(report.find("BEAM_SIZE_SOURCE= COMMAND_LINE\n") != std::string::npos);
|
|
CHECK(report.find("IMAGES_SCORED= 20\n") != std::string::npos);
|
|
CHECK(report.find("IMAGES_ABOVE_THRESHOLD= 4\n") != std::string::npos);
|
|
CHECK(report.find("ICE_ABOVE_THRESHOLD= 0\n") != std::string::npos);
|
|
CHECK(report.find("CRYSTAL_COUNT= 1\n") != std::string::npos);
|
|
CHECK(report.find("END OF REPORT") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("RasterReport_Json", "[Diagnostics]") {
|
|
const GridScanSettings grid = TestGrid();
|
|
const ScanResult scan = TestScan();
|
|
const RasterSettings settings = TestSettings();
|
|
const GridScanResult crystals = AnalyzeGridScan(scan, grid, settings.beam_size_x_um,
|
|
settings.beam_size_y_um,
|
|
settings.protein_score_threshold,
|
|
settings.min_blob_cells);
|
|
|
|
const nlohmann::json j = nlohmann::json::parse(
|
|
RenderRasterJson("test_master.h5", grid, scan, crystals, settings));
|
|
|
|
CHECK(j["grid"]["size_x"] == GRID_NX);
|
|
CHECK(j["grid"]["size_y"] == GRID_NY);
|
|
CHECK(j["scores"]["images_above_threshold"] == 4);
|
|
CHECK(j["settings"]["beam_size_x_um"].get<float>() == Catch::Approx(30.0));
|
|
CHECK(j["settings"]["beam_size_y_um"].get<float>() == Catch::Approx(10.0));
|
|
REQUIRE(j["crystal_count"] == 1);
|
|
REQUIRE(j["crystals"].size() == 1);
|
|
|
|
// The crystal the JSON carries is the one the report's table carries - same numbers, typed.
|
|
const auto &c = j["crystals"][0];
|
|
CHECK(c["n_images"] == 4);
|
|
CHECK(c["image_number"] == crystals.crystals[0].image_number);
|
|
CHECK(c["res_A"].get<float>() == Catch::Approx(2.0));
|
|
CHECK(c["score"].get<float>() == Catch::Approx(0.9).margin(1e-5));
|
|
}
|
|
|
|
TEST_CASE("RasterReport_NoCrystal", "[Diagnostics]") {
|
|
// A raster over an empty loop: the report still has to say so, with a table that is empty rather
|
|
// than absent - a battery that greps CRYSTAL_COUNT must find it on every run.
|
|
const GridScanSettings grid = TestGrid();
|
|
ScanResult scan = TestScan();
|
|
for (auto &elem : scan.images)
|
|
elem.protein_score = 0.1f;
|
|
|
|
const RasterSettings settings = TestSettings();
|
|
const GridScanResult crystals = AnalyzeGridScan(scan, grid, settings.beam_size_x_um,
|
|
settings.beam_size_y_um,
|
|
settings.protein_score_threshold,
|
|
settings.min_blob_cells);
|
|
CHECK(crystals.crystals.empty());
|
|
|
|
const std::string report = RenderRasterReport("test_master.h5", grid, scan, crystals, settings);
|
|
CHECK(report.find("CRYSTAL_COUNT= 0\n") != std::string::npos);
|
|
CHECK(report.find("IMAGES_ABOVE_THRESHOLD= 0\n") != std::string::npos);
|
|
|
|
const nlohmann::json j = nlohmann::json::parse(
|
|
RenderRasterJson("test_master.h5", grid, scan, crystals, settings));
|
|
CHECK(j["crystal_count"] == 0);
|
|
CHECK(j["crystals"].empty());
|
|
}
|