Files
Jungfraujoch/tests/SpotFootprintTest.cpp
T
leonarski_fandClaude Opus 5.5 8394b5988d Integration follows the measured spot footprint where a spot outgrows the r1 disk
The integrator's r1 disk and r2..r3 background ring are fixed in pixels and chosen from spots near
the beam. On small-molecule data at 20-25 keV a spot's standard deviation grows from ~1 px near the
beam to ~5 px at the edge (radially from parallax/obliquity, tangentially from the crystal's
azimuthal spread), so the r1 = 4 disk holds a quarter of the flux there, the background ring a third
of it, and the in-disk second moments the Gaussian is built from saturate near r1^2/4. On top of
that, the profile/summation runaway guard sent 20-30% of these reflections - the strong, wide ones -
back to the truncated r1 box sum.

- SpotFootprint: every pre-scan spot (width frames) is measured with a window that follows it
  (3 sigma, iterated, re-centred), radially and tangentially; the medians per distance-from-beam bin
  become BraggIntegrationSettings::Footprint. Installed only where some bin outgrows r1, and on the
  adaptive side like the radius (pre-pass without; the starvation guard falls back to the settings
  without it).
- BraggStencil: where 3 sigma > r1 the background ring starts at 3 sigma along and across the radius,
  the summation region is the r1 disk plus the 3-sigma footprint ellipse (so the guard's fallback is a
  complete intensity), and the per-reflection Gaussian takes the footprint widths. Compact spots keep
  the stencil bit for bit. Both engines build it from the same header.

SHELXL against COD (R1 / fixed-XDS-model R1(F)): citric acid .101/.230 -> .077/.055, HEPES
.070/.179 -> .048/.050, aspirin 20 keV .059/.070 -> .052/.061, aspirin 25 keV unchanged, L-cystine
25 keV unchanged (.145 -> .144).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB
2026-10-04 02:02:15 +02:00

69 lines
3.3 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 <catch2/matchers/catch_matchers_floating_point.hpp>
#include <cmath>
#include <vector>
#include "../image_analysis/bragg_integration/SpotFootprint.h"
// Spots drawn as Gaussians elongated along and across the radius are measured back at their widths,
// whatever their azimuth, and tabulated by distance from the beam.
TEST_CASE("SpotFootprint_MeasuresRadialAndTangentialWidths", "[Integration][portable]") {
const int W = 1200, H = 1200;
const float bx = 600.0f, by = 600.0f;
std::vector<int32_t> img(static_cast<size_t>(W) * H, 10); // flat background
std::vector<float> xs, ys;
// Width grows with distance: sigma_rad = 1 + r/200, sigma_tan = 1 + r/100.
for (int k = 0; k < 400; ++k) {
const float r = 60.0f + 480.0f * static_cast<float>(k % 20) / 20.0f;
const float phi = 0.61f * static_cast<float>(k);
const float x = bx + r * std::cos(phi), y = by + r * std::sin(phi);
const float ux = std::cos(phi), uy = std::sin(phi);
const float sr = 1.0f + r / 200.0f, st = 1.0f + r / 100.0f;
for (int py = static_cast<int>(y) - 30; py <= static_cast<int>(y) + 30; ++py)
for (int px = static_cast<int>(x) - 30; px <= static_cast<int>(x) + 30; ++px) {
if (px < 0 || py < 0 || px >= W || py >= H) continue;
const float dx = px - x, dy = py - y;
const float rad = dx * ux + dy * uy, tn = -dx * uy + dy * ux;
img[static_cast<size_t>(py) * W + px] += static_cast<int32_t>(
std::lround(2000.0f * std::exp(-rad * rad / (2 * sr * sr) - tn * tn / (2 * st * st))));
}
xs.push_back(x);
ys.push_back(y);
}
// Overlapping spots are not what this checks: keep those far from every other one.
std::vector<float> kx, ky;
for (size_t i = 0; i < xs.size(); ++i) {
bool alone = true;
for (size_t j = 0; j < xs.size(); ++j)
if (i != j && std::hypot(xs[i] - xs[j], ys[i] - ys[j]) < 45.0f) alone = false;
if (alone) { kx.push_back(xs[i]); ky.push_back(ys[i]); }
}
REQUIRE(kx.size() > 40);
std::vector<FootprintSpot> spots;
MeasureFootprintSpots(img.data(), W, H, bx, by, kx, ky, spots);
REQUIRE(spots.size() > 30);
for (const auto &s : spots) {
CHECK_THAT(s.sigma_rad, Catch::Matchers::WithinRel(1.0f + s.r_px / 200.0f, 0.12f));
CHECK_THAT(s.sigma_tan, Catch::Matchers::WithinRel(1.0f + s.r_px / 100.0f, 0.12f));
}
}
TEST_CASE("SpotFootprint_TableFillsSparseBinsFromNeighbours", "[Integration][portable]") {
std::vector<FootprintSpot> spots;
for (int i = 0; i < FOOTPRINT_MIN_SPOTS_PER_BIN; ++i) {
spots.push_back({50.0f, 1.0f, 1.5f}); // bin 0
spots.push_back({1150.0f, 3.0f, 4.0f}); // last bin
}
const SpotFootprint fp = FootprintFromSpots(spots, 1200.0f);
REQUIRE(fp.sigma_rad.size() == static_cast<size_t>(FOOTPRINT_BINS));
REQUIRE(fp.sigma_rad.front() == 1.0f);
REQUIRE(fp.sigma_tan.back() == 4.0f);
REQUIRE(fp.sigma_rad[2] == 1.0f); // nearer the first bin
REQUIRE(fp.sigma_rad[FOOTPRINT_BINS - 3] == 3.0f); // nearer the last
REQUIRE(FootprintFromSpots({}, 1200.0f).empty());
}