Files
Jungfraujoch/tests/BackgroundApertureTest.cpp
T
leonarski_fandClaude Opus 5.5 07a43a13eb Stills: the radial curvature correction decided per run on the same empty apertures
The first stills battery of e8145d8 lost on one Rayonix set (lcls242_s, dimple R_free 0.2302 ->
0.2323): there the whole-frame radial correction hurts. On 40,000 of its frames: battery default
0.2462, ring 12 with the correction 0.2444, ring 12 without it 0.2336, ring 14 with it 0.2467. Its
beam is strongly polarised and the azimuthally averaged ring background does not describe the local
background.

So the empty apertures now also carry the corrected difference: the frame's ring background
(RingBackground, factored out of the engine) averaged over the ring pixels minus over the disk pixels
- what the integrator's kernel takes off a reflection - is subtracted from each disk-minus-ring
difference. The ring is still chosen on the plain differences, so its reach does not rest on the
radial model; where the correction was left to auto it is then kept only if it makes that ring predict
the disk better. Decisions on 15-60 pre-scan frames:
  pal86 ring 10, correction on (240.6 -> 221.1)   lcls222 ring 10, on (6.34e4 -> 3.41e4)
  lcls242_s ring 12, off (277.2 -> 285.8)         alvra213_int16 ring 14, on (0.01424 -> 0.01422)
Choosing ring and correction jointly instead picked ring 14 with the correction on lcls222, where dimple
reads 0.204 against 0.196 at ring 10; a median instead of the mean square picked ring 14 with the
correction on lcls242_s, the worst arm there. The sequential rule is the one that agrees with dimple on
every set measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
2026-10-21 20:58:32 +02:00

119 lines
5.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <catch2/catch_test_macros.hpp>
#include <cmath>
#include <random>
#include "../image_analysis/bragg_integration/BackgroundAperture.h"
#include "../common/JFJochMath.h"
namespace {
constexpr int W = 600, H = 600;
// A background of `level` counts a pixel with Poisson noise, modulated by `ripple` (relative) with a
// period of 20 px along x - the structure a ring further from the disk reads wrong.
ImagePreprocessorBuffer Frame(double level, double ripple, uint32_t seed) {
std::mt19937 rng(seed);
ImagePreprocessorBuffer img(static_cast<size_t>(W) * H);
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++) {
std::poisson_distribution<int> p(level * (1.0 + ripple * std::sin(2.0 * PI * x / 20.0)));
img[static_cast<size_t>(y) * W + x] = p(rng);
}
return img;
}
const RingBackground no_rings({}, 0, 0.0f, 0.0f);
DiffractionGeometry Geometry() {
DiffractionGeometry g;
g.BeamX_pxl(300.0f).BeamY_pxl(300.0f).DetectorDistance_mm(60.0f).PixelSize_mm(0.075f).Wavelength_A(1.0f);
return g;
}
int BestR3(double level, double ripple) {
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
background_aperture::Tally tally;
for (uint32_t seed = 1; seed <= 4; seed++) {
auto img = Frame(level, ripple, seed);
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, no_rings, Geometry(),
DetectorModules(), tally);
}
REQUIRE(tally.apertures > 100);
CHECK(tally.apertures_radial == 0);
return static_cast<int>(r3[background_aperture::Best(tally.MeanSquare(false))]);
}
} // namespace
TEST_CASE("BackgroundAperture_CandidateR3", "[Integration]") {
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
REQUIRE(r3.size() == 5);
CHECK(r3.front() == 10.0f);
CHECK(r3.back() == 14.0f);
}
// A few counts a pixel on a flat background: the noise wins, and the widest ring is kept.
TEST_CASE("BackgroundAperture_FlatNoisyKeepsWidestRing", "[Integration]") {
CHECK(BestR3(0.5, 0.0) == 14);
}
// Thousands of counts a pixel on a background that varies over the stencil's own scale: the structure
// wins, and the narrowest ring is kept.
TEST_CASE("BackgroundAperture_StructuredBrightTakesNarrowestRing", "[Integration]") {
CHECK(BestR3(2000.0, 0.05) == 10);
}
// An aperture over an unreadable pixel is not used.
TEST_CASE("BackgroundAperture_SkipsUnreadablePixels", "[Integration]") {
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
auto img = Frame(1.0, 0.0, 7);
background_aperture::Tally all;
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, no_rings, Geometry(),
DetectorModules(), all);
for (size_t i = 0; i < static_cast<size_t>(W) * H; i += 97)
img[i] = INT32_MIN;
background_aperture::Tally masked;
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 4.0f, 1.0f, no_rings, Geometry(),
DetectorModules(), masked);
CHECK(masked.apertures == 0);
CHECK(all.apertures > 0);
}
// A bright background that is a curved function of the radius alone: the radial correction, read off
// the frame's ring background, makes the ring predict the disk better than it does without it.
TEST_CASE("BackgroundAperture_RadialCorrectionHelpsOnARadialBackground", "[Integration]") {
const DiffractionGeometry g = Geometry();
const auto background = [](double r) { return 2000.0 + 0.2 * (r - 150.0) * (r - 150.0); };
std::mt19937 rng(3);
ImagePreprocessorBuffer img(static_cast<size_t>(W) * H);
for (int y = 0; y < H; y++)
for (int x = 0; x < W; x++) {
std::poisson_distribution<int> p(background(std::hypot(x - 300.0, y - 300.0)));
img[static_cast<size_t>(y) * W + x] = p(rng);
}
const float low_q = 0.05f, dq = 0.005f;
std::vector<float> profile(1000);
for (size_t i = 0; i < profile.size(); i++) {
const double q = low_q + (i + 0.5) * dq;
const double r = 60.0 / 0.075 * std::tan(2.0 * std::asin(q * 1.0 / (4.0 * PI)));
profile[i] = static_cast<float>(background(r));
}
const RingBackground rings(profile, static_cast<int32_t>(profile.size()), low_q, dq);
const auto r3 = background_aperture::CandidateR3(8.0f, 14.0f);
background_aperture::Tally tally;
// Unclipped: a gradient this steep would put the high side of the ring above a photon clip.
background_aperture::MeasureFrame(img, W, H, {}, 6.0f, 8.0f, r3, 0.0f, 1.0f, rings, g, DetectorModules(), tally);
REQUIRE(tally.apertures_radial == tally.apertures);
REQUIRE(tally.apertures > 100);
const auto plain = tally.MeanSquare(false), radial = tally.MeanSquare(true);
// The pre-scan's question: at the ring the plain differences choose, does the correction help.
const int best = background_aperture::Best(plain);
CHECK(radial[best] < 0.8 * plain[best]);
// With the curvature taken out, the noise wins again and a wide ring is best.
CHECK(r3[background_aperture::Best(radial)] >= 12.0f);
}