From df861de7fbc33b480a22ec510043a06346262ec0 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 6 Nov 2026 16:32:14 +0100 Subject: [PATCH] Live stills: the q range stays fixed when the running refinement moves the geometry An azimuthal q range with no high limit reaches the detector's edge, which depends on the distance. When the running geometry refinement moved the distance, the rebuilt mapping could have a different number of bins from the start message and the run's profile. Adding the next frame's profile then threw "Error combining two radial integration profiles" and cancelled the collection. The q range is now frozen at its current value when the geometry moves. The replay tool adds up the frames' profiles as the receiver does, so this path is exercised there; it ignored them before. Co-Authored-By: Claude Opus 5.5 --- receiver/StillsGPUAnalysis.cpp | 11 +++++++++-- receiver/StillsGPUAnalysis.h | 4 ++++ tests/StillsGPUAnalysisTest.cpp | 27 +++++++++++++++++++++++++++ tools/jfjoch_stills_gpu_replay.cpp | 12 +++++++++++- 4 files changed, 51 insertions(+), 3 deletions(-) diff --git a/receiver/StillsGPUAnalysis.cpp b/receiver/StillsGPUAnalysis.cpp index e5ccebe65..9eac35441 100644 --- a/receiver/StillsGPUAnalysis.cpp +++ b/receiver/StillsGPUAnalysis.cpp @@ -337,6 +337,13 @@ void StillsGPUAnalysis::AnalysisThread() { } } +void MoveStillsGeometry(DiffractionExperiment &experiment, float beam_x_pxl, float beam_y_pxl, float distance_mm) { + AzimuthalIntegrationSettings azint = experiment.GetAzimuthalIntegrationSettings(); + azint.QRange_recipA(azint.GetLowQ_recipA(), azint.GetHighQ_recipA()); + experiment.ImportAzimuthalIntegrationSettings(azint); + experiment.BeamX_pxl(beam_x_pxl).BeamY_pxl(beam_y_pxl).DetectorDistance_mm(distance_mm); +} + void StillsGPUAnalysis::SwitchGeometry() { // One fit, in the analysis thread that offered the last frame it needed; the others go on meanwhile. const auto t0 = std::chrono::steady_clock::now(); @@ -361,8 +368,8 @@ void StillsGPUAnalysis::SwitchGeometry() { } { std::unique_lock ul(geometry_mutex); - setup.experiment.BeamX_pxl(static_cast(r->beam_x_px)).BeamY_pxl(static_cast(r->beam_y_px)) - .DetectorDistance_mm(static_cast(r->distance_mm)); + MoveStillsGeometry(setup.experiment, static_cast(r->beam_x_px), static_cast(r->beam_y_px), + static_cast(r->distance_mm)); retired_mappings.push_back(std::move(setup.mapping)); setup.mapping = std::make_unique(setup.experiment, setup.mask); ++geometry_epoch; diff --git a/receiver/StillsGPUAnalysis.h b/receiver/StillsGPUAnalysis.h index d6350127f..d37b8148c 100644 --- a/receiver/StillsGPUAnalysis.h +++ b/receiver/StillsGPUAnalysis.h @@ -51,6 +51,10 @@ // switches to that geometry as each of GEOMETRY_ROUNDS fits is made. The frames analysed before the last // switch are not merged (their reflections were predicted at an earlier geometry; the images are gone); where // no fit can be made they are merged as analysed. +// Moves the analysis to a refined geometry during the run. The q range stays where it was, also one left to +// reach the detector's edge: the start message and the run's profile are binned on it. +void MoveStillsGeometry(DiffractionExperiment &experiment, float beam_x_pxl, float beam_y_pxl, float distance_mm); + class StillsGPUAnalysis { public: // Enough frames for the distance to be refined at a serial hit rate: of 100 frames of a collection indexed diff --git a/tests/StillsGPUAnalysisTest.cpp b/tests/StillsGPUAnalysisTest.cpp index ef803a2e7..b890ac222 100644 --- a/tests/StillsGPUAnalysisTest.cpp +++ b/tests/StillsGPUAnalysisTest.cpp @@ -125,3 +125,30 @@ TEST_CASE("StillsGPUAnalysis_FailingFrameDoesNotStopAnalysis", "[StillsGPUAnalys CHECK(s.steps.back().duration_s); CHECK(buffer.GetStatus().preparation_slots == 0); } + +TEST_CASE("StillsGPUAnalysis_MovedGeometryKeepsTheProfileBins", "[StillsGPUAnalysis]") { + DiffractionExperiment x = StillsExperiment(); + const PixelMask mask(x); + const AzimuthalIntegrationMapping before(x, mask); + AzimuthalIntegrationProfile run_profile(before); + + // Left to itself the high q follows the detector's edge, and with it the number of bins. + DiffractionExperiment unfrozen = x; + unfrozen.DetectorDistance_mm(120); + REQUIRE(AzimuthalIntegrationMapping(unfrozen, mask).GetBinNumber() != before.GetBinNumber()); + + MoveStillsGeometry(x, 510, 260, 120); + CHECK(x.GetDetectorDistance_mm() == Catch::Approx(120)); + CHECK(x.GetBeamX_pxl() == Catch::Approx(510)); + CHECK(x.GetBeamY_pxl() == Catch::Approx(260)); + + const AzimuthalIntegrationMapping after(x, mask); + CHECK(after.GetBinNumber() == before.GetBinNumber()); + CHECK(after.GetBinToQ() == before.GetBinToQ()); + const AzimuthalIntegrationProfile frame_profile(after); + CHECK_NOTHROW(run_profile += frame_profile); + + // A second move stays on the same bins. + MoveStillsGeometry(x, 505, 255, 90); + CHECK(AzimuthalIntegrationMapping(x, mask).GetBinToQ() == before.GetBinToQ()); +} diff --git a/tools/jfjoch_stills_gpu_replay.cpp b/tools/jfjoch_stills_gpu_replay.cpp index 722da95d9..0e4c723df 100644 --- a/tools/jfjoch_stills_gpu_replay.cpp +++ b/tools/jfjoch_stills_gpu_replay.cpp @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "../common/ImageBuffer.h" #include "../common/Logger.h" @@ -142,9 +144,12 @@ int main(int argc, char **argv) { const auto t0 = std::chrono::steady_clock::now(); { // As the receiver: half the buffer for the frames waiting, the other half for those sent. + std::mutex run_profile_mutex; + std::unique_ptr run_profile; StillsGPUAnalysis gpu(x, mask, start, &pool, nthreads, buffer.GetStatus().total_slots / 2, logger, [&](const StillsGPUAnalysis::Setup &s) { t_ready = std::chrono::steady_clock::now(); + run_profile = std::make_unique(*s.mapping); const auto &m = s.mask.GetMask(); std::ofstream f(prefix + "_mask.u32", std::ios::binary); f.write(reinterpret_cast(m.data()), m.size() * sizeof(uint32_t)); @@ -152,7 +157,12 @@ int main(int argc, char **argv) { std::chrono::duration(t_ready - t0).count(), s.experiment.GetBeamX_pxl(), s.experiment.GetBeamY_pxl()); }, - [&](DataMessage &m, const AzimuthalIntegrationProfile &) { + [&](DataMessage &m, const AzimuthalIntegrationProfile &profile) { + { + // As the receiver's plots do: the frames' profiles add up over the run. + std::unique_lock ul(run_profile_mutex); + *run_profile += profile; + } ++analysed; spots += static_cast(m.spots.size()); processing_s += m.processing_time_s.value_or(0.0f);