Stream receivers: saturated and error counts on the puller path are the card's

In an unsigned image the card writes saturated and error pixels with one value, so the analysis could
not tell them apart: JFJochReceiverLite reported card saturated 8, error 8 as saturated 0, error 16.
The puller already places the card's module statistics in the message, so the analysis no longer
overwrites counts a stream states. A DECTRIS message carries none and is not affected.

It is a variant of Analyze the two stream receivers that analyse call, rather than a rule inside
Analyze, because Analyze runs again on one message elsewhere (the viewer on a changed mask), where it has
to recount. Rotation forwards the message as it came, so it needs nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-07 18:18:07 +01:00
1 parent 29afe06a78
commit 4c3a7852e9
5 files changed
+53 -3

No files matched your search

+13
View File
@@ -448,6 +448,19 @@ void MXAnalysisWithoutFPGA::RebuildROI() {
roi = std::make_unique<ROIIntegrationCPU>(experiment);
}
void MXAnalysisWithoutFPGA::AnalyzeStreamImage(DataMessage &output,
AzimuthalIntegrationProfile &profile,
const SpotFindingSettings &spot_finding_settings) {
// In an unsigned image from the card saturated and error pixels have the same value: only the card knows which is which
const auto saturated_pixel_count = output.saturated_pixel_count;
const auto error_pixel_count = output.error_pixel_count;
Analyze(output, profile, spot_finding_settings);
if (saturated_pixel_count)
output.saturated_pixel_count = saturated_pixel_count;
if (error_pixel_count)
output.error_pixel_count = error_pixel_count;
}
void MXAnalysisWithoutFPGA::AnalyzeROIOnly(DataMessage &output) {
if ((output.image.GetWidth() != xpixels)
|| (output.image.GetWidth() * output.image.GetHeight() != npixels))
+2
View File
@@ -117,6 +117,8 @@ public:
MXAnalysisWithoutFPGA(const DiffractionExperiment &experiment, const AzimuthalIntegrationMapping &integration,
const PixelMask &mask, IndexAndRefine &indexer, bool enable_fused_adaptive_gpu = false);
void Analyze(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings);
// Analyze for an image from a stream: saturated and error counts the stream states stay, a DECTRIS message has none.
void AnalyzeStreamImage(DataMessage &output, AzimuthalIntegrationProfile &profile, const SpotFindingSettings &spot_finding_settings);
// Surgical ROI-only paths used when a full re-analysis is not wanted: rebuild the
// ROI engine after the ROI set changes, recompute ROIs after preprocessing a new
+1 -1
View File
@@ -342,7 +342,7 @@ void JFJochReceiverLite::DataAnalysisThread(uint32_t id) {
AzimuthalIntegrationProfile profile(*az_int_mapping);
if (analysis)
analysis->Analyze(data_msg, profile, GetSpotFindingSettings());
analysis->AnalyzeStreamImage(data_msg, profile, GetSpotFindingSettings());
auto image_end_time = std::chrono::high_resolution_clock::now();
std::chrono::duration<float> image_duration = image_end_time - image_start_time;
+1 -1
View File
@@ -330,7 +330,7 @@ void StillsGPUAnalysis::AnalysisThread() {
profile.emplace(*setup.mapping);
try {
if (analysis)
analysis->Analyze(message, *profile, settings());
analysis->AnalyzeStreamImage(message, *profile, settings());
} catch (const JFJochException &e) {
// The frame is still written, without its analysis.
logger.ErrorException(e);
+36 -1
View File
@@ -142,7 +142,7 @@ namespace {
JFJochReceiverOutput output;
};
PathResult Run(const DiffractionExperiment &x, bool puller, int64_t distinct_frames) {
PathResult Run(const DiffractionExperiment &x, bool puller, int64_t distinct_frames, bool rugnux = false) {
auto devices = Devices(x);
Logger logger("FPGAImagePullerTest");
CollectingPusher pusher;
@@ -150,6 +150,7 @@ namespace {
service.NumThreads(4);
service.SetSpotFindingSettings(Spots());
service.UseFPGAImagePuller(puller);
service.RugnuxLive(rugnux, nullptr, 2, 30);
const bool jungfrau = x.GetDetectorType() == DetectorType::JUNGFRAU;
for (int64_t f = 0; f < distinct_frames; f++) {
@@ -675,6 +676,40 @@ TEST_CASE("FPGAImagePuller_RugnuxReceiver", "[FPGAImagePuller]") {
CHECK(a == b); // the third frame in the generator's memory repeats
}
// In an unsigned image the card gives saturated and error pixels one value: the counts are the card's, on every receiver.
TEST_CASE("FPGAImagePuller_SaturatedAndErrorCountsFromTheCard", "[FPGAImagePuller]") {
auto x = Base(DetJF(2), 4, 2);
x.Conversion();
x.Compression(CompressionAlgorithm::BSHUF_LZ4).BitDepthImage(16).PixelSigned(false).FilePrefix("fpga_puller_counts");
const auto card = Run(x, false, 2);
PathResult puller;
SECTION("JFJochReceiverLite") {
puller = Run(x, true, 2);
}
SECTION("JFJochReceiverRugnux, stills") {
puller = Run(x, true, 2, true);
}
SECTION("JFJochReceiverRugnux, rotation") {
auto rotation = x;
rotation.Goniometer(GoniometerAxis("omega", 0, 0.1, Coord(-1, 0, 0), std::nullopt));
puller = Run(rotation, true, 2, true);
}
REQUIRE(card.images.size() == 4);
REQUIRE(puller.images.size() == 4);
int64_t saturated = 0, error = 0;
for (const auto &[n, image]: puller.images) {
INFO("image " << n);
CHECK(image.message.saturated_pixel_count == card.images.at(n).message.saturated_pixel_count);
CHECK(image.message.error_pixel_count == card.images.at(n).message.error_pixel_count);
saturated += image.message.saturated_pixel_count.value_or(0);
error += image.message.error_pixel_count.value_or(0);
}
CHECK(saturated == 8);
CHECK(error == 8);
}
// Rotation forwards the images unanalysed, so nothing would replace what the card's ROI map, not loaded for this run, gave.
TEST_CASE("FPGAImagePuller_NoStaleROI", "[FPGAImagePuller]") {
auto x = Base(DetJF(1), 4, 1);