The r2 regions set aside from a reflection's r2..r3 background ring were those of every prediction in the +-4 sigma rocking window. On a finely sliced dense pattern most of them are the tails of reflections recorded on the frames either side, which fill every ring while the frame shows nothing there.9ac2ca677kept the reflections those rings starved by taking the ring whole, neighbour pixels included, and relying on the high-side clip. A prediction now masks the ring only where it puts at least 5% of its flux on the frame (partiality >= 0.05). A ring still starved by those neighbours has real flux in it, and the reflection is dropped, as before9ac2ca677: taking it whole let the neighbours' wings into the background. The CPU mask holds two levels (tail, flux); the GPU writes them in two launches so the flux mark wins, with no atomics. The count the widened-radius guard reads keeps measuring the pattern's density over every prediction, tails included, so the guard decides on the quantity its 1.13% bound was read off. Read on the gated mask it let a cubic set keep r1=6, and R_meas went 36.9 -> 43.2%. Masked but unreadable ring pixels are no longer counted as the neighbours' doing. Fixed radius, same code base, --model, the 0.05 deg / 7200-frame set: pre-9ac2ca6779ac2ca677this rings starved 88.9% 88.9% 21.5% partials ingested 7.4M 67M 53M completeness 17.9% 79.7% 85.1% ISa 12.8 14.3 14.7 R_meas 6.8% 8.4% 7.6% R_free 0.139 0.176 0.171 radial misfit 0.11 0.185 0.066 R vs model, common hkl to 0.69 A (61k): 0.133 0.130 0.129 (the R_free rise over pre-9ac2ca677 is composition: 63k -> 319k reflections to 0.69 A, the added ones weaker.) Gate at 0.2 instead: 0.1% starved but radial misfit 0.35, R_free 0.180. A crystal whose header-geometry pass indexes a 7x supercell: 9ac2ca677's whole rings raised that pass's I/sigma >= 2 count past the refined pass's by more than 10%. RefinedPassIsWorse then sent the run back to the supercell. Now: the true cell and space group as before9ac2ca677, same ISa, 104M partials in the header pass against 219M. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
79 lines
3.7 KiB
C++
79 lines
3.7 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 <cstdint>
|
|
#include <vector>
|
|
|
|
#include "../common/BraggIntegrationSettings.h"
|
|
#include "../common/DetectorSetup.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../common/Reflection.h"
|
|
#include "../image_analysis/bragg_integration/BraggIntegrationEngineCPU.h"
|
|
#include "../image_analysis/image_preprocessing/ImagePreprocessorBuffer.h"
|
|
|
|
// One spot on a flat background, inside a grid of predictions 8 px apart that put no flux on the
|
|
// frame - the tails of reflections recorded on the frames either side of a finely sliced one. Their
|
|
// r2 regions cover every background ring, so whether the spot keeps a clean ring depends only on
|
|
// whether those predictions are allowed to mask it.
|
|
TEST_CASE("BraggIntegrationEngineCPU_NeighbourMaskFollowsPartiality", "[Integration][portable]") {
|
|
DiffractionExperiment experiment(DetJF(2));
|
|
experiment.DetectorDistance_mm(100.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(400.0f).BeamY_pxl(400.0f);
|
|
experiment.ImportBraggIntegrationSettings(BraggIntegrationSettings());
|
|
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
|
|
|
|
const float cx = 600.3f, cy = 300.2f, amp = 800.0f, sigma = 1.3f;
|
|
ImagePreprocessorBuffer image(npixel);
|
|
for (size_t i = 0; i < npixel; ++i)
|
|
image[i] = 12;
|
|
for (int dy = -6; dy <= 6; ++dy)
|
|
for (int dx = -6; dx <= 6; ++dx) {
|
|
const int x = static_cast<int>(std::lround(cx)) + dx, y = static_cast<int>(std::lround(cy)) + dy;
|
|
const float ex = x - cx, ey = y - cy;
|
|
image[y * width + x] += static_cast<int32_t>(std::lround(amp * std::exp(-(ex * ex + ey * ey) / (2 * sigma * sigma))));
|
|
}
|
|
|
|
auto run = [&](float neighbour_partiality, BraggIntegrationCounts &counts) {
|
|
std::vector<Reflection> predicted;
|
|
for (int gy = -4; gy <= 4; ++gy)
|
|
for (int gx = -4; gx <= 4; ++gx) {
|
|
Reflection r{};
|
|
r.h = gx; r.k = gy; r.l = 1;
|
|
r.predicted_x = cx + 8.0f * gx;
|
|
r.predicted_y = cy + 8.0f * gy;
|
|
r.d = 2.0f;
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = (gx == 0 && gy == 0) ? 0.5f : neighbour_partiality;
|
|
predicted.push_back(r);
|
|
}
|
|
BraggIntegrationEngineCPU engine(experiment);
|
|
const auto out = engine.Run(image, predicted, predicted.size(), 0);
|
|
counts = engine.Counts();
|
|
std::vector<Reflection> spot;
|
|
for (const auto &r : out)
|
|
if (r.h == 0 && r.k == 0)
|
|
spot.push_back(r);
|
|
return spot;
|
|
};
|
|
|
|
// Tails: no reflection is dropped, and the spot is measured against the clean flat background -
|
|
// while the density the widened-radius guard reads still sees every prediction.
|
|
BraggIntegrationCounts tails;
|
|
const auto spot = run(0.01f, tails);
|
|
CHECK(tails.bkg_starved == 0);
|
|
CHECK(tails.bkg_starved_by_neighbour > 0);
|
|
REQUIRE(spot.size() == 1);
|
|
CHECK(spot[0].bkg == Catch::Approx(12.0f));
|
|
CHECK(spot[0].I == Catch::Approx(6.2831853 * sigma * sigma * amp).epsilon(0.05));
|
|
|
|
// The same predictions with their flux on this frame do take the rings, the spot's among them, and
|
|
// it is the neighbours, not the detector, that starve them - the same rings the density counted.
|
|
BraggIntegrationCounts on_frame;
|
|
CHECK(run(1.0f, on_frame).empty());
|
|
CHECK(on_frame.bkg_starved > 0);
|
|
CHECK(on_frame.bkg_starved_by_neighbour == on_frame.bkg_starved);
|
|
CHECK(on_frame.bkg_starved_by_neighbour == tails.bkg_starved_by_neighbour);
|
|
}
|