Stills integration: background ring reach measured on empty apertures, radial curvature from the whole frame

Two changes to the stills integration background, both stills-only (rotation keeps its path bit for bit):

1. The r2..r3 background ring's outer radius is measured per run. The pre-scan lays apertures on a
   grid over its frames, away from every found spot, and for each candidate r3 (r2+2 .. 14) takes the
   mean square difference between the r1 disk mean and the clipped ring mean. With nothing in the
   aperture that is exactly the background error the ring makes (noise + bias); the run keeps the
   minimum. On a photon-counting pixel detector the noise wins and the shipped r3=14 is kept; on a
   binned CCD the background structure wins and the narrowest ring is taken. r1 stays 6: a smaller
   disk (4,6,14) costs 0.0013 dimple R_free on a JUNGFRAU set, so it does not pass the
   photon-counting gate. --adaptive-integration-radius now defaults on for stills too (=off restores
   the fixed 6/8/14). The pre-scan bandwidth estimate is not applied to stills.

2. The radial background-curvature correction on stills takes its curve from the adaptive spot
   finder's peak-excluded per-ring background (whole frame) instead of the predicted reflections'
   own ring pixels, and auto applies it to every stills image (no ice-score gate: a solvent/carrier
   hump curves the background as much as ice). New engine call RadialBackgroundFromRings; CPU, GPU
   and Metal read the handed-over curve. The GPU box sum now sizes the radial bin index separately
   from accumulating, so a handed-over curve is read at the right radius.

Measured (dimple R_free, deposited free set, battery arguments, all frames unless noted):
  pal86  (Rayonix 4x4, 156 um): 0.354 -> 0.218   (ring 10; at 4,6,10 0.213)
  lcls222 (Rayonix 4x4, 177 um): 0.205 -> 0.196  (ring 10)
  sacla236 (MPCCD): 0.2588 -> 0.2558             (ring 11)
  alvra213_int16 (JUNGFRAU): 0.1986 -> 0.1986    (ring 14, unchanged)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-21 18:10:08 +02:00
1 parent 987e812e23
commit e8145d8d2f
17 files changed
+558 -42

No files matched your search

+55
View File
@@ -15,6 +15,7 @@
#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/bragg_integration/BraggIntegrationEngineGPU.h"
@@ -413,6 +414,60 @@ TEST_CASE("BraggIntegrationEngineGPU_ReusedEngineMatchesFresh") {
}
}
// The radial correction with its curve handed over from a per-ring profile (RadialBackgroundFromRings), as
// on stills: the GPU reads the same curve and corrects the same empty aperture as the CPU.
TEST_CASE("BraggIntegrationEngineGPU_RadialCurveFromRingsMatchesCPU", "[Integration]") {
if (get_gpu_count() == 0) {
WARN("No CUDA GPU present. Skipping");
return;
}
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 cpu_image(npixel);
ImagePreprocessorBufferGPU gpu_image(npixel);
for (size_t i = 0; i < npixel; ++i) {
cpu_image[i] = static_cast<int32_t>(std::lround(
background(std::hypot(static_cast<double>(i % width) - bx, static_cast<double>(i / width) - by))));
gpu_image[i] = cpu_image[i];
}
auto stream = std::make_shared<CudaStream>();
REQUIRE(cudaMemcpyAsync(gpu_image.getGPUBuffer(), gpu_image.getBuffer().data(),
npixel * sizeof(int32_t), cudaMemcpyHostToDevice, *stream) == cudaSuccess);
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)))));
}
std::vector<Reflection> predicted;
for (int i = 0; i < 8; ++i)
predicted.push_back(MakeReflection(bx + 300.0f * std::cos(0.7f * i), by + 300.0f * std::sin(0.7f * i), 2.0f, i + 1));
for (const auto mode : {IntegratorMode::BoxSum, IntegratorMode::ProfileGaussian}) {
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings().Integrator(mode).BackgroundRadialCorrection(true));
BraggIntegrationEngineCPU cpu(experiment, PixelMask(experiment));
BraggIntegrationEngineGPU gpu(experiment, stream, PixelMask(experiment));
cpu.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), low_q, dq);
gpu.RadialBackgroundFromRings(rings, static_cast<int32_t>(rings.size()), low_q, dq);
const auto out_cpu = cpu.Run(cpu_image, predicted, predicted.size(), 0);
const auto out_gpu = gpu.Run(gpu_image, predicted, predicted.size(), 0);
REQUIRE(out_cpu.size() == predicted.size());
REQUIRE(out_gpu.size() == out_cpu.size());
for (size_t i = 0; i < out_cpu.size(); ++i) {
INFO("mode " << static_cast<int>(mode) << " reflection " << i);
CHECK(out_gpu[i].bkg == Catch::Approx(out_cpu[i].bkg).epsilon(0.001).margin(0.05));
CHECK(std::fabs(out_gpu[i].I) < 10.0f);
CHECK(std::fabs(out_cpu[i].I) < 10.0f);
}
}
}
// Hidden ([.]) benchmark: the raison d'etre of the GPU port is < 2 ms/frame (vs ~142 ms on the CPU
// for ProfileIntegrate2D). Run explicitly with: ./jfjoch_test "[bragg_bench]"
TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {