Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast. * Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU). * Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors. * Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer. Reviewed-on: #90 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
543 lines
28 KiB
C++
543 lines
28 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <vector>
|
|
|
|
#include "../common/BraggIntegrationSettings.h"
|
|
#include "../common/DetectorSetup.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../common/JFJochMath.h"
|
|
#include "../common/Reflection.h"
|
|
#include "../image_analysis/bragg_integration/BraggIntegrationEngineCPU.h"
|
|
#include "../image_analysis/image_preprocessing/ImagePreprocessorBuffer.h"
|
|
|
|
// One spot on a flat background, inside a grid of predictions 8 px apart that put no flux on the
|
|
// frame - the tails of reflections recorded on the frames either side of a finely sliced one. Their
|
|
// r2 regions cover every background ring, so whether the spot keeps a clean ring depends only on
|
|
// whether those predictions are allowed to mask it.
|
|
TEST_CASE("BraggIntegrationEngineCPU_NeighbourMaskFollowsPartiality", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings());
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
const float cx = 600.3f, cy = 300.2f, amp = 800.0f, sigma = 1.3f;
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = 12;
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx) {
|
|
const int x = static_cast<int>(std::lround(cx)) + dx, y = static_cast<int>(std::lround(cy)) + dy;
|
|
const float ex = x - cx, ey = y - cy;
|
|
image[y * width + x] += static_cast<int32_t>(std::lround(amp * std::exp(-(ex * ex + ey * ey) / (2 * sigma * sigma))));
|
|
}
|
|
|
|
auto run = [&](float neighbour_partiality, BraggIntegrationCounts &counts) {
|
|
std::vector<Reflection> predicted;
|
|
for (int gy = -4; gy <= 4; ++gy)
|
|
for (int gx = -4; gx <= 4; ++gx) {
|
|
Reflection r{};
|
|
r.h = gx; r.k = gy; r.l = 1;
|
|
r.predicted_x = cx + 8.0f * gx;
|
|
r.predicted_y = cy + 8.0f * gy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = (gx == 0 && gy == 0) ? 0.5f : neighbour_partiality;
|
|
predicted.push_back(r);
|
|
}
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
const auto out = engine.Run(image, predicted, predicted.size(), 0);
|
|
counts = engine.Counts();
|
|
std::vector<Reflection> spot;
|
|
for (const auto &r : out)
|
|
if (r.h == 0 && r.k == 0)
|
|
spot.push_back(r);
|
|
return spot;
|
|
};
|
|
|
|
// Tails: no reflection is dropped, and the spot is measured against the clean flat background -
|
|
// while the density the widened-radius guard reads still sees every prediction.
|
|
BraggIntegrationCounts tails;
|
|
const auto spot = run(0.01f, tails);
|
|
CHECK(tails.bkg_starved == 0);
|
|
CHECK(tails.bkg_starved_by_neighbour > 0);
|
|
REQUIRE(spot.size() == 1);
|
|
CHECK(spot[0].bkg == Catch::Approx(12.0f));
|
|
CHECK(spot[0].I == Catch::Approx(6.2831853 * sigma * sigma * amp).epsilon(0.05));
|
|
|
|
// The same predictions with their flux on this frame do take the rings, the spot's among them, and
|
|
// it is the neighbours, not the detector, that starve them - the same rings the density counted.
|
|
BraggIntegrationCounts on_frame;
|
|
CHECK(run(1.0f, on_frame).empty());
|
|
CHECK(on_frame.bkg_starved > 0);
|
|
CHECK(on_frame.bkg_starved_by_neighbour == on_frame.bkg_starved);
|
|
CHECK(on_frame.bkg_starved_by_neighbour == tails.bkg_starved_by_neighbour);
|
|
}
|
|
|
|
// The neighbour count taken without an image (CountNeighbourCrowding) is the one the integrator
|
|
// takes while integrating, on a frame whose pixels are all readable, whatever the partialities.
|
|
TEST_CASE("BraggIntegrationEngineCPU_CrowdingCountedWithoutImage", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings().R1(5.0f).R2(7.0f).R3(13.0f));
|
|
const size_t npixel = experiment.GetPixelsNum();
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = 12;
|
|
|
|
for (float spacing : {7.0f, 11.0f, 16.0f}) {
|
|
std::vector<Reflection> predicted;
|
|
for (int gy = -6; gy <= 6; ++gy)
|
|
for (int gx = -6; gx <= 6; ++gx) {
|
|
Reflection r{};
|
|
r.h = gx; r.k = gy; r.l = 1;
|
|
r.predicted_x = 600.3f + spacing * gx + 0.37f * gy;
|
|
r.predicted_y = 300.2f + spacing * gy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = (gx + gy) % 3 == 0 ? 0.01f : 0.8f;
|
|
predicted.push_back(r);
|
|
}
|
|
BraggIntegrationEngineCPU integrated(experiment, PixelMask(experiment));
|
|
integrated.Run(image, predicted, predicted.size(), 0);
|
|
BraggIntegrationEngineCPU counted(experiment, PixelMask(experiment));
|
|
counted.CountNeighbourCrowding(predicted, predicted.size());
|
|
CHECK(counted.Counts().predicted == integrated.Counts().predicted);
|
|
CHECK(counted.Counts().bkg_starved_by_neighbour == integrated.Counts().bkg_starved_by_neighbour);
|
|
if (spacing == 7.0f)
|
|
CHECK(counted.Counts().bkg_starved_by_neighbour > 0);
|
|
}
|
|
}
|
|
|
|
// A saturated pixel (INT32_MAX, as the preprocessor marks it) at the peak of a spot on rotation data: the
|
|
// reflection is not measured but kept, flagged overloaded, so that a rotation merge can drop its whole
|
|
// rocking event. The same spot without the saturated pixel is an ordinary measurement.
|
|
TEST_CASE("BraggIntegrationEngineCPU_SaturatedPeakIsFlaggedNotDropped", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f)
|
|
.Goniometer(GoniometerAxis("omega", 0.0f, 0.1f, Coord(-1, 0, 0), {}));
|
|
experiment.ImportIndexingSettings(IndexingSettings().RotationIndexing(true));
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings());
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
const int cx = 600, cy = 300;
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = 12;
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx)
|
|
image[(cy + dy) * width + cx + dx] += static_cast<int32_t>(std::lround(800.0 * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
|
|
|
|
Reflection r{};
|
|
r.h = 1; r.k = 2; r.l = 3;
|
|
r.predicted_x = cx; r.predicted_y = cy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
const auto clean = engine.Run(image, predicted, 1, 0);
|
|
REQUIRE(clean.size() == 1);
|
|
CHECK_FALSE(clean[0].overloaded);
|
|
|
|
image[cy * width + cx] = INT32_MAX;
|
|
const auto saturated = engine.Run(image, predicted, 1, 0);
|
|
REQUIRE(saturated.size() == 1);
|
|
CHECK(saturated[0].overloaded);
|
|
CHECK(saturated[0].clipped);
|
|
}
|
|
|
|
// An empty aperture on a background that is a curved function of the radius alone: the flat ring mean
|
|
// reads it wrong by the curvature, and the radial correction with the curve taken from a per-ring
|
|
// profile of the whole frame (as the spot finder hands it over on stills) takes that back out.
|
|
TEST_CASE("BraggIntegrationEngineCPU_RadialCurveFromRings", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
const float bx = 400.0f, by = 400.0f;
|
|
const auto background = [](double r) { return 50.0 + 0.05 * (r - 300.0) * (r - 300.0); };
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = static_cast<int32_t>(std::lround(
|
|
background(std::hypot(static_cast<double>(i % width) - bx, static_cast<double>(i / width) - by))));
|
|
|
|
// The profile per q bin, as the spot finder's rings would hold it.
|
|
const float low_q = 0.05f, dq = 0.01f;
|
|
const double F_px = 100.0 / 0.075, lambda = 1.0;
|
|
std::vector<float> rings(500);
|
|
for (size_t i = 0; i < rings.size(); ++i) {
|
|
const double q = low_q + (i + 0.5) * dq;
|
|
rings[i] = static_cast<float>(background(F_px * std::tan(2.0 * std::asin(q * lambda / (4.0 * PI)))));
|
|
}
|
|
rings[100] = NAN; // a ring with no value is bridged
|
|
|
|
Reflection r{};
|
|
r.h = 1; r.k = 0; r.l = 0;
|
|
r.predicted_x = bx + 300.0f;
|
|
r.predicted_y = by;
|
|
r.d = static_cast<float>(lambda / (2.0 * std::sin(0.5 * std::atan(300.0 / F_px)))); // where the curve has it
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
|
|
auto integrate = [&](bool correct) {
|
|
experiment.ImportBraggIntegrationSettings(
|
|
BraggIntegrationSettings().Integrator(IntegratorMode::BoxSum).BackgroundRadialCorrection(correct));
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
engine.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), low_q, dq);
|
|
REQUIRE(!engine.GetRadialBackground().empty());
|
|
const auto out = engine.Run(image, predicted, predicted.size(), 0);
|
|
REQUIRE(out.size() == 1);
|
|
return out[0];
|
|
};
|
|
// The curvature is along the radius only, so the error is n_inner x 0.05 x (<u^2>_ring - <u^2>_disk)
|
|
// with u the radial offset: 49 px x 0.05 x ((6^2 + 13^2) / 4 - 4^2 / 4) = 116 counts read too low.
|
|
const Reflection plain = integrate(false), corrected = integrate(true);
|
|
CHECK(plain.I < -80.0f);
|
|
CHECK(std::fabs(corrected.I) < 5.0f);
|
|
// The non-signal variance follows the corrected background under the signal disk (45 pixels at
|
|
// r1 = 4); the ring's own estimate noise is the same either way.
|
|
CHECK(corrected.var_bkg - plain.var_bkg == Catch::Approx(45.0 * (corrected.bkg - plain.bkg)).epsilon(1e-4));
|
|
|
|
// An empty profile hands the curve back to the reflections' own rings.
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
engine.RadialBackgroundFromRings({}, 500, low_q, dq);
|
|
CHECK(engine.GetRadialBackground().empty());
|
|
}
|
|
|
|
// A ring on a background falling steeply away from the beam: the flat high-side clip cuts the beam-side
|
|
// half of the ring and the background reads low. With the correction on, the clip follows the frame's
|
|
// radial curve (RadialClipOffset) and the empty aperture reads zero.
|
|
TEST_CASE("BraggIntegrationEngineCPU_ClipFollowsRadialCurve", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
const float bx = 400.0f, by = 400.0f;
|
|
const auto background = [](double r) { return 40000.0 - 100.0 * r; };
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = static_cast<int32_t>(std::lround(std::max(0.0,
|
|
background(std::hypot(static_cast<double>(i % width) - bx, static_cast<double>(i / width) - by)))));
|
|
const float low_q = 0.05f, dq = 0.01f;
|
|
const double F_px = 100.0 / 0.075, lambda = 1.0;
|
|
std::vector<float> rings(500);
|
|
for (size_t i = 0; i < rings.size(); ++i) {
|
|
const double q = low_q + (i + 0.5) * dq;
|
|
rings[i] = static_cast<float>(background(F_px * std::tan(2.0 * std::asin(q * lambda / (4.0 * PI)))));
|
|
}
|
|
Reflection r{};
|
|
r.h = 1; r.k = 0; r.l = 0;
|
|
r.predicted_x = bx + 200.0f;
|
|
r.predicted_y = by;
|
|
r.d = static_cast<float>(lambda / (2.0 * std::sin(0.5 * std::atan(200.0 / F_px))));
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
auto background_read = [&](bool correct) {
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings().BackgroundRadialCorrection(correct));
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
engine.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), low_q, dq);
|
|
const auto out = engine.Run(image, predicted, predicted.size(), 0);
|
|
REQUIRE(out.size() == 1);
|
|
return out[0].bkg;
|
|
};
|
|
// The kernel models the radial offset only; the ring's tangential reach adds the circle's own
|
|
// curvature, slope * <v^2> / (2 r) = 100 * 51 / 400 = 13 counts, which it leaves.
|
|
const double truth = background(200.0);
|
|
CHECK(background_read(false) < truth - 100.0);
|
|
CHECK(std::fabs(background_read(true) - truth) < 20.0);
|
|
}
|
|
|
|
// An empty aperture next to the edge of the beam-stop shadow: the background rises towards the beam until
|
|
// the shadow cuts it off, and the shadowed pixels are masked, so the ring has lost its beam-side part. The
|
|
// correction is taken over the ring pixels actually read (BraggCurveCorrection) and recovers the
|
|
// background under the disk (51.7 against 51.5; the plain ring reads 54.3). An average of the curve over
|
|
// the whole annulus counts the curve inside the shadow, where the ring has no pixels, and on the same
|
|
// edge without the tilt moved the background the wrong way (57 against 50.4). The detector is tilted, so
|
|
// the curve has to be read at the reflection's own 2 theta (CurveRadius): the PONI is 17 px from where
|
|
// the beam lands, and read at the distance from it the reflection sits on the flat part of the curve and
|
|
// nothing is corrected (54.3).
|
|
TEST_CASE("BraggIntegrationEngineCPU_RadialCurveOverThePixelsRead", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f)
|
|
.PoniRot1_rad(0.01f);
|
|
const DiffractionGeometry geom = experiment.GetDiffractionGeometry();
|
|
const double F_px = 100.0 / 0.075, lambda = 1.0;
|
|
// The radius the curve is tabulated against, F tan(2 theta), of a pixel.
|
|
const auto radius = [&](double x, double y) {
|
|
return F_px * std::tan(2.0 * std::asin(lambda / (2.0 * geom.PxlToRes(static_cast<float>(x), static_cast<float>(y)))));
|
|
};
|
|
const double edge = 296.0; // the shadow's edge
|
|
const auto background = [](double r) { return 50.0 + 4.0 * std::max(0.0, 302.0 - r); };
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i) {
|
|
const double r = radius(static_cast<double>(i % width), static_cast<double>(i / width));
|
|
image[i] = r < edge ? INT32_MIN : static_cast<int32_t>(std::lround(background(r)));
|
|
}
|
|
// The rings inside the shadow hold what is left there, little; fine enough that the edge is sharp.
|
|
const float low_q = 0.05f, dq = 0.0002f;
|
|
std::vector<float> rings(10000);
|
|
for (size_t i = 0; i < rings.size(); ++i) {
|
|
const double q = low_q + (i + 0.5) * dq;
|
|
const double r = F_px * std::tan(2.0 * std::asin(q * lambda / (4.0 * PI)));
|
|
rings[i] = static_cast<float>(r < edge ? 10.0 : background(r));
|
|
}
|
|
// 304 px out from where the beam lands, on the side away from the PONI.
|
|
const auto [dbx, dby] = geom.GetDirectBeam_pxl();
|
|
REQUIRE(std::fabs(dbx - 400.0f) > 10.0f);
|
|
Reflection r{};
|
|
r.h = 1; r.k = 0; r.l = 0;
|
|
r.predicted_x = std::round(dbx + (dbx > 400.0f ? 304.0f : -304.0f));
|
|
r.predicted_y = std::round(dby);
|
|
r.d = geom.PxlToRes(r.predicted_x, r.predicted_y);
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
auto integrate = [&](bool correct) {
|
|
experiment.ImportBraggIntegrationSettings(
|
|
BraggIntegrationSettings().Integrator(IntegratorMode::BoxSum).BackgroundRadialCorrection(correct));
|
|
BraggIntegrationEngineCPU engine(experiment, PixelMask(experiment));
|
|
engine.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), low_q, dq);
|
|
const auto out = engine.Run(image, predicted, predicted.size(), 0);
|
|
REQUIRE(out.size() == 1);
|
|
return out[0];
|
|
};
|
|
// The background under the disk, from the image itself.
|
|
const float r1 = experiment.GetBraggIntegrationSettings().GetR1();
|
|
double truth = 0.0;
|
|
int n = 0;
|
|
for (int dy = -8; dy <= 8; ++dy)
|
|
for (int dx = -8; dx <= 8; ++dx)
|
|
if (dx * dx + dy * dy < r1 * r1) {
|
|
truth += image[static_cast<size_t>(r.predicted_y + dy) * width + static_cast<size_t>(r.predicted_x + dx)];
|
|
++n;
|
|
}
|
|
truth /= n;
|
|
const Reflection plain = integrate(false), corrected = integrate(true);
|
|
CHECK(plain.bkg > truth + 2.0);
|
|
CHECK(corrected.bkg == Catch::Approx(truth).margin(0.5));
|
|
}
|
|
|
|
// A still has no rocking event to drop, so an overloaded observation is kept as a lower bound: the box sum
|
|
// with every saturated pixel at the saturation limit. A spot with its core clipped at the limit, the
|
|
// clipped pixels marked saturated as the preprocessor marks them: the still gives exactly the clipped
|
|
// sum, unflagged and clipped (it may be low), and below the unclipped spot; rotation data keep the flag.
|
|
TEST_CASE("BraggIntegrationEngineCPU_StillKeepsOverloadAsLowerBound", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings());
|
|
const int32_t limit = 500, bkg = 12;
|
|
experiment.Detector().SaturationLimit(limit);
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
const int cx = 600, cy = 300;
|
|
const float r1 = experiment.GetBraggIntegrationSettings().GetR1();
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = bkg;
|
|
double clipped_sum = 0.0, true_sum = 0.0;
|
|
int n_saturated = 0;
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx) {
|
|
const int32_t v = bkg + static_cast<int32_t>(std::lround(2000.0 * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
|
|
image[(cy + dy) * width + cx + dx] = v >= limit ? INT32_MAX : v;
|
|
if (dx * dx + dy * dy < r1 * r1) {
|
|
clipped_sum += std::min(v, limit) - bkg;
|
|
true_sum += v - bkg;
|
|
n_saturated += v >= limit;
|
|
}
|
|
}
|
|
REQUIRE(n_saturated > 1);
|
|
|
|
Reflection r{};
|
|
r.h = 1; r.k = 2; r.l = 3;
|
|
r.predicted_x = cx; r.predicted_y = cy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
|
|
BraggIntegrationEngineCPU still(experiment, PixelMask(experiment));
|
|
const auto kept = still.Run(image, predicted, 1, 0);
|
|
REQUIRE(kept.size() == 1);
|
|
CHECK_FALSE(kept[0].overloaded);
|
|
CHECK(kept[0].clipped);
|
|
CHECK(kept[0].I == Catch::Approx(clipped_sum).epsilon(0.01));
|
|
CHECK(kept[0].I < 0.8 * true_sum);
|
|
CHECK(kept[0].sigma >= std::sqrt(kept[0].I));
|
|
CHECK(still.Counts().overloads_kept == 1);
|
|
|
|
experiment.Goniometer(GoniometerAxis("omega", 0.0f, 0.1f, Coord(-1, 0, 0), {}))
|
|
.ImportIndexingSettings(IndexingSettings().RotationIndexing(true));
|
|
BraggIntegrationEngineCPU rotation(experiment, PixelMask(experiment));
|
|
const auto flagged = rotation.Run(image, predicted, 1, 0);
|
|
REQUIRE(flagged.size() == 1);
|
|
CHECK(flagged[0].overloaded);
|
|
CHECK(rotation.Counts().overloads_kept == 0);
|
|
}
|
|
|
|
// A still keeps an overloaded observation as a lower bound only where it has pixels at the saturation
|
|
// limit. One overloaded by a pixel unreadable on this frame alone has nothing to count at the limit:
|
|
// it stays flagged and is dropped.
|
|
TEST_CASE("BraggIntegrationEngineCPU_StillKeepsOnlySaturatedOverloads", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings());
|
|
const int32_t limit = 500, bkg = 12;
|
|
experiment.Detector().SaturationLimit(limit);
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = bkg;
|
|
auto spot = [&](int cx, int cy, double amplitude) {
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx) {
|
|
const int32_t v = bkg + static_cast<int32_t>(std::lround(amplitude * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
|
|
image[(cy + dy) * width + cx + dx] = v >= limit ? INT32_MAX : v;
|
|
}
|
|
};
|
|
spot(600, 300, 2000.0); // clipped: a plateau of saturated pixels in a bright spot
|
|
spot(300, 300, 40.0);
|
|
image[300 * width + 300] = INT32_MIN; // its peak unreadable on this frame, not in the mask
|
|
|
|
std::vector<Reflection> predicted;
|
|
for (const auto &[x, y] : {std::pair{600, 300}, std::pair{300, 300}}) {
|
|
Reflection r{};
|
|
r.h = x; r.k = y; r.l = 1;
|
|
r.predicted_x = x; r.predicted_y = y;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
predicted.push_back(r);
|
|
}
|
|
|
|
BraggIntegrationEngineCPU still(experiment, PixelMask(experiment));
|
|
const auto out = still.Run(image, predicted, predicted.size(), 0);
|
|
REQUIRE(out.size() == 2);
|
|
CHECK_FALSE(out[0].overloaded);
|
|
CHECK(out[1].overloaded);
|
|
CHECK(still.Counts().overloads_kept == 1);
|
|
}
|
|
|
|
// A detector that reports gain ADU per photon: the same spot read at gain 4 leaves the integrator in
|
|
// photons - a quarter of the ADU intensity, at the Poisson sigma of that many photons, i.e. half the
|
|
// sigma the ADU counted as photons gave.
|
|
TEST_CASE("BraggIntegrationEngineCPU_PixelGainGivesPhotons", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
const int cx = 600, cy = 300;
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = 40;
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx)
|
|
image[(cy + dy) * width + cx + dx] += static_cast<int32_t>(std::lround(800.0 * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
|
|
|
|
Reflection r{};
|
|
r.h = 1; r.k = 2; r.l = 3;
|
|
r.predicted_x = cx; r.predicted_y = cy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
const std::vector<Reflection> predicted{r};
|
|
|
|
for (const auto mode : {IntegratorMode::BoxSum, IntegratorMode::ProfileGaussian}) {
|
|
BraggIntegrationSettings settings;
|
|
settings.Integrator(mode);
|
|
experiment.ImportBraggIntegrationSettings(settings);
|
|
BraggIntegrationEngineCPU adu(experiment, PixelMask(experiment));
|
|
const auto out_adu = adu.Run(image, predicted, 1, 0);
|
|
settings.PixelGain(4.0f);
|
|
experiment.ImportBraggIntegrationSettings(settings);
|
|
BraggIntegrationEngineCPU photons(experiment, PixelMask(experiment));
|
|
const auto out_ph = photons.Run(image, predicted, 1, 0);
|
|
REQUIRE(out_adu.size() == 1);
|
|
REQUIRE(out_ph.size() == 1);
|
|
CHECK(out_ph[0].I == Catch::Approx(out_adu[0].I / 4.0f));
|
|
CHECK(out_ph[0].sigma == Catch::Approx(out_adu[0].sigma / 2.0f));
|
|
CHECK(out_ph[0].bkg == Catch::Approx(out_adu[0].bkg / 4.0f));
|
|
CHECK(out_ph[0].var_bkg == Catch::Approx(out_adu[0].var_bkg / 4.0f));
|
|
}
|
|
}
|
|
|
|
// The same frame, once in photons and once in a detector's units at 7 ADU per photon, integrates to the
|
|
// same reflections in photons when the engine is given the gain: the background clip, its one-photon
|
|
// floor, the variance floors, the profile fit's weights and a still's overload kept as a lower bound
|
|
// are all taken at the gain. A sparse background of 0-2 photons per pixel, where the floors bind, weak
|
|
// and strong spots, and one spot clipped at the saturation limit.
|
|
TEST_CASE("BraggIntegrationEngineCPU_AduIntegratesAsPhotons", "[Integration][portable]") {
|
|
constexpr int32_t g = 7, limit = 400;
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
ImagePreprocessorBuffer photons(npixel);
|
|
uint32_t state = 12345;
|
|
for (size_t i = 0; i < npixel; ++i) {
|
|
state = state * 1664525u + 1013904223u;
|
|
const uint32_t u = state >> 24;
|
|
photons[i] = u < 200 ? 0 : (u < 245 ? 1 : 2);
|
|
}
|
|
std::vector<Reflection> predicted;
|
|
int n = 0;
|
|
for (int cy = 60; cy < 480; cy += 40)
|
|
for (int cx = 60; cx < 1000; cx += 40, ++n) {
|
|
const double amplitude = n == 7 ? 3000.0 : 2.0 + 15.0 * (n % 23);
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx) {
|
|
int32_t &v = photons[(cy + dy) * width + cx + dx];
|
|
v += static_cast<int32_t>(std::lround(amplitude * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
|
|
if (v >= limit)
|
|
v = INT32_MAX;
|
|
}
|
|
Reflection r{};
|
|
r.h = n; r.k = 1; r.l = 1;
|
|
r.predicted_x = static_cast<float>(cx);
|
|
r.predicted_y = static_cast<float>(cy);
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
predicted.push_back(r);
|
|
}
|
|
ImagePreprocessorBuffer adu(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
adu[i] = photons[i] == INT32_MAX ? INT32_MAX : g * photons[i];
|
|
|
|
for (const auto mode : {IntegratorMode::BoxSum, IntegratorMode::ProfileGaussian}) {
|
|
BraggIntegrationSettings settings;
|
|
settings.Integrator(mode);
|
|
experiment.ImportBraggIntegrationSettings(settings);
|
|
experiment.Detector().SaturationLimit(limit);
|
|
BraggIntegrationEngineCPU in_photons(experiment, PixelMask(experiment));
|
|
const auto ref = in_photons.Run(photons, predicted, predicted.size(), 0);
|
|
settings.PixelGain(static_cast<float>(g));
|
|
experiment.ImportBraggIntegrationSettings(settings);
|
|
experiment.Detector().SaturationLimit(g * limit);
|
|
BraggIntegrationEngineCPU in_adu(experiment, PixelMask(experiment));
|
|
const auto out = in_adu.Run(adu, predicted, predicted.size(), 0);
|
|
REQUIRE(out.size() == ref.size());
|
|
REQUIRE(ref.size() > 100);
|
|
CHECK(in_adu.Counts().overloads_kept == 1);
|
|
CHECK(in_photons.Counts().overloads_kept == 1);
|
|
for (size_t i = 0; i < ref.size(); ++i) {
|
|
CHECK(out[i].h == ref[i].h);
|
|
CHECK(out[i].I == Catch::Approx(ref[i].I).epsilon(1e-4).margin(1e-3));
|
|
CHECK(out[i].sigma == Catch::Approx(ref[i].sigma).epsilon(1e-4));
|
|
CHECK(out[i].bkg == Catch::Approx(ref[i].bkg).epsilon(1e-4).margin(1e-5));
|
|
// 1/den - I cancels: float rounding of den shows at 1e-4 of the difference.
|
|
CHECK(out[i].var_bkg == Catch::Approx(ref[i].var_bkg).epsilon(1e-3).margin(1e-3));
|
|
CHECK(out[i].overloaded == ref[i].overloaded);
|
|
}
|
|
}
|
|
}
|