// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only // Equivalence of the stored (stacked) and the assembled layout of a panel detector. // // One synthetic detector, recorded twice: as an assembled image (panels where they are on the plane, gaps // masked) and as the stacked array a panel detector stores (panels one under the other, in four // orientations, no gaps), with the DetectorModules that place the stacked panels on the plane. Both carry // the same value in every physical pixel, so anything computed on the plane must come out the same. // // [PanelLayout] cases check what is already layout-correct. The [PanelLayoutTodo] cases are hidden: they // state the equivalence each later stage has to deliver, and fail until it does. A stage that converts a // consumer retags its case [PanelLayout]: // stage 1 - dense maps: PanelLayout_AzintMapping, PanelLayout_ResolutionMask (done) // stage 2 - sparse/Bragg: PanelLayout_BraggIntegration // stage 3 - per-panel spot finding: PanelLayout_SpotsNeverCrossPanels (+ PanelLayout_SpotFinderParity on a // GPU); the raster algorithms on the plane raster: PanelLayout_AssembleToPlane, // PanelLayout_ShadowFinder, PanelLayout_BeamCenterFFT, PanelLayout_DetectorDiagnostic - each // comparing the stacked layout with the assembled image cut to the raster. #include #include #include #include #include "../common/AssembleToPlane.h" #include "../common/AzimuthalIntegrationMapping.h" #include "../common/CUDAWrapper.h" #include "../common/BraggIntegrationSettings.h" #include "../common/DetectorModules.h" #include "../common/DetectorSetup.h" #include "../common/DiffractionExperiment.h" #include "../common/PixelMask.h" #include "../common/Reflection.h" #include "../image_analysis/beam_stop/ShadowFinder.h" #include "../image_analysis/bragg_integration/BraggIntegrationEngineCPU.h" #include "../image_analysis/geom_refinement/BeamCenterFFT.h" #include "../image_analysis/image_preprocessing/ImagePreprocessorBuffer.h" #include "../image_analysis/roi/ROIIntegrationCPU.h" #include "../image_analysis/spot_finding/AdaptiveSpotFinderCPU.h" #include "../image_analysis/spot_finding/ImageSpotFinderCPU.h" #include "../image_analysis/spot_finding/SpotUtils.h" #include "../rugnux/DiagnosticOutput.h" #ifdef JFJOCH_USE_CUDA #include "../image_analysis/bragg_integration/BraggIntegrationEngineGPU.h" #include "../image_analysis/image_preprocessing/ImagePreprocessorBufferGPU.h" #include "../image_analysis/spot_finding/AdaptiveSpotFinderGPU.h" #include "../image_analysis/spot_finding/ImageSpotFinderGPU.h" #endif namespace { // One panel as stored, and the plane the four of them are placed on. constexpr int64_t PW = 96, PH = 64, NPANELS = 4; constexpr int64_t PLANE_W = 220, PLANE_H = 200; // Panel p is stored at rows [p * PH, (p + 1) * PH) and placed 2 x 2 on the plane with gaps of 8 and 16 px. DetectorModules StackedPanels() { auto panel = [](int64_t p, double ox, double oy, double fx, double fy, double sx, double sy) { DetectorModule m; m.x0 = 0; m.y0 = p * PH; m.width = PW; m.height = PH; m.ox = ox; m.oy = oy; m.fx = fx; m.fy = fy; m.sx = sx; m.sy = sy; return m; }; return DetectorModules({ panel(0, 10, 10, 1, 0, 0, 1), // as stored: x 10..105, y 10..73 panel(1, 209, 73, -1, 0, 0, -1), // turned 180: x 114..209, y 10..73 panel(2, 73, 90, 0, 1, -1, 0), // turned 90: x 10..73, y 90..185 panel(3, 120, 185, 0, -1, 1, 0) // turned -90: x 120..183, y 90..185 }); } struct Spot { double x, y; // on the plane }; // Spots well inside their panels: more than the spot finder's 15 px window from every panel edge. const std::vector INTERIOR_SPOTS = {{50.3, 40.7}, {80.5, 45.3}, {160.6, 30.2}, {40.4, 140.8}, {150.2, 130.5}}; DiffractionExperiment Experiment(int64_t width, int64_t height, const DetectorModules &modules) { DiffractionExperiment x(DetDECTRIS(width, height, "PanelLayout", "")); // Beam in the gap cross; at 30 mm the plane reaches about 2 A in its corners. x.DetectorDistance_mm(30.0f).IncidentEnergy_keV(WVL_1A_IN_KEV).BeamX_pxl(110.0f).BeamY_pxl(82.0f); x.ImportBraggIntegrationSettings(BraggIntegrationSettings()); x.Modules(modules); return x; } // The same detector, assembled and stacked. struct Scene { DetectorModules modules = StackedPanels(); DiffractionExperiment assembled = Experiment(PLANE_W, PLANE_H, {}); DiffractionExperiment stacked = Experiment(PW, NPANELS * PH, modules); ImagePreprocessorBuffer assembled_image{PLANE_W * PLANE_H}; ImagePreprocessorBuffer stacked_image{PW * NPANELS * PH}; PixelMask assembled_mask; PixelMask stacked_mask{PW, NPANELS * PH}; // Assembled pixel at the plane position of a stacked pixel (the placements are whole-pixel). static size_t AssembledIndex(double px, double py) { return static_cast(std::lround(py)) * PLANE_W + static_cast(std::lround(px)); } explicit Scene(const std::vector &spots) { auto value = [&](double px, double py) { double v = 2.0; for (const auto &s: spots) { const double dx = px - s.x, dy = py - s.y; v += 800.0 * std::exp(-(dx * dx + dy * dy) / (2.0 * 1.3 * 1.3)); } return static_cast(std::lround(v)); }; // Gaps are what the preprocessor makes of a masked pixel. for (size_t i = 0; i < assembled_image.size(); i++) assembled_image[i] = INT32_MIN; std::vector gaps(PLANE_W * PLANE_H, 1u << PixelMask::ModuleGapPixelBit); modules.ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) { stacked_image[i] = value(px, py); assembled_image[AssembledIndex(px, py)] = value(px, py); gaps[AssembledIndex(px, py)] = 0; }); assembled_mask = PixelMask(gaps); } }; std::vector FindSpots(const DiffractionExperiment &x, const ImagePreprocessorBuffer &image, float snr = 0.0f) { SpotFindingSettings settings; settings.signal_to_noise_threshold = snr; settings.photon_count_threshold = 20; settings.min_pix_per_spot = 1; settings.high_res_gap_Q_recipA = std::nullopt; ImageSpotFinderCPU finder(static_cast(x.GetXPixelsNum()), static_cast(x.GetYPixelsNum())); finder.SetPanels(x.GetModules()); finder.SetResolutionMask(std::vector(x.GetPixelsNum(), false)); DataMessage msg{}; msg.number = 0; SpotAnalyze(x, settings, finder.Run(image, settings), msg); return msg.spots; } // Every spot of `a` has one in `b` at the same plane position, and the counts agree. void CheckSameSpots(const std::vector &a, const std::vector &b) { REQUIRE(a.size() == b.size()); for (const auto &s: a) { size_t matched = 0; for (const auto &t: b) if (std::hypot(s.x - t.x, s.y - t.y) < 1e-3) { CHECK(s.intensity == t.intensity); CHECK(s.d_A == Catch::Approx(t.d_A)); matched++; } CHECK(matched == 1); } } // Integrate predictions placed on the plane: on the assembled image directly, on the stacked one through // the panel table - PlaneToPanel in, ImageToPlane of the same panel out. Keyed by h. std::map Integrate(const DiffractionExperiment &x, const PixelMask &mask, const ImagePreprocessorBuffer &image, const std::vector &spots, bool gpu = false) { const auto &modules = x.GetModules(); const auto geom = x.GetDiffractionGeometry(); std::vector predicted; std::map panel_of; for (size_t i = 0; i < spots.size(); i++) { const auto p = modules.PlaneToPanel(spots[i].x, spots[i].y); REQUIRE(p.has_value()); Reflection r{}; r.h = static_cast(i) + 1; r.k = 0; r.l = 1; r.predicted_x = static_cast(p->x); r.predicted_y = static_cast(p->y); r.d = geom.PxlToRes(static_cast(spots[i].x), static_cast(spots[i].y)); r.prescaling_corr = 1.0f; r.partiality = 1.0f; predicted.push_back(r); panel_of[r.h] = p->panel; } std::vector integrated; #ifdef JFJOCH_USE_CUDA if (gpu) { auto stream = std::make_shared(); ImagePreprocessorBufferGPU gpu_image(image.size()); for (size_t i = 0; i < image.size(); i++) gpu_image[i] = image[i]; REQUIRE(cudaMemcpyAsync(gpu_image.getGPUBuffer(), gpu_image.getBuffer().data(), image.size() * sizeof(int32_t), cudaMemcpyHostToDevice, *stream) == cudaSuccess); BraggIntegrationEngineGPU engine(x, stream, mask); integrated = engine.Run(gpu_image, predicted, predicted.size(), 0); } #endif if (!gpu) { BraggIntegrationEngineCPU engine(x, mask); integrated = engine.Run(image, predicted, predicted.size(), 0); } std::map out; for (auto r: integrated) { const int64_t panel = panel_of.at(r.h); const auto [px, py] = modules.ImageToPlane(panel, r.predicted_x, r.predicted_y); r.predicted_x = static_cast(px); r.predicted_y = static_cast(py); const auto [ox, oy] = modules.ImageToPlane(panel, r.observed_x, r.observed_y); r.observed_x = static_cast(ox); r.observed_y = static_cast(oy); out[r.h] = r; } return out; } } // namespace // The panel table itself: every stored pixel is visited once and lands on a free plane pixel, plane -> // panel inverts it, a gap is on no panel, and the plane bounds hold every panel. TEST_CASE("PanelLayout_PanelTable", "[PanelLayout]") { const DetectorModules modules = StackedPanels(); const PlaneBox box = modules.PlaneBounds(PW, NPANELS * PH); CHECK(box.x0 == 9.5); CHECK(box.y0 == 9.5); CHECK(box.x1 == 209.5); CHECK(box.y1 == 185.5); std::vector visits(PW * NPANELS * PH, 0), on_plane(PLANE_W * PLANE_H, 0); modules.ForEachPixel(PW, NPANELS * PH, [&](int64_t panel, size_t i, double px, double py) { visits[i]++; on_plane[Scene::AssembledIndex(px, py)]++; CHECK(panel == static_cast(i / (PW * PH))); const auto p = modules.PlaneToPanel(px, py); REQUIRE(p.has_value()); CHECK(p->panel == panel); CHECK(static_cast(std::lround(p->y)) * PW + static_cast(std::lround(p->x)) == i); const auto [qx, qy] = modules.PlaneToImage(panel, px, py); CHECK(qx == Catch::Approx(p->x)); CHECK(qy == Catch::Approx(p->y)); }); for (int v: visits) CHECK(v == 1); for (int v: on_plane) CHECK(v <= 1); CHECK_FALSE(modules.PlaneToPanel(110.0, 40.0).has_value()); // gap between panels 0 and 1 CHECK_FALSE(modules.PlaneToPanel(110.0, 82.0).has_value()); // gap cross, where the beam is // Empty table: the identity, the whole image one panel -1. const DetectorModules none; const auto p = none.PlaneToPanel(12.5, -3.0); REQUIRE(p.has_value()); CHECK(p->panel == -1); CHECK(p->x == 12.5); CHECK(p->y == -3.0); CHECK(none.ImageToPlane(-1, 4.0, 5.0) == std::pair{4.0, 5.0}); const PlaneBox image = none.PlaneBounds(PW, PH); CHECK(image.x0 == 0.0); CHECK(image.x1 == static_cast(PW)); size_t n = 0; none.ForEachPixel(PW, PH, [&](int64_t panel, size_t i, double px, double py) { CHECK(panel == -1); CHECK(i == n++); CHECK(px == static_cast(i % PW)); CHECK(py == static_cast(i / PW)); }); CHECK(n == static_cast(PW * PH)); } // Spot centroids: found in the image, mapped to the plane by SpotAnalyze. A count threshold on interior // spots, so the finder's neighbourhood never reaches another panel. TEST_CASE("PanelLayout_SpotPlaneCoordinates", "[PanelLayout]") { const Scene scene(INTERIOR_SPOTS); const auto a = FindSpots(scene.assembled, scene.assembled_image); const auto s = FindSpots(scene.stacked, scene.stacked_image); CHECK(a.size() == INTERIOR_SPOTS.size()); CheckSameSpots(a, s); } // Predictions go plane -> panel -> image and come back to where they were on the plane; the integrated // spot is observed where it is on the plane in both layouts. TEST_CASE("PanelLayout_IntegrationRoundTrip", "[PanelLayout]") { const Scene scene(INTERIOR_SPOTS); const auto a = Integrate(scene.assembled, scene.assembled_mask, scene.assembled_image, INTERIOR_SPOTS); const auto s = Integrate(scene.stacked, scene.stacked_mask, scene.stacked_image, INTERIOR_SPOTS); REQUIRE(a.size() == INTERIOR_SPOTS.size()); REQUIRE(s.size() == INTERIOR_SPOTS.size()); for (const auto &[h, r]: s) { const Spot &spot = INTERIOR_SPOTS[h - 1]; CHECK(r.predicted_x == Catch::Approx(spot.x).margin(1e-3)); CHECK(r.predicted_y == Catch::Approx(spot.y).margin(1e-3)); CHECK(r.observed_x == Catch::Approx(a.at(h).observed_x).margin(1e-3)); CHECK(r.observed_y == Catch::Approx(a.at(h).observed_y).margin(1e-3)); } } // The azimuthal-integration map built per stored pixel through the panel table - same bin, resolution and // correction as the assembled pixel it lands on. The same for the azimuthal ROI map, and the ROI centre of // mass comes out on the plane. TEST_CASE("PanelLayout_AzintMapping", "[PanelLayout]") { const Scene scene(INTERIOR_SPOTS); const AzimuthalIntegrationMapping a(scene.assembled, scene.assembled_mask); const AzimuthalIntegrationMapping s(scene.stacked, scene.stacked_mask); size_t bin = 0, resolution = 0, correction = 0; // pixels that differ scene.modules.ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) { const size_t j = Scene::AssembledIndex(px, py); bin += s.GetPixelToBin()[i] != a.GetPixelToBin()[j]; resolution += std::fabs(s.Resolution()[i] - a.Resolution()[j]) > 1e-5f * a.Resolution()[j]; correction += std::fabs(s.Corrections()[i] - a.Corrections()[j]) > 1e-5f * a.Corrections()[j]; }); CHECK(bin == 0); CHECK(resolution == 0); CHECK(correction == 0); DiffractionExperiment assembled = scene.assembled, stacked = scene.stacked; for (auto *x: {&assembled, &stacked}) x->ROI().SetROI(ROIDefinition{.azimuthal = {ROIAzimuthal("ring", 3.0f, 8.0f, 20.0f, 250.0f)}}); const auto a_roi = assembled.ExportROIMap(), s_roi = stacked.ExportROIMap(); size_t roi = 0, in_roi = 0; scene.modules.ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) { roi += s_roi[i] != a_roi[Scene::AssembledIndex(px, py)]; in_roi += s_roi[i] != 0; }); CHECK(roi == 0); CHECK(in_roi > 1000); std::map a_out, s_out; ROIIntegrationCPU(assembled).Run(scene.assembled_image, a_out); ROIIntegrationCPU(stacked).Run(scene.stacked_image, s_out); CHECK(s_out.at("ring").sum == a_out.at("ring").sum); CHECK(s_out.at("ring").pixels == a_out.at("ring").pixels); CHECK(s_out.at("ring").x_weighted == a_out.at("ring").x_weighted); CHECK(s_out.at("ring").y_weighted == a_out.at("ring").y_weighted); } // The spot finders' resolution mask, per stored pixel through the panel table. TEST_CASE("PanelLayout_ResolutionMask", "[PanelLayout]") { const Scene scene(INTERIOR_SPOTS); const auto a = AzimuthalIntegrationMapping(scene.assembled, scene.assembled_mask).ResolutionMaskBits(3.0f, 8.0f); const auto s = AzimuthalIntegrationMapping(scene.stacked, scene.stacked_mask).ResolutionMaskBits(3.0f, 8.0f); auto bit = [](const std::vector &v, size_t i) { return (v[i / 32] >> (i % 32)) & 1u; }; size_t differ = 0; scene.modules.ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) { differ += bit(*s, i) != bit(*a, Scene::AssembledIndex(px, py)); }); CHECK(differ == 0); } // Bragg integration on the stacked layout measures what it measures on the assembled image - stencil // direction, radial background and polarisation taken from the plane position, not the stored one. // Also with what reads the radial direction hardest: polarisation, a ring elongated by a bandwidth, the // radial background correction, the owner map, and the box sum. The interior spots moved off the exact // half pixel: there the profile grid centres on the rounded position, and rounding a half picks the other // neighbour once a panel is turned. void CheckBraggIntegration(bool gpu) { const std::vector spots = {{50.3, 40.7}, {80.4, 45.3}, {160.6, 30.2}, {40.4, 140.8}, {150.2, 130.6}}; const Scene scene(spots); auto setup = [](DiffractionExperiment x, int variant) { BraggIntegrationSettings bis = x.GetBraggIntegrationSettings(); x.PolarizationFactor(0.99f); if (variant == 1) { x.BandwidthFWHM(0.02f); bis.StencilKSigma(2.0f); } if (variant == 2) bis.BackgroundRadialCorrection(true); if (variant == 3) bis.Overlap(OverlapMode::Exclude); if (variant == 4) bis.Integrator(IntegratorMode::BoxSum); if (variant == 5) bis.Integrator(IntegratorMode::ProfileEmpirical); x.ImportBraggIntegrationSettings(bis); return x; }; for (int variant = 0; variant < 6; variant++) { CAPTURE(variant); const auto a = Integrate(setup(scene.assembled, variant), scene.assembled_mask, scene.assembled_image, spots, gpu); const auto s = Integrate(setup(scene.stacked, variant), scene.stacked_mask, scene.stacked_image, spots, gpu); REQUIRE(s.size() == a.size()); for (const auto &[h, r]: s) { CAPTURE(h); CHECK(r.I == Catch::Approx(a.at(h).I).epsilon(1e-5)); CHECK(r.sigma == Catch::Approx(a.at(h).sigma).epsilon(1e-5)); CHECK(r.bkg == Catch::Approx(a.at(h).bkg).epsilon(1e-5)); CHECK(r.image_scale_corr == Catch::Approx(a.at(h).image_scale_corr).epsilon(1e-5)); } } } TEST_CASE("PanelLayout_BraggIntegration", "[PanelLayout]") { CheckBraggIntegration(false); } // The same on the CUDA engine, stacked against assembled on the GPU. TEST_CASE("PanelLayout_BraggIntegrationGPU", "[PanelLayout][GPU]") { if (get_gpu_count() == 0) SKIP("No GPU"); CheckBraggIntegration(true); } // Spot finding per panel. Two half spots on the facing edges of panels 0 and 1 as stored - adjacent rows of // the stacked array, 139 px apart on the plane - stay two spots, and the local-background (SNR) finder // agrees with the assembled image: its windows stop at the panel edge, as they stop at the masked gap on // the assembled image (16 px there, wider than the window's 15), instead of reaching into the other spot. TEST_CASE("PanelLayout_SpotsNeverCrossPanels", "[PanelLayout]") { // Panel 0 stored row 63 and panel 1 stored row 0, both at stored column 30. const std::vector edge = {{40.0, 73.0}, {179.0, 73.0}}; const Scene scene(edge); const auto a = FindSpots(scene.assembled, scene.assembled_image); CHECK(a.size() == 2); CheckSameSpots(a, FindSpots(scene.stacked, scene.stacked_image)); const auto a_snr = FindSpots(scene.assembled, scene.assembled_image, 3.0f); CHECK(a_snr.size() == 2); CheckSameSpots(a_snr, FindSpots(scene.stacked, scene.stacked_image, 3.0f)); const Scene interior(INTERIOR_SPOTS); CheckSameSpots(FindSpots(interior.assembled, interior.assembled_image, 3.0f), FindSpots(interior.stacked, interior.stacked_image, 3.0f)); } #ifdef JFJOCH_USE_CUDA // The GPU finders (local box, adaptive, and the device connected components) work panel by panel as the CPU // ones do: on the stacked edge scene the same components, in the same order, on the same panels. TEST_CASE("PanelLayout_SpotFinderParity", "[PanelLayout]") { if (get_gpu_count() == 0) { WARN("No CUDA GPU present. Skipping PanelLayout_SpotFinderParity"); return; } const Scene scene({{40.0, 73.0}, {179.0, 73.0}, {50.3, 40.7}, {40.4, 140.8}}); const DiffractionExperiment &x = scene.stacked; const auto w = static_cast(x.GetXPixelsNum()), h = static_cast(x.GetYPixelsNum()); ImagePreprocessorBufferGPU buffer(x.GetPixelsNum()); for (size_t i = 0; i < buffer.size(); i++) buffer[i] = scene.stacked_image[i]; REQUIRE(cudaMemcpy(buffer.getGPUBuffer(), buffer.getBuffer().data(), x.GetPixelsNum() * sizeof(int32_t), cudaMemcpyHostToDevice) == cudaSuccess); REQUIRE(cudaDeviceSynchronize() == cudaSuccess); const auto same = [&](const std::vector &cpu, const std::vector &gpu) { REQUIRE(cpu.size() == gpu.size()); for (size_t i = 0; i < cpu.size(); i++) { CHECK(cpu[i].RawCoord().x == gpu[i].RawCoord().x); CHECK(cpu[i].RawCoord().y == gpu[i].RawCoord().y); CHECK(cpu[i].Count() == gpu[i].Count()); CHECK(cpu[i].Panel() == gpu[i].Panel()); const auto c = cpu[i].RawCoord(); CHECK(cpu[i].Panel() == x.GetModules().ModuleOfImagePixel(c.x, c.y)); } }; const std::vector no_mask(x.GetPixelsNum(), false); auto stream = std::make_shared(); SpotFindingSettings settings; settings.signal_to_noise_threshold = 3.0f; settings.photon_count_threshold = 20; settings.min_pix_per_spot = 1; ImageSpotFinderCPU cpu(w, h); ImageSpotFinderGPU gpu(w, h, stream); for (ImageSpotFinder *f: {static_cast(&cpu), static_cast(&gpu)}) { f->SetPanels(x.GetModules()); f->SetResolutionMask(no_mask); } const auto cpu_spots = cpu.Run(buffer, settings); CHECK(cpu_spots.size() == 4); same(cpu_spots, gpu.Run(buffer, settings)); const AzimuthalIntegrationMapping mapping(x, scene.stacked_mask); SpotFindingSettings adaptive = settings; adaptive.adaptive_threshold = true; adaptive.false_pixels_per_frame = 10.0f; AdaptiveSpotFinderCPU adaptive_cpu(mapping); AdaptiveSpotFinderGPU adaptive_gpu(mapping, stream); for (ImageSpotFinder *f: {static_cast(&adaptive_cpu), static_cast(&adaptive_gpu)}) { f->SetPanels(x.GetModules()); f->SetResolutionMask(no_mask); } const auto adaptive_spots = adaptive_cpu.Run(buffer, adaptive); CHECK(adaptive_spots.size() == 4); same(adaptive_spots, adaptive_gpu.Run(buffer, adaptive)); } #endif namespace { // The raster the stacked panels are assembled to starts at the plane origin and ends at the last panel // pixel, so it is the assembled image cut to its size: the same picture, in which the raster algorithms // must find the same answer for both layouts. template std::vector CutToRaster(const std::vector &assembled, const PlaneRaster &r) { std::vector out; for (int64_t j = 0; j < r.height; j++) for (int64_t i = 0; i < r.width; i++) out.push_back(assembled[(r.y0 + j) * PLANE_W + r.x0 + i]); return out; } PlaneRaster StackedRaster() { return PlaneRasterOf(StackedPanels(), PW, NPANELS * PH); } // A plane picture: background b, nothing inside the disk of radius r about the beam and along an arm running // right of it, and a ring of radius 60 about (cx, cy). std::vector Frame(const DetectorModules &modules, int64_t width, int64_t height, bool stacked, double r, double cx, double cy) { const auto value = [&](double px, double py) { const double dx = px - 110.0, dy = py - 82.0; if (dx * dx + dy * dy <= r * r || (dx >= 0 && std::fabs(dy) <= 4.0)) return int32_t{0}; const double ring = std::hypot(px - cx, py - cy) - 60.0; return static_cast(4 + std::lround(20.0 * std::exp(-ring * ring / 8.0))); }; std::vector out(static_cast(width * height), INT32_MIN); if (stacked) modules.ForEachPixel(width, height, [&](int64_t, size_t i, double px, double py) { out[i] = value(px, py); }); else StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t, double px, double py) { out[std::lround(py) * width + std::lround(px)] = value(px, py); }); return out; } // The assembled detector at the raster's size, gaps masked. struct RasterScene { PlaneRaster raster = StackedRaster(); DiffractionExperiment assembled = Experiment(raster.width, raster.height, {}); PixelMask assembled_mask; DiffractionExperiment stacked = Experiment(PW, NPANELS * PH, StackedPanels()); PixelMask stacked_mask{PW, NPANELS * PH}; RasterScene() { std::vector gaps(raster.width * raster.height, 1u << PixelMask::ModuleGapPixelBit); StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t, double px, double py) { gaps[std::lround(py) * raster.width + std::lround(px)] = 0; }); assembled_mask = PixelMask(gaps); } }; } // namespace // The panels put together on the plane raster are the assembled image, gaps and all; an empty table is a copy. TEST_CASE("PanelLayout_AssembleToPlane", "[PanelLayout]") { const Scene scene(INTERIOR_SPOTS); const PlaneRaster r = StackedRaster(); CHECK(r.x0 == 0); CHECK(r.y0 == 0); CHECK(r.width == 210); CHECK(r.height == 186); const std::vector stacked(scene.stacked_image.data(), scene.stacked_image.data() + scene.stacked_image.size()); const std::vector assembled(scene.assembled_image.data(), scene.assembled_image.data() + scene.assembled_image.size()); const auto plane = AssembleToPlane(scene.modules, stacked, PW, NPANELS * PH, r, INT32_MIN); CHECK(plane == CutToRaster(assembled, r)); CHECK(GatherFromPlane(scene.modules, plane, r, PW, NPANELS * PH, 0) == stacked); const DetectorModules none; const PlaneRaster image = PlaneRasterOf(none, PLANE_W, PLANE_H); CHECK(image.width == PLANE_W); CHECK(image.height == PLANE_H); CHECK(AssembleToPlane(none, assembled, PLANE_W, PLANE_H, image, 0) == assembled); CHECK(GatherFromPlane(none, assembled, image, PLANE_W, PLANE_H, 0) == assembled); } // The beam stop - a disk on the beam in the gap cross, reaching onto all four panels, and an arm across the // gap between panels 0 and 1 - is found on the plane: each stored pixel gets the mask of the assembled pixel // it is. TEST_CASE("PanelLayout_ShadowFinder", "[PanelLayout]") { const RasterScene rs; ShadowFinder assembled(rs.assembled, rs.assembled_mask); ShadowFinder stacked(rs.stacked, rs.stacked_mask); std::vector> frames; std::vector buffer; for (int f = 0; f < 12; f++) { frames.push_back(Frame({}, rs.raster.width, rs.raster.height, false, 25.0, 110.0, 82.0)); DataMessage a{}; a.image = CompressedImage(frames.back(), rs.raster.width, rs.raster.height); assembled.AddImage(a, buffer); frames.push_back(Frame(StackedPanels(), PW, NPANELS * PH, true, 25.0, 110.0, 82.0)); DataMessage s{}; s.image = CompressedImage(frames.back(), PW, NPANELS * PH); stacked.AddImage(s, buffer); } const auto a = assembled.GetMask(); const auto s = stacked.GetMask(); REQUIRE(s.size() == static_cast(PW * NPANELS * PH)); size_t shadowed = 0, differ = 0; StackedPanels().ForEachPixel(PW, NPANELS * PH, [&](int64_t, size_t i, double px, double py) { shadowed += s[i] != 0; differ += s[i] != a[std::lround(py) * rs.raster.width + std::lround(px)]; }); CHECK(shadowed > 500); CHECK(differ == 0); } // The point-symmetry capture scores the plane raster and reports plane coordinates: the same shortlist from // both layouts, led by the centre of the ring. TEST_CASE("PanelLayout_BeamCenterFFT", "[PanelLayout]") { const RasterScene rs; const auto to_float = [](const std::vector &v) { std::vector out; for (int32_t i: v) out.push_back(i == INT32_MIN ? NAN : static_cast(i)); return out; }; const auto a = FindBeamCenterFFT(rs.assembled, to_float(Frame({}, rs.raster.width, rs.raster.height, false, 0.0, 104.0, 95.0))); const auto s = FindBeamCenterFFT(rs.stacked, to_float(Frame(StackedPanels(), PW, NPANELS * PH, true, 0.0, 104.0, 95.0))); REQUIRE(!a.point.empty()); REQUIRE(s.point.size() == a.point.size()); for (size_t i = 0; i < a.point.size(); i++) { CHECK(s.point[i].beam_x_pxl == a.point[i].beam_x_pxl); CHECK(s.point[i].beam_y_pxl == a.point[i].beam_y_pxl); CHECK(s.point[i].score == a.point[i].score); } CHECK(s.margin_point == a.margin_point); } // The diagnostic picture shows the detector as it is, gaps in the gap colour: the same JPEG from both layouts. TEST_CASE("PanelLayout_DetectorDiagnostic", "[PanelLayout]") { const RasterScene rs; const auto to_float = [](const std::vector &v) { std::vector out; for (int32_t i: v) out.push_back(i == INT32_MIN ? NAN : static_cast(i)); return out; }; const auto a = RenderDetectorDiagnostic(rs.assembled, rs.assembled_mask, to_float(Frame({}, rs.raster.width, rs.raster.height, false, 25.0, 104.0, 95.0))); const auto s = RenderDetectorDiagnostic(rs.stacked, rs.stacked_mask, to_float(Frame(StackedPanels(), PW, NPANELS * PH, true, 25.0, 104.0, 95.0))); CHECK(s == a); }