Files
Jungfraujoch/tests/RasterReportTest.cpp
T
leonarski_fandClaude Opus 5 21eca107fa 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
2026-09-08 00:01:39 +02:00

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());
}