Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast. * Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU). * Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors. * Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer. Reviewed-on: #90 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
638 lines
30 KiB
C++
638 lines
30 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// 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 <catch2/catch_all.hpp>
|
|
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
#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<Spot> 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<size_t>(std::lround(py)) * PLANE_W + static_cast<size_t>(std::lround(px));
|
|
}
|
|
|
|
explicit Scene(const std::vector<Spot> &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<int32_t>(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<uint32_t> 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<SpotToSave> 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<int32_t>(x.GetXPixelsNum()), static_cast<int32_t>(x.GetYPixelsNum()));
|
|
finder.SetPanels(x.GetModules());
|
|
finder.SetResolutionMask(std::vector<bool>(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<SpotToSave> &a, const std::vector<SpotToSave> &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<int, Reflection> Integrate(const DiffractionExperiment &x, const PixelMask &mask,
|
|
const ImagePreprocessorBuffer &image, const std::vector<Spot> &spots,
|
|
bool gpu = false) {
|
|
const auto &modules = x.GetModules();
|
|
const auto geom = x.GetDiffractionGeometry();
|
|
std::vector<Reflection> predicted;
|
|
std::map<int, int64_t> 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<int>(i) + 1; r.k = 0; r.l = 1;
|
|
r.predicted_x = static_cast<float>(p->x);
|
|
r.predicted_y = static_cast<float>(p->y);
|
|
r.d = geom.PxlToRes(static_cast<float>(spots[i].x), static_cast<float>(spots[i].y));
|
|
r.prescaling_corr = 1.0f;
|
|
r.partiality = 1.0f;
|
|
predicted.push_back(r);
|
|
panel_of[r.h] = p->panel;
|
|
}
|
|
std::vector<Reflection> integrated;
|
|
#ifdef JFJOCH_USE_CUDA
|
|
if (gpu) {
|
|
auto stream = std::make_shared<CudaStream>();
|
|
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<int, Reflection> 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<float>(px);
|
|
r.predicted_y = static_cast<float>(py);
|
|
const auto [ox, oy] = modules.ImageToPlane(panel, r.observed_x, r.observed_y);
|
|
r.observed_x = static_cast<float>(ox);
|
|
r.observed_y = static_cast<float>(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<int> 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<int64_t>(i / (PW * PH)));
|
|
const auto p = modules.PlaneToPanel(px, py);
|
|
REQUIRE(p.has_value());
|
|
CHECK(p->panel == panel);
|
|
CHECK(static_cast<size_t>(std::lround(p->y)) * PW + static_cast<size_t>(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<double>(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<double>(i % PW));
|
|
CHECK(py == static_cast<double>(i / PW));
|
|
});
|
|
CHECK(n == static_cast<size_t>(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<std::string, ROIMessage> 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<uint32_t> &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<Spot> 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<Spot> 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<int32_t>(x.GetXPixelsNum()), h = static_cast<int32_t>(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<DiffractionSpot> &cpu, const std::vector<DiffractionSpot> &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<bool> no_mask(x.GetPixelsNum(), false);
|
|
auto stream = std::make_shared<CudaStream>();
|
|
|
|
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<ImageSpotFinder *>(&cpu), static_cast<ImageSpotFinder *>(&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<ImageSpotFinder *>(&adaptive_cpu), static_cast<ImageSpotFinder *>(&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<class T>
|
|
std::vector<T> CutToRaster(const std::vector<T> &assembled, const PlaneRaster &r) {
|
|
std::vector<T> 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<int32_t> 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<int32_t>(4 + std::lround(20.0 * std::exp(-ring * ring / 8.0)));
|
|
};
|
|
std::vector<int32_t> out(static_cast<size_t>(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<uint32_t> 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<int32_t> stacked(scene.stacked_image.data(), scene.stacked_image.data() + scene.stacked_image.size());
|
|
const std::vector<int32_t> 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<std::vector<int32_t>> frames;
|
|
std::vector<uint8_t> 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<size_t>(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<int32_t> &v) {
|
|
std::vector<float> out;
|
|
for (int32_t i: v)
|
|
out.push_back(i == INT32_MIN ? NAN : static_cast<float>(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<int32_t> &v) {
|
|
std::vector<float> out;
|
|
for (int32_t i: v)
|
|
out.push_back(i == INT32_MIN ? NAN : static_cast<float>(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);
|
|
}
|