Azimuthal integration: review follow-ups for the Q binning

The low-Q headroom check and the detector-edge high Q are floored by the linear
spacing only where one was given, and by the smallest allowed Q otherwise, instead
of by a spacing log10 bins do not use. GetQSpacing_recipA says that under log10 it
means nothing about the bins. GetQBinEdges_recipA, which only the tests used, is
dropped: the tests state the edges from the documented formula. A test pins the
far-edge clamp of linear-by-spacing bins on a two-row setting. The broker docs note
that a GET carries q_bins and no q_spacing once bins are configured.

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 05daa46553
commit 8d1157f711
4 files changed
+31 -27

No files matched your search

+3 -13
View File
@@ -26,7 +26,7 @@ AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::QRange_recipA(float
// The low limit has to leave room for at least one bin below maxQ. This used to follow from the
// high limit being mandatory (high <= maxQ and high > low); once "unset" became legal, ResolveHighQ
// was left computing std::clamp(q, low + spacing, maxQ) with the lower bound above the upper one.
check_max("Low Q for azimuthal integration", low, maxQ_recipA - q_spacing);
check_max("Low Q for azimuthal integration", low, maxQ_recipA - GetRequestedQSpacing_recipA().value_or(minQ_recipA));
if (high.has_value()) {
check_finite("High Q for azimuthal integration", *high);
check_max("High Q for azimuthal integration", *high, maxQ_recipA);
@@ -47,7 +47,8 @@ void AzimuthalIntegrationSettings::ResolveHighQ(float detector_max_q_recipA) {
if (requested_high_q_recipA.has_value())
return;
high_q_recipA = std::clamp(detector_max_q_recipA, low_q_recipA + q_spacing, maxQ_recipA);
high_q_recipA = std::clamp(detector_max_q_recipA, low_q_recipA + GetRequestedQSpacing_recipA().value_or(minQ_recipA),
maxQ_recipA);
UpdateBinCount();
}
@@ -127,17 +128,6 @@ std::vector<double> AzimuthalIntegrationSettings::GetQBinCentres_recipA() const
return ret;
}
std::vector<double> AzimuthalIntegrationSettings::GetQBinEdges_recipA() const {
std::vector<double> ret(q_bins + 1);
for (int32_t i = 0; i <= q_bins; i++) {
if (q_bin_spacing == QBinSpacing::Linear)
ret[i] = bin_step * static_cast<double>(i) + low_q_recipA;
else
ret[i] = std::pow(10.0, std::log10(static_cast<double>(low_q_recipA)) + bin_step * static_cast<double>(i));
}
return ret;
}
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::BkgEstimateQRange_recipA(float low, float high) {
check_finite("Low Q for background estimation", low);
check_finite("High Q for background estimation", high);
+3 -3
View File
@@ -68,15 +68,15 @@ public:
[[nodiscard]] float GetHighQ_recipA() const;
[[nodiscard]] std::optional<float> GetRequestedHighQ_recipA() const;
[[nodiscard]] float GetLowQ_recipA() const;
// The width of a linear bin (as given, or as the bin count makes it).
// The width of a linear bin (as given, or as the bin count makes it). Under log10 binning it is the
// last linear spacing and means nothing about the bins - use GetQBinCentres_recipA.
[[nodiscard]] float GetQSpacing_recipA() const;
[[nodiscard]] QBinSpacing GetQBinSpacing() const;
// The spacing and the bin count as they were given; one of them is unset.
[[nodiscard]] std::optional<float> GetRequestedQSpacing_recipA() const;
[[nodiscard]] std::optional<int32_t> GetRequestedQBinCount() const;
// Centre of every Q bin (mid-point of its edges on the bin scale), and its GetQBinCount() + 1 edges.
// Centre of every Q bin: the mid-point of its edges on the bin scale.
[[nodiscard]] std::vector<double> GetQBinCentres_recipA() const;
[[nodiscard]] std::vector<double> GetQBinEdges_recipA() const;
[[nodiscard]] int32_t GetBinCount() const;
[[nodiscard]] int32_t GetQBinCount() const;
+2
View File
@@ -179,6 +179,8 @@ setting (`/config/dataset`, `polarization_factor`), not part of `azim_int`. The
are linear by default, set by `q_spacing` (0.01 Å⁻¹ when nothing is given) or by their number,
`q_bins` - one or the other, not both. `"q_binning": "log10"` lays them out in equal steps of
log10 Q instead (fine at low Q, coarse at high Q); it takes `q_bins` and refuses `q_spacing`.
A GET returns the one that was set, so once `q_bins` (or log10) is configured the reply carries
`q_bins` and no `q_spacing`, which a client generated before `q_spacing` became optional cannot parse.
## Following a collection
+23 -11
View File
@@ -678,8 +678,10 @@ TEST_CASE("AzimuthalIntegrationSettings_QBinning_Validation","[AzimuthalIntegrat
CHECK(s.GetQBinCount() == 50);
}
// The linear spacing bins exactly as it always has: the same count, the same bin for every q and the
// same centres, compared bit for bit with the formulas it used before log10 binning was added.
// The linear spacing bins as it always has: the same count, the same bin for every q and the same
// centres, compared bit for bit with the formulas it used before log10 binning was added. The one
// exception, which this uniform sample of q cannot reach, is a q that rounds onto the far edge of the
// last bin: it now stays in that bin (AzimuthalIntegrationSettings_Linear_FarEdge).
TEST_CASE("AzimuthalIntegrationSettings_Linear_Unchanged","[AzimuthalIntegration]") {
const float low = 0.1f, high = 4.5f, spacing = 0.01f;
AzimuthalIntegrationSettings s;
@@ -704,14 +706,22 @@ TEST_CASE("AzimuthalIntegrationSettings_Linear_Unchanged","[AzimuthalIntegration
}
}
// A q just below high Q whose (q - low) / spacing rounds up to the bin count used to spill into the
// first bin of the next azimuthal row.
TEST_CASE("AzimuthalIntegrationSettings_Linear_FarEdge","[AzimuthalIntegration]") {
const float low = 0.1f, high = 3.7f, spacing = 0.1f;
AzimuthalIntegrationSettings s;
s.QSpacing_recipA(spacing).QRange_recipA(low, high).AzimuthalBinCount(2);
const float q = std::nextafter(high, 0.0f);
REQUIRE(static_cast<int32_t>(std::floor((q - low) / spacing)) == s.GetQBinCount());
CHECK(s.GetBin(q, 350.0f) == 2 * s.GetQBinCount() - 1);
}
TEST_CASE("AzimuthalIntegrationSettings_Linear_BinCount","[AzimuthalIntegration]") {
AzimuthalIntegrationSettings s;
s.QRange_recipA(0.1f, 4.1f).QBinning(QBinSpacing::Linear, std::nullopt, 40);
CHECK(s.GetQSpacing_recipA() == Catch::Approx(0.1));
const auto edges = s.GetQBinEdges_recipA();
REQUIRE(edges.size() == 41);
CHECK(edges.front() == Catch::Approx(0.1));
CHECK(edges.back() == Catch::Approx(4.1));
const auto centres = s.GetQBinCentres_recipA();
CHECK(centres[0] == Catch::Approx(0.15));
CHECK(centres[39] == Catch::Approx(4.05));
@@ -730,12 +740,12 @@ TEST_CASE("AzimuthalIntegrationSettings_Log10","[AzimuthalIntegration]") {
REQUIRE(s.GetQBinCount() == n);
REQUIRE(s.GetBinCount() == 2 * n);
const auto edges = s.GetQBinEdges_recipA();
// The edges as the docs state them: q_i = low (high / low)^(i / n).
std::vector<double> edges(n + 1);
for (int i = 0; i <= n; i++)
edges[i] = low * std::pow(static_cast<double>(high) / low, static_cast<double>(i) / n);
const auto centres = s.GetQBinCentres_recipA();
REQUIRE(edges.size() == n + 1);
REQUIRE(centres.size() == n);
CHECK(edges.front() == Catch::Approx(low));
CHECK(edges.back() == Catch::Approx(high));
// Equal steps in log10(q): three decades in 30 bins, every edge 10^0.1 times the one before.
for (int i = 0; i < n; i++) {
CHECK(edges[i + 1] > edges[i]);
@@ -779,7 +789,9 @@ TEST_CASE("AzimuthalIntegrationMapping_Log10","[AzimuthalIntegration]") {
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
const auto &s = mapping.Settings();
const auto edges = s.GetQBinEdges_recipA();
std::vector<double> edges(101);
for (int i = 0; i <= 100; i++)
edges[i] = 0.01 * std::pow(2.0 / 0.01, i / 100.0);
const auto centres = s.GetQBinCentres_recipA();
REQUIRE(mapping.GetQBinCount() == 100);