From d0a7d6a3145795caa0a2bfb2e51aa1c7e16e3523 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 10 Nov 2026 16:06:38 +0100 Subject: [PATCH] Broker: Q binning settings through the API and the config JSON q_binning (linear|log10) and q_bins are read and returned; q_spacing is now optional. Linear binning takes q_spacing or q_bins, not both (neither keeps the 0.01 1/A default); log10 needs q_bins and refuses q_spacing; a refused combination is a 400 with the reason. GET returns the spacing or the bin count that was set, and the binning. Clients that send only q_spacing work unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK --- broker/OpenAPIConvert.cpp | 18 +++++++- tests/DiffractionExperimentTest.cpp | 72 +++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/broker/OpenAPIConvert.cpp b/broker/OpenAPIConvert.cpp index b1c4432c1..73ef27641 100644 --- a/broker/OpenAPIConvert.cpp +++ b/broker/OpenAPIConvert.cpp @@ -450,7 +450,13 @@ AzimuthalIntegrationSettings Convert(const org::openapitools::server::model::Azi AzimuthalIntegrationSettings ret{}; ret.SolidAngleCorrection(input.isSolidAngleCorr()); ret.PolarizationCorrection(input.isPolarizationCorr()); - ret.QSpacing_recipA(input.getQSpacing()); + QBinSpacing spacing = QBinSpacing::Linear; + if (input.qBinningIsSet() + && input.getQBinning().getValue() == org::openapitools::server::model::Q_binning::eQ_binning::LOG10) + spacing = QBinSpacing::Log10; + ret.QBinning(spacing, + input.qSpacingIsSet() ? std::optional(input.getQSpacing()) : std::nullopt, + input.qBinsIsSet() ? std::optional(input.getQBins()) : std::nullopt); ret.QRange_recipA(input.getLowQRecipA(), input.highQRecipAIsSet() ? std::optional(input.getHighQRecipA()) : std::nullopt); @@ -466,7 +472,15 @@ org::openapitools::server::model::Azim_int_settings Convert(const AzimuthalInteg if (const auto high_q = settings.GetRequestedHighQ_recipA()) ret.setHighQRecipA(high_q.value()); ret.setLowQRecipA(settings.GetLowQ_recipA()); - ret.setQSpacing(settings.GetQSpacing_recipA()); + if (const auto q_spacing = settings.GetRequestedQSpacing_recipA()) + ret.setQSpacing(q_spacing.value()); + if (const auto q_bins = settings.GetRequestedQBinCount()) + ret.setQBins(q_bins.value()); + org::openapitools::server::model::Q_binning q_binning; + q_binning.setValue(settings.GetQBinSpacing() == QBinSpacing::Log10 + ? org::openapitools::server::model::Q_binning::eQ_binning::LOG10 + : org::openapitools::server::model::Q_binning::eQ_binning::LINEAR); + ret.setQBinning(q_binning); ret.setAzimuthalBins(settings.GetAzimuthalBinCount()); ret.setForceCpu(settings.IsForceCPUinFPGAWorkflow()); return ret; diff --git a/tests/DiffractionExperimentTest.cpp b/tests/DiffractionExperimentTest.cpp index e5db38e08..606a59d16 100644 --- a/tests/DiffractionExperimentTest.cpp +++ b/tests/DiffractionExperimentTest.cpp @@ -1262,3 +1262,75 @@ TEST_CASE("DiffractionExperiment_GridScanAtStationaryAxis", "[DiffractionExperim CHECK(!settings.GetGoniometer()->IsScanning()); CHECK(settings.GetGoniometer()->GetStart_deg() == Catch::Approx(90.0f)); } + +// The Q binning through the OpenAPI model, both ways. A client that only knows q_spacing keeps +// working, and what comes back is what was set. +TEST_CASE("DiffractionExperiment_AzIntegration_QBinning_OpenAPI", "[DiffractionExperiment]") { + auto parse = [](const char *json) { + org::openapitools::server::model::Azim_int_settings model; + from_json(nlohmann::json::parse(json), model); + return model; + }; + + SECTION("Linear by spacing, as before") { + const auto s = Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "high_q_recipA": 4.0, "q_spacing": 0.00087})")); + CHECK(s.GetQBinSpacing() == QBinSpacing::Linear); + CHECK(s.GetQSpacing_recipA() == 0.00087f); + CHECK(s.GetQBinCount() == static_cast(std::ceil((4.0f - 0.1f) / 0.00087f))); + const auto back = Convert(s); + REQUIRE(back.qSpacingIsSet()); + CHECK(back.getQSpacing() == 0.00087f); + CHECK(!back.qBinsIsSet()); + CHECK(back.getQBinning().getValue() == org::openapitools::server::model::Q_binning::eQ_binning::LINEAR); + } + + SECTION("Neither spacing nor bin count: default spacing") { + const auto s = Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "high_q_recipA": 4.0})")); + CHECK(s.GetQSpacing_recipA() == 0.01f); + CHECK(s.GetQBinCount() == static_cast(std::ceil((4.0f - 0.1f) / 0.01f))); + } + + SECTION("Linear by bin count") { + const auto s = Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "high_q_recipA": 4.1, "q_bins": 80})")); + CHECK(s.GetQBinCount() == 80); + CHECK(s.GetQSpacing_recipA() == Catch::Approx(0.05)); + const auto back = Convert(s); + CHECK(!back.qSpacingIsSet()); + REQUIRE(back.qBinsIsSet()); + CHECK(back.getQBins() == 80); + } + + SECTION("Log10") { + const auto s = Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.001, "high_q_recipA": 1.0, + "q_binning": "log10", "q_bins": 300})")); + CHECK(s.GetQBinSpacing() == QBinSpacing::Log10); + CHECK(s.GetQBinCount() == 300); + const auto back = Convert(s); + CHECK(back.getQBinning().getValue() == org::openapitools::server::model::Q_binning::eQ_binning::LOG10); + CHECK(!back.qSpacingIsSet()); + CHECK(back.getQBins() == 300); + + // Through a DiffractionExperiment, which is how the broker keeps them. + DiffractionExperiment x; + x.ImportAzimuthalIntegrationSettings(s); + CHECK(x.GetAzimuthalIntegrationSettings().GetQBinSpacing() == QBinSpacing::Log10); + CHECK(Convert(x.GetAzimuthalIntegrationSettings()).getQBins() == 300); + } + + SECTION("Refused combinations") { + CHECK_THROWS_WITH(Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "q_spacing": 0.01, "q_bins": 100})")), + Catch::Matchers::ContainsSubstring("not both")); + CHECK_THROWS_WITH(Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "q_binning": "log10"})")), + Catch::Matchers::ContainsSubstring("needs the number of Q bins")); + CHECK_THROWS_WITH(Convert(parse(R"({"solid_angle_corr": true, "polarization_corr": true, + "low_q_recipA": 0.1, "q_binning": "log10", + "q_spacing": 0.01, "q_bins": 100})")), + Catch::Matchers::ContainsSubstring("only to linear")); + } +}