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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-10 16:55:48 +01:00
1 parent bf1461acd5
commit d0a7d6a314
2 files changed
+88 -2

No files matched your search

+16 -2
View File
@@ -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<float>(input.getQSpacing()) : std::nullopt,
input.qBinsIsSet() ? std::optional<int32_t>(input.getQBins()) : std::nullopt);
ret.QRange_recipA(input.getLowQRecipA(),
input.highQRecipAIsSet() ? std::optional<float>(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;
+72
View File
@@ -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<int32_t>(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<int32_t>(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"));
}
}