Files
Jungfraujoch/tests/AzimuthalIntegrationTest.cpp
T
leonarski_fandClaude Opus 5 0f00b76a9a image analysis: the ice score takes the pipeline's own band width, and the ice quantities are named for what kind of number they are
Two things, both about telling one ice quantity from another.

The ice score's spot channel had its own band half-width of 0.02 A^-1 while the spot finder marks
ice rings at 0.03 (ice_ring_width_Q_recipA). The 0.02 was justified by a 5 pp specificity gain
measured on the PYTHON PROTOTYPE, which used a fitted beam centre and a mask-derived coverage table;
the shipped port, which takes the geometry's centre and the azimuthal profile's own live pixel
count, does not reproduce it. Measured over the corpus by truth class rather than by directory
label, at 0.02 vs 0.03 on the combined score: ice loops 62.13/62.19%, _icy protein 89.03/89.79%,
_clean protein 16.51/16.31%, water 17.19/20.03%. The widths are indistinguishable except on water,
where one of the four loops is independently known to carry a full hexagonal pattern. So the width
is now a parameter and the pipeline's own value is passed in - one band width, not two. The 0.012
tolerance in the radial channel is NOT a second band width, and is renamed CENTRE_SMEAR_Q to say so:
it is how far either side the channel looks for the bin a mis-set beam centre moved the ring to.

The rest is naming. Three kinds of number were all called score, or built from things called count,
and a reader could not tell from the name whether 1 meant "none" or "certain" - which are opposite.
The convention, now stated in docs/CPU_DATA_ANALYSIS.md: *_score is bounded [0,1] and 1 is
certainty, *_ratio is unbounded and 1 is nothing, *_count is a count. The C++ identifiers for the
ice ring ratio follow it (ice_ring_score -> ice_ring_ratio, GetIceRingScore -> GetIceRingRatio,
PlotType::IceRingScore -> IceRingRatio), and the local in the scaling gate that shadowed the new
ice_score while meaning the ring ratio is renamed with them.

Nothing outside the source moved: the CBOR keys ice_ring_score and ice_ring_score_mean, the datasets
/entry/MX/iceRingScore and iceRingScoreMean, the ice_ring_score plot type and the --ice-min-score
flag are all unchanged, and were checked to be after the rename. Renaming those changes stored
files, the stream format, the REST API and a CLI flag, and is a separate decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
2026-09-08 07:19:03 +02:00

597 lines
23 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_all.hpp>
#include "../common/AzimuthalIntegrationProfile.h"
#include "../common/AzimuthalIntegrationMapping.h"
TEST_CASE("AzimuthalIntegrationMapping_Constructor","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
REQUIRE(x.GetPixelsNum() == 2164*2068);
std::unique_ptr<AzimuthalIntegrationMapping> radial;
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 5);
PixelMask pixel_mask(x);
REQUIRE_NOTHROW(radial = std::make_unique<AzimuthalIntegrationMapping>(x, pixel_mask));
}
TEST_CASE("AzimuthalIntegrationMapping_GetBinNumber","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
REQUIRE(mapping.GetBinNumber() == 39);
}
TEST_CASE("AzimuthalIntegrationMapping_GetBinNumber_mask","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 9);
std::vector<uint32_t> pixel_mask(x.GetPixelsNum(), 0);
auto geom = x.GetDiffractionGeometry();
for (int row = 0; row < x.GetYPixelsNum(); row++) {
for (int col = 0; col < x.GetXPixelsNum(); col++) {
float d = geom.PxlToRes(col, row);
float q = 2 * M_PI / d;
if (q >= 3.1)
pixel_mask[row * x.GetXPixelsNum() + col] = 1;
}
}
PixelMask pixel_mask_obj(x);
pixel_mask_obj.LoadUserMask(x, pixel_mask);
AzimuthalIntegrationMapping mapping(x, pixel_mask_obj);
REQUIRE(mapping.GetBinNumber() == 89);
}
TEST_CASE("AzimuthalIntegrationMapping_GetBinNumber_DetectorLimit","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 9.9);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
REQUIRE(mapping.GetBinNumber() == 98);
}
TEST_CASE("AzimuthalIntegrationMapping_GetBinToQ","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
auto bin_to_q = mapping.GetBinToQ();
REQUIRE(bin_to_q.size() == 39);
CHECK(bin_to_q[0] == Catch::Approx(0.15));
CHECK(bin_to_q[1] == Catch::Approx(0.25));
CHECK(bin_to_q[15] == Catch::Approx(1.65));
CHECK(bin_to_q[38] == Catch::Approx(3.95));
}
TEST_CASE("AzimuthalIntegrationMapping_GetBinToPhi","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.1).QRange_recipA(0.1, 4.1).AzimuthalBinCount(4);
x.ImportAzimuthalIntegrationSettings(settings);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
auto &bin_to_q = mapping.GetBinToQ();
REQUIRE(bin_to_q.size() == 40 * 4);
CHECK(bin_to_q[0] == Catch::Approx(0.15));
CHECK(bin_to_q[1] == Catch::Approx(0.25));
CHECK(bin_to_q[15] == Catch::Approx(1.65));
CHECK(bin_to_q[38] == Catch::Approx(3.95));
auto &bin_to_phi = mapping.GetBinToPhi();
REQUIRE(bin_to_phi.size() == 40 * 4);
CHECK(bin_to_phi[0] == Catch::Approx(0));
CHECK(bin_to_phi[1] == Catch::Approx(0));
CHECK(bin_to_phi[38] == Catch::Approx(0));
CHECK(bin_to_phi[40] == Catch::Approx(90));
}
TEST_CASE("AzimuthalIntegrationMapping_GetBin","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.1).QRange_recipA(0.1, 4.1).AzimuthalBinCount(4);
CHECK(settings.GetBin(0.11f, 0.0f) == 0);
CHECK(settings.GetBin(0.11f, 95.0f) == 40);
CHECK(settings.GetBin(0.11f, 185.0f) == 80);
CHECK(settings.GetBin(4.02f, 280.0f) == 120 + 39);
}
#include "../preview/JFJochTIFF.h"
TEST_CASE("AzimuthalIntegrationMapping_GetMapping","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(100).BeamX_pxl(1000).BeamY_pxl(1000);
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.1).QRange_recipA(0.1, 4.1).AzimuthalBinCount(4);
x.ImportAzimuthalIntegrationSettings(settings);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
auto map = mapping.GetPixelToBin();
CompressedImage image(map, x.GetXPixelsNum(), x.GetYPixelsNum());
WriteTIFFToFile("test.tiff", image);
CHECK(map[x.GetXPixelsNum() * 500 + 1500] / 40 == 3);
CHECK(map[x.GetXPixelsNum() * 500 + 500] / 40 == 2);
CHECK(map[x.GetXPixelsNum() * 1500 + 500] / 40 == 1);
CHECK(map[x.GetXPixelsNum() * 1500 + 1500] / 40 == 0);
}
TEST_CASE("AzimuthalIntegrationMapping_QToBin","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
REQUIRE(mapping.QToBin(0.0) == 0);
REQUIRE(std::floor(mapping.QToBin(0.200001)) == 1);
REQUIRE(mapping.QToBin(0.6) == Catch::Approx(5));
REQUIRE(mapping.QToBin(50.0) == Catch::Approx(38));
}
TEST_CASE("AzimuthalIntegrationProfile","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile(mapping);
std::vector<float> sum(mapping.GetBinNumber()), sum2(mapping.GetBinNumber());
std::vector<uint32_t> count(mapping.GetBinNumber());
for (int i = 0; i < mapping.GetBinNumber(); i++) {
sum[i] = i * i * 4;
sum2[i] = i * i * i * i * 4;
count[i] = i;
}
REQUIRE_NOTHROW(profile.Add(sum, sum2, count));
REQUIRE_NOTHROW(profile.Add(sum, sum2, count));
std::vector<float> sum_wr(mapping.GetBinNumber() - 1);
REQUIRE_THROWS(profile.Add(sum_wr, sum2, count));
auto plot = profile.GetPlot();
REQUIRE(plot.GetPlots().size() == 1);
REQUIRE(plot.GetPlots()[0].x.size() == mapping.GetBinNumber());
REQUIRE(plot.GetPlots()[0].y.size() == mapping.GetBinNumber());
REQUIRE(plot.GetPlots()[0].x[0] == Catch::Approx(mapping.GetBinToQ()[0]));
REQUIRE(std::isnan(plot.GetPlots()[0].y[0]));
for (int i = 1; i < mapping.GetBinNumber(); i++) {
REQUIRE(plot.GetPlots()[0].x[i] == Catch::Approx(mapping.GetBinToQ()[i]));
REQUIRE(plot.GetPlots()[0].y[i] == Catch::Approx(i * 4));
}
}
TEST_CASE("AzimuthalIntegrationProfile_GetStd","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile(mapping);
REQUIRE(mapping.GetBinNumber() >= 4);
std::vector<float> sum(mapping.GetBinNumber()), sum2(mapping.GetBinNumber());
std::vector<uint32_t> count(mapping.GetBinNumber());
sum[0] = 2 + 3 + 4 + 5;
sum2[0] = 4 + 9 + 16 + 25;
count[0] = 4;
sum[1] = 1 + 1;
sum2[1] = 1 + 1;
count[1] = 2;
sum[2] = 1;
sum2[2] = 1;
count[2] = 1;
sum[3] = 0;
sum2[3] = 0;
count[3] = 0;
REQUIRE_NOTHROW(profile.Add(sum, sum2, count));
auto ret_mean = profile.GetResult();
auto ret_stddev = profile.GetStd();
auto ret_count = profile.GetPixelCount();
REQUIRE(ret_mean.size() == mapping.GetBinNumber());
REQUIRE(ret_stddev.size() == mapping.GetBinNumber());
REQUIRE(ret_count.size() == mapping.GetBinNumber());
CHECK(ret_mean[0] == Catch::Approx(3.5));
CHECK(ret_stddev[0] == Catch::Approx(std::sqrt(5.0/ 3.0)));
CHECK(ret_count[0] == 4);
CHECK(ret_mean[1] == Catch::Approx(1.0));
CHECK(ret_stddev[1] == 0.0f);
CHECK(ret_count[1] == 2);
CHECK(ret_mean[2] == Catch::Approx(1.0));
CHECK(std::isnan(ret_stddev[2]));
CHECK(ret_count[2] == 1);
CHECK(std::isnan(ret_mean[3]));
CHECK(std::isnan(ret_stddev[3]));
CHECK(ret_count[3] == 0);
}
TEST_CASE("AzimuthalIntegrationMapping_DimensionsMatchPixelToBin","[AzimuthalIntegration]") {
// The reported dimensions have to describe the image pixel_to_bin was built for, in both
// geometry modes - the adaptive spot finders walk the image with them and index pixel_to_bin.
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(80).BeamX_pxl(1030).BeamY_pxl(1080);
x.QSpacingForAzimInt_recipA(0.05).QRangeForAzimInt_recipA(0.05, 5.0);
PixelMask converted_mask(x);
AzimuthalIntegrationMapping converted(x, converted_mask);
CHECK(converted.GetWidth() * converted.GetHeight() == converted.GetPixelToBin().size());
x.GeometryTransformation(false);
PixelMask raw_mask(x);
AzimuthalIntegrationMapping raw(x, raw_mask);
CHECK(raw.GetWidth() * raw.GetHeight() == raw.GetPixelToBin().size());
}
TEST_CASE("AzimuthalIntegrationProfile_GetStd_AfterClear","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile(mapping);
std::vector<float> sum(mapping.GetBinNumber()), sum2(mapping.GetBinNumber());
std::vector<uint32_t> count(mapping.GetBinNumber());
sum[0] = 2 + 3 + 4 + 5;
sum2[0] = 4 + 9 + 16 + 25;
count[0] = 4;
// The same frame twice, with a Clear() in between: a profile is reused for every image of a
// dataset, so the second image has to give exactly the first one's standard deviation.
profile.Add(sum, sum2, count);
const auto first = profile.GetStd();
profile.Clear(mapping);
profile.Add(sum, sum2, count);
const auto second = profile.GetStd();
CHECK(first[0] == Catch::Approx(std::sqrt(5.0 / 3.0)));
CHECK(second[0] == Catch::Approx(first[0]));
}
TEST_CASE("AzimuthalIntegrationProfile_ClearToLargerMapping","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping small(x, pixel_mask);
// A wider q range - the viewer re-uses one profile across datasets, so every vector Clear()
// touches has to end up the size the new mapping asks for.
x.QRangeForAzimInt_recipA(0.1, 9);
AzimuthalIntegrationMapping large(x, pixel_mask);
REQUIRE(large.GetBinNumber() > small.GetBinNumber());
AzimuthalIntegrationProfile profile(small);
profile.Clear(large);
std::vector<float> sum(large.GetBinNumber()), sum2(large.GetBinNumber());
std::vector<uint32_t> count(large.GetBinNumber());
// A bin that exists only in the wider mapping.
const auto bin = small.GetBinNumber();
sum[bin] = 2 + 3 + 4 + 5;
sum2[bin] = 4 + 9 + 16 + 25;
count[bin] = 4;
profile.Add(sum, sum2, count);
const auto stddev = profile.GetStd();
REQUIRE(stddev.size() == large.GetBinNumber());
CHECK(stddev[bin] == Catch::Approx(std::sqrt(5.0 / 3.0)));
}
TEST_CASE("AzimuthalIntegrationProfile_operatorAdd","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile0(mapping), profile1(mapping);
std::vector<float> sum(mapping.GetBinNumber()), sum2(mapping.GetBinNumber());
std::vector<uint32_t> count(mapping.GetBinNumber());
for (int i = 0; i < mapping.GetBinNumber(); i++) {
sum[i] = (i + 1) * i * 4;
sum2[i] = (i+ 1) * i * 5;
count[i] = i + 1;
}
REQUIRE_NOTHROW(profile0.Add(sum, sum2, count));
REQUIRE_NOTHROW(profile1 += profile0);
auto plot = profile1.GetPlot();
REQUIRE(plot.GetPlots().size() == 1);
REQUIRE(plot.GetPlots()[0].x.size() == mapping.GetBinNumber());
REQUIRE(plot.GetPlots()[0].y.size() == mapping.GetBinNumber());
for (int i = 0; i < mapping.GetBinNumber(); i++) {
REQUIRE(plot.GetPlots()[0].x[i] == Catch::Approx(mapping.GetBinToQ()[i]));
REQUIRE(plot.GetPlots()[0].y[i] == Catch::Approx(i * 4));
}
}
TEST_CASE("AzimuthalIntegrationProfile_GetMeanValueOfBins","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 4);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile(mapping);
std::vector<float> sum(mapping.GetBinNumber());
std::vector<float> sum2(mapping.GetBinNumber());
std::vector<uint32_t> count(mapping.GetBinNumber());
for (int i = 0; i < mapping.GetBinNumber(); i++) {
sum[i] = i * i * 4;
sum2[i] = i * i * i * i * 4;
count[i] = i;
}
REQUIRE_NOTHROW(profile.Add(sum, sum2, count));
REQUIRE(profile.GetMeanValueOfBins(0,2) == Catch::Approx((sum[0] + sum[1] + sum[2]) / double(count[0] + count[1] + count[2])));
REQUIRE(profile.GetMeanValueOfBins(5,7) == Catch::Approx((sum[5] + sum[6] + sum[7]) / double (count[5] + count[6] + count[7])));
x.BkgEstimateQRange_recipA(0.7, 0.8);
REQUIRE(profile.GetBkgEstimate(x.GetAzimuthalIntegrationSettings()) == Catch::Approx((sum[5] + sum[6] + sum[7]) / double (count[5] + count[6] + count[7])));
x.BkgEstimateQRange_recipA(0.01, 0.345);
REQUIRE(profile.GetBkgEstimate(x.GetAzimuthalIntegrationSettings()) == Catch::Approx((sum[0] + sum[1] + sum[2]) / double(count[0] + count[1] + count[2])));
}
TEST_CASE("AzimuthalIntegrationProfile_GetResult1D","[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF4M());
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.1f).QRange_recipA(0.1f, 0.4f).AzimuthalBinCount(3);
x.ImportAzimuthalIntegrationSettings(settings);
PixelMask pixel_mask(x);
AzimuthalIntegrationMapping mapping(x, pixel_mask);
AzimuthalIntegrationProfile profile(mapping);
REQUIRE(mapping.GetQBinCount() == 3);
REQUIRE(mapping.GetAzimuthalBinCount() == 3);
REQUIRE(mapping.GetBinNumber() == 9);
std::vector<float> sum(mapping.GetBinNumber(), 0.0f);
std::vector<float> sum2(mapping.GetBinNumber(), 20.0f);
std::vector<uint32_t> count(mapping.GetBinNumber(), 0);
// Layout is [azimuth][q], flattened:
// az0: q0 q1 q2
// az1: q0 q1 q2
// az2: q0 q1 q2
//
// Choose values so the correct collapsed result is easy to verify:
// q0 -> (10 + 20 + 30) / 3 = 20
// q1 -> (11 + 21 + 31) / 3 = 21
// q2 -> (12 + 22 + 32) / 3 = 22
sum[0] = 10; count[0] = 1; // az0 q0
sum[1] = 11; count[1] = 1; // az0 q1
sum[2] = 12; count[2] = 1; // az0 q2
sum[3] = 20; count[3] = 1; // az1 q0
sum[4] = 21; count[4] = 1; // az1 q1
sum[5] = 22; count[5] = 1; // az1 q2
sum[6] = 30; count[6] = 1; // az2 q0
sum[7] = 31; count[7] = 1; // az2 q1
sum[8] = 32; count[8] = 1; // az2 q2
REQUIRE_NOTHROW(profile.Add(sum, sum2, count));
auto result_1d = profile.GetResult1D();
REQUIRE(result_1d.size() == 3);
CHECK(result_1d[0] == Catch::Approx(20.0f));
CHECK(result_1d[1] == Catch::Approx(21.0f));
CHECK(result_1d[2] == Catch::Approx(22.0f));
}
template <class T>
static void RequireVectorsEqual(const std::vector<T> &ref,
const std::vector<T> &other,
const std::string &name,
int nthreads) {
INFO(name << ", threads=" << nthreads);
REQUIRE(ref.size() == other.size());
CHECK(memcmp(ref.data(), other.data(), sizeof(T) * ref.size()) == 0);
}
static void CheckAzimuthalIntegrationMappingThreadingExact(const DiffractionExperiment &experiment) {
PixelMask pixel_mask(experiment);
AzimuthalIntegrationMapping mapping_1(experiment, pixel_mask, 1);
AzimuthalIntegrationMapping mapping_2(experiment, pixel_mask, 2);
AzimuthalIntegrationMapping mapping_16(experiment, pixel_mask, 16);
AzimuthalIntegrationMapping mapping_0(experiment, pixel_mask, 0);
REQUIRE(mapping_1.GetBinNumber() == mapping_2.GetBinNumber());
REQUIRE(mapping_1.GetBinNumber() == mapping_16.GetBinNumber());
REQUIRE(mapping_1.GetBinNumber() == mapping_0.GetBinNumber());
REQUIRE(mapping_1.GetQBinCount() == mapping_2.GetQBinCount());
REQUIRE(mapping_1.GetQBinCount() == mapping_16.GetQBinCount());
REQUIRE(mapping_1.GetQBinCount() == mapping_0.GetQBinCount());
REQUIRE(mapping_1.GetAzimuthalBinCount() == mapping_2.GetAzimuthalBinCount());
REQUIRE(mapping_1.GetAzimuthalBinCount() == mapping_16.GetAzimuthalBinCount());
REQUIRE(mapping_1.GetAzimuthalBinCount() == mapping_0.GetAzimuthalBinCount());
RequireVectorsEqual(mapping_1.GetPixelToBin(), mapping_2.GetPixelToBin(), "pixel_to_bin", 2);
RequireVectorsEqual(mapping_1.GetPixelToBin(), mapping_16.GetPixelToBin(), "pixel_to_bin", 16);
RequireVectorsEqual(mapping_1.GetPixelToBin(), mapping_0.GetPixelToBin(), "pixel_to_bin", 0);
RequireVectorsEqual(mapping_1.Resolution(), mapping_2.Resolution(), "resolution", 2);
RequireVectorsEqual(mapping_1.Resolution(), mapping_16.Resolution(), "resolution", 16);
RequireVectorsEqual(mapping_1.Resolution(), mapping_0.Resolution(), "resolution", 0);
RequireVectorsEqual(mapping_1.Corrections(), mapping_2.Corrections(), "corrections", 2);
RequireVectorsEqual(mapping_1.Corrections(), mapping_16.Corrections(), "corrections", 16);
RequireVectorsEqual(mapping_1.Corrections(), mapping_0.Corrections(), "corrections", 0);
RequireVectorsEqual(mapping_1.GetBinToQ(), mapping_2.GetBinToQ(), "bin_to_q", 2);
RequireVectorsEqual(mapping_1.GetBinToQ(), mapping_16.GetBinToQ(), "bin_to_q", 16);
RequireVectorsEqual(mapping_1.GetBinToQ(), mapping_0.GetBinToQ(), "bin_to_q", 0);
RequireVectorsEqual(mapping_1.GetBinToD(), mapping_2.GetBinToD(), "bin_to_d", 2);
RequireVectorsEqual(mapping_1.GetBinToD(), mapping_16.GetBinToD(), "bin_to_d", 16);
RequireVectorsEqual(mapping_1.GetBinToD(), mapping_0.GetBinToD(), "bin_to_d", 0);
RequireVectorsEqual(mapping_1.GetBinToTwoTheta(), mapping_2.GetBinToTwoTheta(), "bin_to_2theta", 2);
RequireVectorsEqual(mapping_1.GetBinToTwoTheta(), mapping_16.GetBinToTwoTheta(), "bin_to_2theta", 16);
RequireVectorsEqual(mapping_1.GetBinToTwoTheta(), mapping_0.GetBinToTwoTheta(), "bin_to_2theta", 0);
RequireVectorsEqual(mapping_1.GetBinToPhi(), mapping_2.GetBinToPhi(), "bin_to_phi", 2);
RequireVectorsEqual(mapping_1.GetBinToPhi(), mapping_16.GetBinToPhi(), "bin_to_phi", 16);
RequireVectorsEqual(mapping_1.GetBinToPhi(), mapping_0.GetBinToPhi(), "bin_to_phi", 0);
}
TEST_CASE("AzimuthalIntegrationMapping_Threading_FixedGeometry_2000x2000", "[AzimuthalIntegration]") {
DiffractionExperiment x(DetDECTRIS(2000, 2000, "E16M", ""));
x.DetectorDistance_mm(50).BeamX_pxl(1000).BeamY_pxl(1000);
x.QSpacingForAzimInt_recipA(0.1).QRangeForAzimInt_recipA(0.1, 10);
x.PolarizationFactor(0.99f);
REQUIRE(x.IsGeometryTransformed());
CheckAzimuthalIntegrationMappingThreadingExact(x);
}
TEST_CASE("AzimuthalIntegrationMapping_Threading_RawGeometry_18Modules", "[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF9M());
x.Raw();
x.DetectorDistance_mm(100).BeamX_pxl(1500).BeamY_pxl(1500);
x.QSpacingForAzimInt_recipA(0.05).QRangeForAzimInt_recipA(0.1, 8.0);
x.PolarizationFactor(0.99f);
REQUIRE(!x.IsGeometryTransformed());
REQUIRE(x.GetModulesNum() == 18);
CheckAzimuthalIntegrationMappingThreadingExact(x);
}
TEST_CASE("AzimuthalIntegrationMapping_Threading_ConvertedGeometry_18Modules", "[AzimuthalIntegration]") {
DiffractionExperiment x(DetJF9M());
x.Conversion();
x.DetectorDistance_mm(100).BeamX_pxl(1500).BeamY_pxl(1500);
x.QSpacingForAzimInt_recipA(0.05).QRangeForAzimInt_recipA(0.1, 8.0);
x.PolarizationFactor(0.99f);
REQUIRE(x.IsGeometryTransformed());
REQUIRE(x.GetModulesNum() == 18);
CheckAzimuthalIntegrationMappingThreadingExact(x);
}
// The ice ring ratio on an explicit profile: flat means no ice (1.0), and a bump planted on a hexagonal
// ring is reported at its own height over the background. Uses the static entry point, which is what
// feeds the score the peak-excluded per-ring background instead of the plain profile.
TEST_CASE("AzimuthalIntegrationProfile_IceRingRatio","[AzimuthalIntegration]") {
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.01f).QRange_recipA(0.1f, 4.5f);
const int q_bins = settings.GetQBinCount();
REQUIRE(q_bins > 400);
auto bin_of = [&](float d_A) {
const float q = 6.283185307f / d_A;
return static_cast<int>(std::lround((q - settings.GetLowQ_recipA()) / settings.GetQSpacing_recipA() - 0.5f));
};
std::vector<float> profile(q_bins, 100.0f);
CHECK(AzimuthalIntegrationProfile::IceRingRatio(profile, q_bins, settings, 0.03f) == Catch::Approx(1.0f));
// A bump at the 2.249 A hexagonal ring, on a background the running median still reads as 100.
profile[bin_of(2.249f)] = 250.0f;
CHECK(AzimuthalIntegrationProfile::IceRingRatio(profile, q_bins, settings, 0.03f) == Catch::Approx(2.5f));
// A bump of the same size well away from every ring is not ice and must not be reported.
std::vector<float> off_ring(q_bins, 100.0f);
off_ring[bin_of(2.500f)] = 250.0f;
CHECK(AzimuthalIntegrationProfile::IceRingRatio(off_ring, q_bins, settings, 0.03f) == Catch::Approx(1.0f));
}
// A profile given per (q, azimuth) bin is averaged over azimuth first, so a ring seen in every sector
// scores the same as the equivalent 1-D profile.
TEST_CASE("AzimuthalIntegrationProfile_IceRingRatio_Azimuthal","[AzimuthalIntegration]") {
AzimuthalIntegrationSettings settings;
settings.QSpacing_recipA(0.01f).QRange_recipA(0.1f, 4.5f).AzimuthalBinCount(4);
const int q_bins = settings.GetQBinCount();
const float q = 6.283185307f / 2.249f;
const int ring = static_cast<int>(std::lround((q - settings.GetLowQ_recipA()) / settings.GetQSpacing_recipA() - 0.5f));
std::vector<float> flat(q_bins, 100.0f);
std::vector<float> sectors(static_cast<size_t>(q_bins) * 4, 100.0f);
flat[ring] = 250.0f;
for (int az = 0; az < 4; az++)
sectors[static_cast<size_t>(az) * q_bins + ring] = 250.0f;
CHECK(AzimuthalIntegrationProfile::IceRingRatio(sectors, q_bins, settings, 0.03f)
== Catch::Approx(AzimuthalIntegrationProfile::IceRingRatio(flat, q_bins, settings, 0.03f)));
}