Files
Jungfraujoch/tests/SpotFootprintTest.cpp
leonarski_f a395f358ef
Build Packages / Create release (push) Successful in 17s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m22s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m37s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m33s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 10m39s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 11m4s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 13m19s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 17m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 18m49s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 19m10s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m26s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m31s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 18m54s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m45s
Build Packages / Generate python client (push) Successful in 37s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m20s
Build Packages / Build documentation (push) Successful in 1m32s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m37s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m6s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 19m49s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 20m29s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 17m2s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 14m27s
Build Packages / Unit tests (push) Successful in 1h18m12s
1.0.0-rc.174 (#84)
* Rugnux: Performance improvements on GPU and CPU (more of the pre-scan and of scaling on the GPU, faster CPU spot finding and crystal refinement), with unchanged results.
* Rugnux: More robust processing - patches of persistently hot pixels are masked, an inconsistent merge triggers a retry at the measured beam centre, and builds targeting different CPU levels give the same results.
* Rugnux: Improved scaling and merging - reflections with an overloaded pixel are dropped, as in XDS, sparse rotation sweeps are scaled more reliably, and French-Wilson amplitudes use an anisotropic Wilson prior.
* Rugnux: Improved space-group determination - glide planes in groups without a centre of symmetry, screw axes from short or weak axial rows kept when a higher group is adopted, and more reliable decisions on twinned and pseudo-symmetric crystals.
* Rugnux: Improved small-molecule processing - spots that grow wider than the integration disk and split spots are integrated over their measured footprint, sparse lattices are integrated on every frame, and the `.hkl` file holds unmerged scaled reflections (SHELX HKLF 4).
* Rugnux: Reads Rigaku d*TREK SMV images (Saturn CCD), including detector 2theta and encoded pixel overflows; home-source (rotating-anode) datasets were added to the validation battery.
* jfjoch_viewer: Fixed processing failing at the end with "Wrong JPEG library version" on Linux; the merge window shows the space group with proper subscripts and a checklist of crystal pathologies.

Reviewed-on: #84
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-06 14:03:18 +02:00

105 lines
5.4 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());
}
// A reflection recorded as a doublet: two indexed spots on either side of the prediction, along the
// radius. The offsets are found against the reflection of the same hkl, and their mean square is
// added to the widths of the bin they fall in.
TEST_CASE("SpotFootprint_OffsetsFromPredictionWidenTheTable", "[Integration][portable]") {
const float bx = 600.0f, by = 600.0f;
std::vector<Reflection> refl(1);
refl[0].h = 1; refl[0].k = 2; refl[0].l = 3;
refl[0].predicted_x = bx + 1000.0f; // on +x, so radial = x
refl[0].predicted_y = by;
std::vector<SpotToSave> spots;
spots.push_back({.x = bx + 1004.0f, .y = by, .lattice = 0, .h = 1, .k = 2, .l = 3, .indexed = true});
spots.push_back({.x = bx + 996.0f, .y = by, .lattice = 0, .h = 1, .k = 2, .l = 3, .indexed = true});
spots.push_back({.x = bx + 1000.0f, .y = by + 30.0f, .lattice = 0, .h = 3, .k = 2, .l = 1, .indexed = true}); // no such prediction
spots.push_back({.x = bx + 1000.0f, .y = by + 30.0f, .lattice = -1, .h = 1, .k = 2, .l = 3, .indexed = false}); // not indexed
std::vector<FootprintOffset> offsets;
MeasureFootprintOffsets(spots, refl, bx, by, offsets);
REQUIRE(offsets.size() == 2);
REQUIRE_THAT(offsets[0].off_rad, Catch::Matchers::WithinAbs(4.0, 1e-4));
REQUIRE_THAT(offsets[0].off_tan, Catch::Matchers::WithinAbs(0.0, 1e-4));
std::vector<FootprintOffset> pool;
for (int i = 0; i < FOOTPRINT_MIN_SPOTS_PER_BIN; ++i)
pool.insert(pool.end(), offsets.begin(), offsets.end());
SpotFootprint widths;
widths.bin_px = 100.0f;
widths.sigma_rad.assign(12, 1.0f);
widths.sigma_tan.assign(12, 2.0f);
const SpotFootprint fp = FootprintWithOffsets(widths, pool);
for (int b = 0; b < 12; ++b) { // one filled bin (10) serves them all
REQUIRE_THAT(fp.sigma_rad[b], Catch::Matchers::WithinAbs(std::sqrt(17.0), 1e-4));
REQUIRE_THAT(fp.sigma_tan[b], Catch::Matchers::WithinAbs(2.0, 1e-4));
}
REQUIRE(FootprintWithOffsets(widths, {}).sigma_rad[3] == 1.0f);
REQUIRE(FootprintWithOffsets(SpotFootprint{}, pool).empty());
}