// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include #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() == Catch::Approx(30.0)); CHECK(j["settings"]["beam_size_y_um"].get() == 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() == Catch::Approx(2.0)); CHECK(c["score"].get() == 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()); }