Stills keep an overload as a lower bound only where it has pixels at the limit; run-level gate removed

The lower bound counts each saturated pixel at the saturation limit, so it needs one. An observation
overloaded only by pixels unreadable on this frame (a per-frame bad-pixel flag, data value 0) was kept
as a box sum with those pixels counted as zero, which bounds nothing; it now stays overloaded and is
dropped, per observation.

With the stuck pixels masked by the pre-scan, the run-level gate (kept overloads must be enriched
coarser than 4 A) has nothing left to catch: without it every still set gives the gated result
(clipped CCD and Rayonix sets identical to the fourth decimal, photon-counting sets unchanged), so it
is removed with Reflection::overload_kept. A neighbour-brightness test per saturated pixel was tried
and not kept: after the mask it only dropped 9-20% of the Rayonix overloads and worsened R_split on
one set (0.245 -> 0.278).

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-22 17:27:44 +02:00
1 parent b7e4ea23a3
commit 5905d34cba
5 files changed
+101 -44

No files matched your search

+44
View File
@@ -307,3 +307,47 @@ TEST_CASE("BraggIntegrationEngineCPU_StillKeepsOverloadAsLowerBound", "[Integrat
CHECK(flagged[0].overloaded);
CHECK(rotation.Counts().overloads_kept == 0);
}
// A still keeps an overloaded observation as a lower bound only where it has pixels at the saturation
// limit. One overloaded by a pixel unreadable on this frame alone has nothing to count at the limit:
// it stays flagged and is dropped.
TEST_CASE("BraggIntegrationEngineCPU_StillKeepsOnlySaturatedOverloads", "[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 int32_t limit = 500, bkg = 12;
experiment.Detector().SaturationLimit(limit);
const size_t width = experiment.GetXPixelsNum(), npixel = experiment.GetPixelsNum();
ImagePreprocessorBuffer image(npixel);
for (size_t i = 0; i < npixel; ++i)
image[i] = bkg;
auto spot = [&](int cx, int cy, double amplitude) {
for (int dy = -6; dy <= 6; ++dy)
for (int dx = -6; dx <= 6; ++dx) {
const int32_t v = bkg + static_cast<int32_t>(std::lround(amplitude * std::exp(-(dx * dx + dy * dy) / (2 * 1.3 * 1.3))));
image[(cy + dy) * width + cx + dx] = v >= limit ? INT32_MAX : v;
}
};
spot(600, 300, 2000.0); // clipped: a plateau of saturated pixels in a bright spot
spot(300, 300, 40.0);
image[300 * width + 300] = INT32_MIN; // its peak unreadable on this frame, not in the mask
std::vector<Reflection> predicted;
for (const auto &[x, y] : {std::pair{600, 300}, std::pair{300, 300}}) {
Reflection r{};
r.h = x; r.k = y; r.l = 1;
r.predicted_x = x; r.predicted_y = y;
r.d = 2.0f;
r.prescaling_corr = 1.0f;
r.partiality = 1.0f;
predicted.push_back(r);
}
BraggIntegrationEngineCPU still(experiment, PixelMask(experiment));
const auto out = still.Run(image, predicted, predicted.size(), 0);
REQUIRE(out.size() == 2);
CHECK_FALSE(out[0].overloaded);
CHECK(out[1].overloaded);
CHECK(still.Counts().overloads_kept == 1);
}