beam centre: the capture's transforms run on the GPU, and the CPU stays the fallback
BeamCenterFFTScore is split the way the FFT indexer is: an engine interface with a cuFFT implementation and an fftw3f one, chosen by whether CUDA is compiled in, a device is visible and the card has room for the image. Everything that decides anything - the preparation, the masked Pearson, the shortlist and the margins - is shared, so the engines can differ only in how the four convolutions are computed, and the parity test compares two shortlists rather than two answers. The whole-detector transform was the entire added cost of the capture (4-15 s a run on CPU, all of it the transforms). On the device it is milliseconds, so the composition is now cheaper than the walk it replaced rather than dearer - which is what makes 2x2 binning, the other way out, unnecessary: full resolution is affordable and the binned capture was measured to pick a neighbouring peak on one dataset of 51. Device discipline, because the card is shared with the run's own analysis workers: the four convolution surfaces are brought back to the host and combined there, so the device holds only one real buffer and three spectra; the spectrum of the image is reused for its square; plans and buffers are created inside the call that needs them and freed when it returns; and an image that would not fit is scored on the CPU instead. Tests, none of which need a GPU or a dataset: the autoconvolution identity (an exactly symmetric image is recovered at integer and half-pixel centres, scoring exactly 1), shift equivariance, the four convolutions against brute force, the no-variance overlap that VARIANCE_FLOOR exists for, the smooth pad, GPU against CPU, and the composition - a walk seeded at the capture where the walk alone declines, the fallback to the capture alone at BEAM_CENTER_CAPTURE_SIGMA_PXL, and the beam stop being blanked out of the scored image.
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "../image_analysis/geom_refinement/BeamCenterFromBackground.h"
|
||||
#include "../common/DetectorSetup.h"
|
||||
@@ -182,3 +184,126 @@ TEST_CASE("BeamCenterFromBackground_DoesNotDependOnTheThreadCount", "[BeamCenter
|
||||
CHECK(one->beam_y_pxl == many->beam_y_pxl);
|
||||
CHECK(one->sigma_pxl == many->sigma_pxl);
|
||||
}
|
||||
|
||||
// FindBeamCenter is the walk with an FFT capture in front of it: the capture chooses the basin and
|
||||
// the walk finishes inside it and reports what it knows the answer to. The tests below are about
|
||||
// the composition - that the walk really is seeded where the capture landed, that a decline falls
|
||||
// back the way it is documented to, and that the beam stop is taken out of the image the capture
|
||||
// scores.
|
||||
|
||||
namespace {
|
||||
|
||||
// A projection that is NAN everywhere except one exactly centrosymmetric patch. The capture has an
|
||||
// unambiguous answer on it (a correlation coefficient cannot exceed 1, and the patch reaches 1 at
|
||||
// its own centre) while the ring fit has nothing at all: no ring of the 12-2.2 A band is covered
|
||||
// by a patch that small, from any start.
|
||||
std::vector<float> SymmetricPatchOnly(const DiffractionExperiment &experiment, float cx, float cy,
|
||||
int half_size) {
|
||||
const auto W = static_cast<int>(experiment.GetXPixelsNumConv());
|
||||
const auto H = static_cast<int>(experiment.GetYPixelsNumConv());
|
||||
std::vector<float> mean(static_cast<size_t>(W) * H, NAN);
|
||||
for (int y = static_cast<int>(cy) - half_size; y <= static_cast<int>(cy) + half_size; y++)
|
||||
for (int x = static_cast<int>(cx) - half_size; x <= static_cast<int>(cx) + half_size; x++) {
|
||||
// Keyed on the canonical member of the +- pair about (cx, cy), so the patch is exactly
|
||||
// its own mirror there.
|
||||
int64_t dx = std::lround(2.0f * (static_cast<float>(x) - cx));
|
||||
int64_t dy = std::lround(2.0f * (static_cast<float>(y) - cy));
|
||||
if (dy < 0 || (dy == 0 && dx < 0)) {
|
||||
dx = -dx;
|
||||
dy = -dy;
|
||||
}
|
||||
uint64_t k = static_cast<uint64_t>(dx) * 0x9e3779b97f4a7c15ULL
|
||||
+ static_cast<uint64_t>(dy) * 0xc2b2ae3d27d4eb4fULL;
|
||||
k ^= k >> 31;
|
||||
k *= 0xbf58476d1ce4e5b9ULL;
|
||||
k ^= k >> 29;
|
||||
mean[static_cast<size_t>(y) * W + x] = 100.0f + static_cast<float>(k % 4096u) * 0.25f;
|
||||
}
|
||||
return mean;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The case the composition exists for. With the centre in the file destroyed - the value a
|
||||
// beamline writes when it has nothing to write - the walk has no ring of the fitted band covered
|
||||
// from where it starts, so it declines and says nothing at all. The capture does not care how
|
||||
// wrong the file is: it scores every centre on the detector in one transform set, and the walk
|
||||
// then finishes from there with a real fit sigma, low enough that a caller would commit it.
|
||||
TEST_CASE("FindBeamCenter_AnswersWhereTheWalkDeclines", "[BeamCenter]") {
|
||||
DiffractionExperiment x = TestExperiment();
|
||||
PixelMask pixel_mask(x);
|
||||
|
||||
const DiffractionGeometry geom_true = x.GetDiffractionGeometry();
|
||||
const auto projection = SynthesiseProjection(x, pixel_mask, geom_true, 60.0f, 1.0f);
|
||||
x.BeamX_pxl(0.0f).BeamY_pxl(0.0f); // the destroyed header
|
||||
|
||||
CHECK(!FindBeamCenterFromBackground(x, pixel_mask, projection).has_value());
|
||||
|
||||
const auto composed = FindBeamCenter(x, pixel_mask, projection);
|
||||
REQUIRE(composed.has_value());
|
||||
CHECK(composed->beam_x_pxl == Catch::Approx(geom_true.GetBeamX_pxl()).margin(1.0));
|
||||
CHECK(composed->beam_y_pxl == Catch::Approx(geom_true.GetBeamY_pxl()).margin(1.0));
|
||||
CHECK(composed->sigma_pxl < 1.0f);
|
||||
}
|
||||
|
||||
// The end of the fallback chain. Where the walk has nothing to fit from either start - neither from
|
||||
// the capture nor from the centre in the file - the capture stands alone, and it is disclosed as a
|
||||
// capture: BEAM_CENTER_CAPTURE_SIGMA_PXL sits above every ceiling a caller commits geometry on, so
|
||||
// the answer reaches the consumers that only need a hypothesis to test and is refused by the two
|
||||
// that would change the geometry with it.
|
||||
TEST_CASE("FindBeamCenter_FallsBackToTheCaptureAlone", "[BeamCenter]") {
|
||||
DiffractionExperiment x = TestExperiment();
|
||||
PixelMask pixel_mask(x);
|
||||
|
||||
const float cx = 700.5f, cy = 640.0f;
|
||||
const auto projection = SymmetricPatchOnly(x, cx, cy, 90);
|
||||
|
||||
CHECK(!FindBeamCenterFromBackground(x, pixel_mask, projection).has_value());
|
||||
|
||||
const auto composed = FindBeamCenter(x, pixel_mask, projection);
|
||||
REQUIRE(composed.has_value());
|
||||
CHECK(composed->beam_x_pxl == cx);
|
||||
CHECK(composed->beam_y_pxl == cy);
|
||||
CHECK(composed->sigma_pxl == BEAM_CENTER_CAPTURE_SIGMA_PXL);
|
||||
}
|
||||
|
||||
// The beam stop is blanked out of the image the capture scores. A large one-sided opaque region is
|
||||
// centrosymmetric about ITS own centre, and where it is big enough that preference beats the
|
||||
// background's - measured on a real umbra over 9.6 % of the detector, the capture landed 48 px out
|
||||
// and masking it put it back to 1.1 px. The mask that reaches the capture already has the stop in
|
||||
// it, so this costs nothing but has to keep working.
|
||||
TEST_CASE("FindBeamCenter_BlanksTheBeamStopOutOfTheCapture", "[BeamCenter]") {
|
||||
DiffractionExperiment x = TestExperiment();
|
||||
PixelMask pixel_mask(x);
|
||||
|
||||
const DiffractionGeometry geom_true = x.GetDiffractionGeometry();
|
||||
auto projection = SynthesiseProjection(x, pixel_mask, geom_true, 60.0f, 1.0f);
|
||||
|
||||
// An umbra on one side of the beam: opaque, so the pixels under it carry no background.
|
||||
const auto W = static_cast<int>(x.GetXPixelsNumConv());
|
||||
const auto H = static_cast<int>(x.GetYPixelsNumConv());
|
||||
const float sx = geom_true.GetBeamX_pxl() + 330.0f, sy = geom_true.GetBeamY_pxl() + 70.0f;
|
||||
std::vector<uint32_t> stop(static_cast<size_t>(W) * H, 0);
|
||||
for (int y = 0; y < H; y++)
|
||||
for (int x_pxl = 0; x_pxl < W; x_pxl++)
|
||||
if (std::hypot(x_pxl - sx, y - sy) < 300.0f) {
|
||||
stop[static_cast<size_t>(y) * W + x_pxl] = 1;
|
||||
projection[static_cast<size_t>(y) * W + x_pxl] = 0.0f;
|
||||
}
|
||||
|
||||
const auto unmasked = BeamCenterFFTScore(W, H, projection);
|
||||
REQUIRE(!unmasked.point.empty());
|
||||
|
||||
pixel_mask.LoadBeamStopMask(x, stop);
|
||||
BeamCenterFFTResult capture;
|
||||
const auto composed = FindBeamCenter(x, pixel_mask, projection, 0, &capture);
|
||||
REQUIRE(composed.has_value());
|
||||
REQUIRE(!capture.point.empty());
|
||||
|
||||
const float masked_error = std::hypot(capture.point[0].beam_x_pxl - geom_true.GetBeamX_pxl(),
|
||||
capture.point[0].beam_y_pxl - geom_true.GetBeamY_pxl());
|
||||
const float unmasked_error = std::hypot(unmasked.point[0].beam_x_pxl - geom_true.GetBeamX_pxl(),
|
||||
unmasked.point[0].beam_y_pxl - geom_true.GetBeamY_pxl());
|
||||
CHECK(masked_error < 12.0f);
|
||||
CHECK(masked_error <= unmasked_error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user