Bragg integration: a shared signal pixel belongs to the nearer reflection
Nothing kept a neighbour's flux out of a reflection's own signal disk. The union mask keeps neighbour cores out of the BACKGROUND ring, but the r1 disk was read whole, so on a dense pattern a crowded reflection measures part of its neighbour as its own. Ownership is decided once per image into a per-pixel (quantised distance, reflection) key written with an atomic minimum, so the nearest predicted centre wins whatever order the writes arrive in and the lowest index breaks a tie. `--overlap exclude`, now the default, drops the pixels a nearer neighbour owns from the profile fit. A profile fit is the amplitude of a normalised profile, so leaving pixels out renormalises the estimator by construction and the reflection stays unbiased rather than being discarded; the summation-fallback guard is scaled back to the disk the box-sum seed actually read, so it still compares like with like. `--overlap reject` is the XDS MINPK alternative - drop the reflection when less than `--overlap-minpk` of its expected profile is cleanly its own. A box sum has no profile to renormalise with, so `exclude` is a no-op there and only `reject` acts on it. Widening the split - keeping a pixel only where no other centre is within its distance PLUS a margin - was built and measured, and it is worse monotonically: the residual bias of the pixels that were kept grows from +0.072 to +0.209 in ln intensity at 0 to 3 px of margin. What the margin removes is the reflection's own profile, not the neighbour's tail, so the plain nearest-centre split is the rule. Measured on the full 38-crystal rotation battery against the same binary with the treatment off: ISa better 15 / worse 8, summed shortfall against XDS 39.7 -> 28.1. Three of the losses are the two-pass loop taking its other branch - their median mosaicity moves between the two known attractors - rather than the change under test; excluding those it is better 15 / worse 5 and the shortfall goes 31.3 -> 14.4. The two crowded crystals gain 38% and 52% of their ISa, one of them passing XDS. High-shell CC1/2 over the 35 crystals that neither flipped branch nor carry a collapsed error model is better 7 / worse 7. Space groups unchanged at 35/38. The owner map is built only when a treatment is asked for and costs 1.1% of the battery's wall clock - 23% on a genuinely crowded crystal, nothing where no two predictions touch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,7 +38,10 @@ Reflection MakeReflection(float x, float y, float d, int hkl) {
|
||||
return r;
|
||||
}
|
||||
|
||||
Scene BuildScene(size_t width, size_t height, int spacing = 60) {
|
||||
// companion_dx > 0 puts a second spot that many pixels beside every grid spot, so their r1 signal
|
||||
// disks share pixels while the background rings still see clean sky - which is what a dense pattern
|
||||
// actually looks like (crowded along one reciprocal axis, sparse across it).
|
||||
Scene BuildScene(size_t width, size_t height, int spacing = 60, float companion_dx = 0.0f) {
|
||||
Scene s;
|
||||
s.width = width;
|
||||
s.height = height;
|
||||
@@ -66,6 +69,19 @@ Scene BuildScene(size_t width, size_t height, int spacing = 60) {
|
||||
}
|
||||
const float d = 1.4f + 0.12f * static_cast<float>((gx + gy) % 12); // 1.4..2.72 A
|
||||
s.predicted.push_back(MakeReflection(cx, cy, d, hkl++));
|
||||
if (companion_dx > 0.0f) {
|
||||
const float ccx = cx + companion_dx;
|
||||
for (int dy = -6; dy <= 6; ++dy)
|
||||
for (int dx = -6; dx <= 6; ++dx) {
|
||||
const int x = static_cast<int>(std::lround(ccx)) + dx;
|
||||
const int y = static_cast<int>(std::lround(cy)) + dy;
|
||||
if (x < 0 || y < 0 || x >= static_cast<int>(width) || y >= static_cast<int>(height)) continue;
|
||||
const double ex = x - ccx, ey = y - cy;
|
||||
const double g = 0.6 * amp * std::exp(-(ex * ex + ey * ey) / (2.0 * sigma * sigma));
|
||||
s.image[y * width + x] += static_cast<int32_t>(std::lround(g));
|
||||
}
|
||||
s.predicted.push_back(MakeReflection(ccx, cy, d, hkl++));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +101,8 @@ DiffractionExperiment MakeExperiment(IntegratorMode mode, std::optional<float> b
|
||||
bool radial = false,
|
||||
const DetectorSetup &det = DetJF(2),
|
||||
float stencil_k = 0.0f,
|
||||
float r1 = 0.0f, float r2 = 0.0f, float r3 = 0.0f) {
|
||||
float r1 = 0.0f, float r2 = 0.0f, float r3 = 0.0f,
|
||||
OverlapMode overlap = OverlapMode::Off) {
|
||||
DiffractionExperiment experiment(det); // DetJF(2) (small) keeps the correctness test fast
|
||||
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV)
|
||||
.BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
||||
@@ -100,6 +117,7 @@ DiffractionExperiment MakeExperiment(IntegratorMode mode, std::optional<float> b
|
||||
settings.BackgroundTrimFraction(0.10f);
|
||||
settings.BackgroundRadialCorrection(radial);
|
||||
settings.StencilKSigma(stencil_k);
|
||||
settings.Overlap(overlap);
|
||||
experiment.ImportBraggIntegrationSettings(settings);
|
||||
return experiment;
|
||||
}
|
||||
@@ -107,15 +125,17 @@ DiffractionExperiment MakeExperiment(IntegratorMode mode, std::optional<float> b
|
||||
void CompareCpuVsGpu(IntegratorMode mode, std::optional<float> bandwidth_fwhm,
|
||||
float clip_nsigma = 4.0f, bool radial = false, int spacing = 60,
|
||||
float stencil_k = 0.0f,
|
||||
float r1 = 0.0f, float r2 = 0.0f, float r3 = 0.0f) {
|
||||
float r1 = 0.0f, float r2 = 0.0f, float r3 = 0.0f,
|
||||
OverlapMode overlap = OverlapMode::Off, float companion_dx = 0.0f) {
|
||||
const DiffractionExperiment experiment =
|
||||
MakeExperiment(mode, bandwidth_fwhm, clip_nsigma, radial, DetJF(2), stencil_k, r1, r2, r3);
|
||||
MakeExperiment(mode, bandwidth_fwhm, clip_nsigma, radial, DetJF(2), stencil_k, r1, r2, r3,
|
||||
overlap);
|
||||
const size_t width = experiment.GetXPixelsNum();
|
||||
const size_t height = experiment.GetYPixelsNum();
|
||||
const size_t npixel = experiment.GetPixelsNum();
|
||||
REQUIRE(npixel == width * height);
|
||||
|
||||
const Scene scene = BuildScene(width, height, spacing);
|
||||
const Scene scene = BuildScene(width, height, spacing, companion_dx);
|
||||
REQUIRE(scene.image.size() == npixel);
|
||||
REQUIRE(scene.predicted.size() > 60);
|
||||
|
||||
@@ -197,6 +217,27 @@ TEST_CASE("BraggIntegrationEngineGPU_MatchesCPU") {
|
||||
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, 0.04f, 0.0f, false, 120, 4.0f, 6.0f, 8.0f, 12.0f);
|
||||
}
|
||||
SECTION("ProfileEmpirical") { CompareCpuVsGpu(IntegratorMode::ProfileEmpirical, std::nullopt); }
|
||||
// Overlap treatment: companions 4 px apart put each reflection's centre inside its neighbour's
|
||||
// signal disk, so the owner map, the excluded pixels and the profile fraction the two modes act on
|
||||
// all have to come out the same in both engines - the ownership atomic in particular is settled by
|
||||
// an atomicMin on the GPU and a serial minimum on the CPU.
|
||||
SECTION("ProfileGaussian overlap exclude") {
|
||||
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, false, 60, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, OverlapMode::Exclude, 4.0f);
|
||||
}
|
||||
SECTION("ProfileGaussian overlap reject") {
|
||||
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, false, 60, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, OverlapMode::Reject, 4.0f);
|
||||
}
|
||||
SECTION("BoxSum overlap reject") {
|
||||
CompareCpuVsGpu(IntegratorMode::BoxSum, std::nullopt, 4.0f, false, 60, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, OverlapMode::Reject, 4.0f);
|
||||
}
|
||||
// Nothing shares a pixel at this spacing, so an overlap treatment has to leave the result alone.
|
||||
SECTION("ProfileGaussian overlap inert") {
|
||||
CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 4.0f, false, 60, 0.0f,
|
||||
0.0f, 0.0f, 0.0f, OverlapMode::Exclude);
|
||||
}
|
||||
SECTION("ProfileGaussian mono trim") { CompareCpuVsGpu(IntegratorMode::ProfileGaussian, std::nullopt, 0.0f); }
|
||||
// The radial background curvature correction is computed independently in the two engines
|
||||
// (host loop vs radial_correct kernel), so it needs its own parity coverage.
|
||||
@@ -217,8 +258,12 @@ TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {
|
||||
WARN("No CUDA GPU present. Skipping benchmark");
|
||||
return;
|
||||
}
|
||||
// The overlap treatment is priced here too: it adds an owner map over the whole frame plus one
|
||||
// atomic per claimed pixel, so what it costs is a property of the frame more than of the crowding.
|
||||
for (OverlapMode ovl : {OverlapMode::Off, OverlapMode::Reject, OverlapMode::Exclude}) {
|
||||
const DiffractionExperiment experiment = MakeExperiment(IntegratorMode::ProfileGaussian, std::nullopt,
|
||||
4.0f, false, DetJF4M());
|
||||
4.0f, false, DetJF4M(), 0.0f, 0.0f, 0.0f, 0.0f,
|
||||
ovl);
|
||||
const size_t width = experiment.GetXPixelsNum();
|
||||
const size_t height = experiment.GetYPixelsNum();
|
||||
const size_t npixel = experiment.GetPixelsNum();
|
||||
@@ -226,7 +271,7 @@ TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {
|
||||
|
||||
auto stream = std::make_shared<CudaStream>();
|
||||
BraggIntegrationEngineGPU gpu(experiment, stream);
|
||||
for (int spacing : {28, 40, 60, 90}) {
|
||||
for (int spacing : {28, 60}) {
|
||||
const Scene scene = BuildScene(width, height, spacing);
|
||||
const size_t nrefl = scene.predicted.size();
|
||||
|
||||
@@ -254,9 +299,10 @@ TEST_CASE("BraggIntegrationEngineGPU_Benchmark", "[.][bragg_bench]") {
|
||||
const auto c1 = std::chrono::steady_clock::now();
|
||||
const double cpu_ms = std::chrono::duration<double, std::milli>(c1 - c0).count();
|
||||
|
||||
WARN(width << "x" << height << " | " << nrefl << " refl (" << observed / iters
|
||||
<< " obs) | GPU " << ms << " ms | CPU " << cpu_ms << " ms (" << cpu_observed
|
||||
<< " obs) | speedup " << cpu_ms / ms << "x");
|
||||
WARN((int) ovl << " | " << width << "x" << height << " | " << nrefl << " refl ("
|
||||
<< observed / iters << " obs) | GPU " << ms << " ms | CPU " << cpu_ms << " ms ("
|
||||
<< cpu_observed << " obs) | speedup " << cpu_ms / ms << "x");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user