Build Packages / Create release (push) Successful in 24s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m29s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m27s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 9m53s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 9m58s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 11m22s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 13m39s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 18m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 16m32s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m11s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m3s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m23s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m41s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / Build documentation (push) Successful in 1m16s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m0s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 18m38s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 17m55s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m50s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 18m38s
Build Packages / Unit tests (push) Successful in 1h46m14s
* jfjoch_broker: Optional per-dataset authentication - statistics, images and plots can require a bearer token, which jfjoch_viewer supports. * jfjoch_viewer: Dark mode and a theme-matched colour scheme, a magnifier panel, and simpler contrast and background controls. * Rugnux: Multiple performance improvements on GPU and CPU (CPU-only processing up to 40% faster, faster image decoding on ARM), with unchanged results. * Rugnux: `--model` rigid-body refinement runs on the GPU, and the model-validation check is faster and more reliable. * Rugnux: Improved scaling and merging - error model, outlier rejection, absorption correction and French-Wilson amplitudes now agree more closely with XDS and ctruncate. * Rugnux: Improved integration - radial background on powder and ice rings, crowded rotation data keep their reflections, and CPU-only builds integrate large unit cells as GPU builds do. * Rugnux: More robust detector geometry - measured beam centre, X-ray bandwidth and goniometer rate, and geometry refinement accepted only on significant evidence. * Rugnux: Merged files are written in the standard setting, or in the setting of a reference MTZ, structure-factor mmCIF or model, with its free-R flags. * Rugnux: Richer report - ice and powder rings, further lattices, superstructure candidates and mosaicity, with warnings worded as prompts to check. * Rugnux: Clear error messages when a data set needs more GPU or host memory than is available. Reviewed-on: #83 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
99 lines
5.4 KiB
C++
99 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_all.hpp>
|
|
|
|
#include <cmath>
|
|
#include <random>
|
|
#include <vector>
|
|
|
|
#include "../common/DiffractionGeometry.h"
|
|
#include "../image_analysis/SensorAbsorption.h"
|
|
#include "../rugnux/SpotWidth.h"
|
|
|
|
namespace {
|
|
constexpr int W = 3000, H = 3000, C = 1500;
|
|
constexpr double DIST_MM = 100.0, PIXEL_MM = 0.075, LAMBDA_A = 1.0;
|
|
constexpr double THICKNESS_UM = 450.0;
|
|
constexpr double ANGULAR_SIGMA = 1.5e-4; // rad: divergence and crystal, the same in every direction
|
|
|
|
// Spots on an untilted detector, each an elliptical Gaussian with its long axis along the radius:
|
|
// the isotropic angular width, the bandwidth's radial streak 2 tan(theta) sigma, and the sensor
|
|
// parallax - a conversion depth z moves the photon by z tan(psi) along the radius, and on an
|
|
// untilted detector psi = 2theta. Worked out here from the flat-detector geometry, independently
|
|
// of the estimator's own Jacobians: R = D tan(2theta), so one radian of 2theta is D/cos^2(2theta)
|
|
// along the radius and one radian across the scattering plane D/cos(2theta) across it. Painted
|
|
// with 4x4 sub-pixel sampling so each pixel carries the box it integrates, and Poisson noise.
|
|
void PaintSpots(std::vector<int32_t> &image, std::vector<DiffractionSpot> &spots, double bw_fwhm,
|
|
int offset, std::mt19937 &rng) {
|
|
const double sigma_bw = bw_fwhm / 2.3548;
|
|
const double L_um = sensor_absorption::AttenuationLength_um("Si", LAMBDA_A);
|
|
std::vector<double> mean(image.size(), 3.0);
|
|
for (int cy = 40 + offset; cy < H - 40; cy += 60)
|
|
for (int cx = 40 + offset; cx < W - 40; cx += 60) {
|
|
const double rx = (cx - C) * PIXEL_MM, ry = (cy - C) * PIXEL_MM, r = std::hypot(rx, ry);
|
|
if (r < 8.0) continue;
|
|
const double tt = std::atan2(r, DIST_MM), c2 = std::cos(tt);
|
|
const double jr = DIST_MM / (PIXEL_MM * c2 * c2), jt = DIST_MM / (PIXEL_MM * c2);
|
|
const double depth_var = sensor_absorption::ConversionDepthVariance_um2(L_um * c2, THICKNESS_UM);
|
|
const double par = depth_var * std::tan(tt) * std::tan(tt) / (PIXEL_MM * 1000.0 * PIXEL_MM * 1000.0);
|
|
const double streak = 2.0 * std::tan(tt / 2.0) * sigma_bw;
|
|
const double var_u = jr * jr * (ANGULAR_SIGMA * ANGULAR_SIGMA + streak * streak) + par;
|
|
const double var_v = jt * jt * ANGULAR_SIGMA * ANGULAR_SIGMA;
|
|
const double ux = rx / r, uy = ry / r;
|
|
const double total = 20000.0;
|
|
const double norm = total / (2.0 * M_PI * std::sqrt(var_u * var_v) * 16.0);
|
|
for (int dy = -14; dy <= 14; dy++)
|
|
for (int dx = -14; dx <= 14; dx++) {
|
|
double v = 0.0;
|
|
for (int sy = 0; sy < 4; sy++)
|
|
for (int sx = 0; sx < 4; sx++) {
|
|
const double px = dx - 0.375 + 0.25 * sx, py = dy - 0.375 + 0.25 * sy;
|
|
const double u = px * ux + py * uy, t = -px * uy + py * ux;
|
|
v += std::exp(-0.5 * (u * u / var_u + t * t / var_v));
|
|
}
|
|
mean[static_cast<size_t>(cy + dy) * W + cx + dx] += norm * v;
|
|
}
|
|
spots.emplace_back(static_cast<uint32_t>(cx), static_cast<uint32_t>(cy),
|
|
static_cast<int64_t>(total));
|
|
}
|
|
for (size_t i = 0; i < image.size(); i++)
|
|
image[i] = std::poisson_distribution<int32_t>(mean[i])(rng);
|
|
}
|
|
|
|
std::optional<spot_width::BandwidthEstimate> Estimate(double bw_fwhm) {
|
|
DiffractionGeometry geometry;
|
|
geometry.BeamX_pxl(C).BeamY_pxl(C).DetectorDistance_mm(DIST_MM).PixelSize_mm(PIXEL_MM)
|
|
.Wavelength_A(LAMBDA_A);
|
|
std::mt19937 rng(7);
|
|
std::vector<spot_width::FluxCurve> curves;
|
|
for (int offset : {0, 20, 40}) {
|
|
ImagePreprocessorBuffer image(static_cast<size_t>(W) * H);
|
|
std::vector<DiffractionSpot> spots;
|
|
PaintSpots(image.getBuffer(), spots, bw_fwhm, offset, rng);
|
|
MeasureSpotFluxCurves(image, W, H, geometry, spots, curves);
|
|
}
|
|
return spot_width::EstimateBandwidth(curves, sensor_absorption::AttenuationLength_um("Si", LAMBDA_A),
|
|
THICKNESS_UM, PIXEL_MM * 1000.0);
|
|
}
|
|
}
|
|
|
|
// A 0.45 % FWHM bandwidth - a multilayer's - on a thick silicon sensor, whose parallax elongates the
|
|
// spots along the radius as well and grows with angle much as the bandwidth does. The estimator has
|
|
// to take the parallax out from the sensor's physics and return the bandwidth, significantly.
|
|
TEST_CASE("BandwidthEstimate_RecoversBandwidthPastParallax", "[SpotWidth]") {
|
|
const auto e = Estimate(0.0045);
|
|
REQUIRE(e.has_value());
|
|
CHECK(e->spots >= spot_width::BANDWIDTH_MIN_SPOTS);
|
|
CHECK(e->fwhm == Catch::Approx(0.0045).margin(0.0006));
|
|
CHECK(e->z > spot_width::BANDWIDTH_Z);
|
|
}
|
|
|
|
// The same spots from a monochromatic beam: the parallax alone must not read as a bandwidth.
|
|
TEST_CASE("BandwidthEstimate_MonochromaticIsNotSignificant", "[SpotWidth]") {
|
|
const auto e = Estimate(0.0);
|
|
REQUIRE(e.has_value());
|
|
CHECK(e->z < spot_width::BANDWIDTH_Z);
|
|
CHECK(std::abs(e->fwhm) < 0.0015);
|
|
}
|