// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include "../common/AnalysisSettings.h" #include "../common/DiffractionExperiment.h" #include "../frame_serialize/CBORStream2Serializer.h" #include "../frame_serialize/CBORStream2Deserializer.h" #include "../writer/FileWriter.h" #include "../reader/JFJochHDF5Reader.h" TEST_CASE("AnalysisMode_Default", "[AnalysisMode]") { // MXStills, not None: a deployment whose configuration predates the mode must keep analysing. CHECK(AnalysisSettings().GetMode() == AnalysisMode::MXStills); CHECK(DiffractionExperiment().GetAnalysisMode() == AnalysisMode::MXStills); } TEST_CASE("AnalysisMode_Names", "[AnalysisMode]") { for (auto mode : {AnalysisMode::None, AnalysisMode::MXRotation, AnalysisMode::MXStills, AnalysisMode::Azint, AnalysisMode::Grid, AnalysisMode::PowderCalibration}) { auto back = AnalysisModeFromName(AnalysisModeName(mode)); REQUIRE(back.has_value()); CHECK(*back == mode); } CHECK_FALSE(AnalysisModeFromName("").has_value()); CHECK_FALSE(AnalysisModeFromName("mx").has_value()); } TEST_CASE("AnalysisMode_Stages", "[AnalysisMode]") { // The table itself. Bragg integration must never be on where indexing is off - nothing is // predicted without a lattice. for (auto mode : {AnalysisMode::None, AnalysisMode::MXRotation, AnalysisMode::MXStills, AnalysisMode::Azint, AnalysisMode::Grid, AnalysisMode::PowderCalibration}) { const auto s = AnalysisModeStages(mode); CHECK((!s.bragg_integration || s.indexing)); CHECK((!s.scale_merge || s.bragg_integration)); } CHECK(AnalysisModeStages(AnalysisMode::MXStills).indexing); CHECK(AnalysisModeStages(AnalysisMode::MXRotation).indexing); const auto none = AnalysisModeStages(AnalysisMode::None); CHECK_FALSE(none.spot_finding); CHECK_FALSE(none.azimuthal_integration); const auto azint = AnalysisModeStages(AnalysisMode::Azint); CHECK_FALSE(azint.spot_finding); CHECK_FALSE(azint.indexing); CHECK_FALSE(azint.scoring); CHECK(azint.azimuthal_integration); // Calibration keeps the spot finder: --calibration spots fits the pooled spots. const auto calib = AnalysisModeStages(AnalysisMode::PowderCalibration); CHECK(calib.spot_finding); CHECK_FALSE(calib.indexing); const auto grid = AnalysisModeStages(AnalysisMode::Grid); CHECK(grid.spot_finding); CHECK(grid.scoring); CHECK_FALSE(grid.indexing); } TEST_CASE("AnalysisMode_PrecedenceOverSpotFindingSwitch", "[AnalysisMode]") { DiffractionExperiment experiment; DatasetSettings dataset; dataset.SpotFindingEnable(true).MaxSpotCount(500); experiment.ImportDatasetSettings(dataset); CHECK(experiment.IsSpotFindingEnabled()); // A mode that analyses no spots wins over the deprecated per-dataset switch, and takes the spot // budget with it. experiment.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::Azint)); CHECK_FALSE(experiment.IsSpotFindingEnabled()); CHECK(experiment.GetMaxSpotCount() == 0); // Under a mode that does find spots, the deprecated switch is still able to turn it off. experiment.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::MXStills)); CHECK(experiment.IsSpotFindingEnabled()); dataset.SpotFindingEnable(false); experiment.ImportDatasetSettings(dataset); CHECK_FALSE(experiment.IsSpotFindingEnabled()); } TEST_CASE("AnalysisMode_PowderCalibrationForcesCPUAzInt", "[AnalysisMode]") { // The FPGA integration core holds 2048 bins in total, so a sectored profile cannot be built there. DiffractionExperiment experiment; REQUIRE_FALSE(experiment.GetAzimuthalIntegrationSettings().IsForceCPUinFPGAWorkflow()); experiment.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::PowderCalibration)); CHECK(experiment.GetAzimuthalIntegrationSettings().IsForceCPUinFPGAWorkflow()); CHECK(experiment.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount() == CALIBRATION_AZIM_BINS_DEFAULT); // An explicit sector count stands; the mode only supplies one where none is usable. DiffractionExperiment explicit_bins; explicit_bins.ImportAzimuthalIntegrationSettings( AzimuthalIntegrationSettings().AzimuthalBinCount(64)); explicit_bins.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::PowderCalibration)); CHECK(explicit_bins.GetAzimuthalIntegrationSettings().GetAzimuthalBinCount() == 64); } TEST_CASE("AnalysisMode_CBORStartRoundTrip", "[AnalysisMode][CBOR]") { std::vector buffer(1024 * 1024); CBORStream2Serializer serializer(buffer.data(), buffer.size()); DiffractionExperiment experiment; experiment.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::Grid)); StartMessage message{}; experiment.FillMessage(message); REQUIRE(message.analysis_mode.has_value()); REQUIRE_NOTHROW(serializer.SerializeSequenceStart(message)); auto deserialized = CBORStream2Deserialize(buffer.data(), serializer.GetBufferSize()); REQUIRE(deserialized); REQUIRE(deserialized->start_message); REQUIRE(deserialized->start_message->analysis_mode.has_value()); CHECK(*deserialized->start_message->analysis_mode == AnalysisMode::Grid); } TEST_CASE("AnalysisMode_CBORStartAbsentMode", "[AnalysisMode][CBOR]") { // A stream written before the mode existed says nothing, and is read back as saying nothing - // naming a mode for it would be an invention rather than provenance. std::vector buffer(1024 * 1024); CBORStream2Serializer serializer(buffer.data(), buffer.size()); StartMessage message{}; REQUIRE_NOTHROW(serializer.SerializeSequenceStart(message)); auto deserialized = CBORStream2Deserialize(buffer.data(), serializer.GetBufferSize()); REQUIRE(deserialized); REQUIRE(deserialized->start_message); CHECK_FALSE(deserialized->start_message->analysis_mode.has_value()); } TEST_CASE("AnalysisMode_HDF5MasterRoundTrip", "[AnalysisMode][HDF5][Full]") { // The mode is dataset-wide metadata: written to the master and read back from it, so a stored // file re-opens knowing what produced it. DiffractionExperiment x(DetJF(1)); x.FilePrefix("test_analysis_mode").ImagesPerTrigger(1).OverwriteExistingFiles(true); x.BeamX_pxl(100).BeamY_pxl(200).DetectorDistance_mm(150) .IncidentEnergy_keV(WVL_1A_IN_KEV).PixelSigned(false).BitDepthImage(16) .FrameTime(std::chrono::microseconds(500), std::chrono::microseconds(10)); x.ImportAnalysisSettings(AnalysisSettings().Mode(AnalysisMode::Grid)); RegisterHDF5Filter(); std::vector image(x.GetPixelsNum(), 0); { StartMessage start_message; x.FillMessage(start_message); FileWriter file_set(start_message); DataMessage message{}; message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum()); message.number = 0; REQUIRE_NOTHROW(file_set.WriteHDF5(message)); EndMessage end_message; end_message.max_image_number = 1; REQUIRE_NOTHROW(file_set.WriteHDF5(end_message)); file_set.Finalize(); } { JFJochHDF5Reader reader; reader.ReadFile("test_analysis_mode_master.h5"); auto dataset = reader.GetDataset(); REQUIRE(dataset->file_analysis_mode.has_value()); CHECK(*dataset->file_analysis_mode == AnalysisMode::Grid); } remove("test_analysis_mode_master.h5"); remove("test_analysis_mode_data_000001.h5"); }