Files
Jungfraujoch/image_analysis/geom_refinement/BeamCenterFFTCPU.cpp
T
leonarski_f 6326c8b301 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.
2026-09-13 07:37:05 +02:00

199 lines
8.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "BeamCenterFFTCPU.h"
#include <algorithm>
#include <complex>
#include <cstring>
#include <mutex>
#include <stdexcept>
#include <fftw3.h>
namespace {
// FFTW planning is not thread-safe (execution is); same idiom as FFTIndexerCPU.
std::mutex &FftwPlanMutex() {
static std::mutex m;
return m;
}
// One real-to-complex 2D FFT workspace of fixed padded size, reused for every forward transform.
class Rfft2 {
int64_t ny, nx, nxc;
std::vector<float> real;
fftwf_plan fwd = nullptr;
fftwf_plan bwd = nullptr;
public:
std::vector<std::complex<float>> spectrum;
Rfft2(int64_t ny_, int64_t nx_) : ny(ny_), nx(nx_), nxc(nx_ / 2 + 1) {
real.resize(static_cast<size_t>(ny * nx));
spectrum.resize(static_cast<size_t>(ny * nxc));
std::unique_lock lock(FftwPlanMutex());
fwd = fftwf_plan_dft_r2c_2d(static_cast<int>(ny), static_cast<int>(nx), real.data(),
reinterpret_cast<fftwf_complex *>(spectrum.data()),
FFTW_ESTIMATE);
bwd = fftwf_plan_dft_c2r_2d(static_cast<int>(ny), static_cast<int>(nx),
reinterpret_cast<fftwf_complex *>(spectrum.data()),
real.data(), FFTW_ESTIMATE);
if (!fwd || !bwd)
throw std::runtime_error("BeamCenterFFT: fftwf plan failed");
}
Rfft2(const Rfft2 &) = delete;
Rfft2 &operator=(const Rfft2 &) = delete;
~Rfft2() {
std::unique_lock lock(FftwPlanMutex());
if (fwd)
fftwf_destroy_plan(fwd);
if (bwd)
fftwf_destroy_plan(bwd);
}
// Zero-pad `src` (height x width) into the workspace and return its spectrum.
std::vector<std::complex<float>> Forward(const std::vector<float> &src, int64_t height,
int64_t width) {
std::fill(real.begin(), real.end(), 0.0f);
for (int64_t y = 0; y < height; y++)
std::memcpy(&real[static_cast<size_t>(y * nx)], &src[static_cast<size_t>(y * width)],
sizeof(float) * static_cast<size_t>(width));
fftwf_execute(fwd);
return spectrum; // copy: the workspace is reused
}
// Inverse-transform the elementwise product u*v, normalised, cropped to out_h x out_w.
std::vector<float> InverseProduct(const std::vector<std::complex<float>> &u,
const std::vector<std::complex<float>> &v, int64_t out_h,
int64_t out_w) {
const float norm = 1.0f / (static_cast<float>(ny) * static_cast<float>(nx));
for (size_t i = 0; i < spectrum.size(); i++)
spectrum[i] = u[i] * v[i];
fftwf_execute(bwd);
std::vector<float> out(static_cast<size_t>(out_h * out_w));
for (int64_t y = 0; y < out_h; y++)
for (int64_t x = 0; x < out_w; x++)
out[static_cast<size_t>(y * out_w + x)] =
real[static_cast<size_t>(y * nx + x)] * norm;
return out;
}
};
std::vector<float> Transpose(const std::vector<float> &src, int64_t h, int64_t w) {
std::vector<float> out(src.size());
for (int64_t y = 0; y < h; y++)
for (int64_t x = 0; x < w; x++)
out[static_cast<size_t>(x * h + y)] = src[static_cast<size_t>(y * w + x)];
return out;
}
} // namespace
BeamCenterConvSurfaces2D BeamCenterFFTCPU::PointSurfaces(const std::vector<float> &a,
const std::vector<float> &m, int64_t h,
int64_t w) {
Rfft2 fft(BeamCenterFFTPadSize(2 * h), BeamCenterFFTPadSize(2 * w));
const auto A = fft.Forward(a, h, w);
const auto M = fft.Forward(m, h, w);
std::vector<float> a2(a.size());
for (size_t i = 0; i < a2.size(); i++)
a2[i] = a[i] * a[i];
const auto A2 = fft.Forward(a2, h, w);
BeamCenterConvSurfaces2D out;
out.C = fft.InverseProduct(A, A, 2 * h, 2 * w);
out.S = fft.InverseProduct(A, M, 2 * h, 2 * w);
out.Q = fft.InverseProduct(A2, M, 2 * h, 2 * w);
out.D = fft.InverseProduct(M, M, 2 * h, 2 * w);
return out;
}
// Each sequence is correlated with its own mirror and the four accumulators are summed over the
// other coordinate in double complex (the same cancellation argument as the 2D surface), so only
// four inverse transforms are needed however many sequences there are.
BeamCenterConvSurfaces1D BeamCenterFFTCPU::LineSurfaces(const std::vector<float> &a_in,
const std::vector<float> &m_in, int64_t h,
int64_t w, BeamCenterMirror mirror) {
// Mirroring the column coordinate is the same computation on the transposed image.
const std::vector<float> at = mirror == BeamCenterMirror::Rows ? std::vector<float>()
: Transpose(a_in, h, w);
const std::vector<float> mt = mirror == BeamCenterMirror::Rows ? std::vector<float>()
: Transpose(m_in, h, w);
const std::vector<float> &a = mirror == BeamCenterMirror::Rows ? a_in : at;
const std::vector<float> &m = mirror == BeamCenterMirror::Rows ? m_in : mt;
const int64_t nseq = mirror == BeamCenterMirror::Rows ? h : w;
const int64_t nbatch = mirror == BeamCenterMirror::Rows ? w : h;
const int64_t nfft = BeamCenterFFTPadSize(2 * nseq);
const int64_t nc = nfft / 2 + 1;
std::vector<float> in(static_cast<size_t>(nfft));
std::vector<std::complex<float>> A(static_cast<size_t>(nc)), M(static_cast<size_t>(nc)),
A2(static_cast<size_t>(nc));
std::vector<std::complex<double>> accC(static_cast<size_t>(nc)), accS(static_cast<size_t>(nc)),
accQ(static_cast<size_t>(nc)), accD(static_cast<size_t>(nc));
fftwf_plan fwd, bwd;
{
std::unique_lock lock(FftwPlanMutex());
fwd = fftwf_plan_dft_r2c_1d(static_cast<int>(nfft), in.data(),
reinterpret_cast<fftwf_complex *>(A.data()), FFTW_ESTIMATE);
bwd = fftwf_plan_dft_c2r_1d(static_cast<int>(nfft),
reinterpret_cast<fftwf_complex *>(A.data()), in.data(),
FFTW_ESTIMATE);
}
if (!fwd || !bwd)
throw std::runtime_error("BeamCenterFFT: fftwf 1D plan failed");
auto forward_into = [&](auto value_of, std::vector<std::complex<float>> &dst) {
for (int64_t y = 0; y < nseq; y++)
in[static_cast<size_t>(y)] = value_of(y);
std::fill(in.begin() + nseq, in.end(), 0.0f);
fftwf_execute_dft_r2c(fwd, in.data(), reinterpret_cast<fftwf_complex *>(A.data()));
dst = A;
};
for (int64_t x = 0; x < nbatch; x++) {
forward_into([&](int64_t y) { return a[static_cast<size_t>(y * nbatch + x)]; }, A2);
std::swap(A, A2); // A = spectrum of the sequence of a
std::vector<std::complex<float>> Acol = A;
forward_into([&](int64_t y) { return m[static_cast<size_t>(y * nbatch + x)]; }, M);
forward_into(
[&](int64_t y) {
const float v = a[static_cast<size_t>(y * nbatch + x)];
return v * v;
},
A2);
for (int64_t i = 0; i < nc; i++) {
const std::complex<double> ca(Acol[i]), cm(M[i]), ca2(A2[i]);
accC[i] += ca * ca;
accS[i] += ca * cm;
accQ[i] += ca2 * cm;
accD[i] += cm * cm;
}
}
const double norm = 1.0 / static_cast<double>(nfft);
auto inverse = [&](const std::vector<std::complex<double>> &acc) {
for (int64_t i = 0; i < nc; i++)
A[i] = std::complex<float>(acc[i]);
fftwf_execute_dft_c2r(bwd, reinterpret_cast<fftwf_complex *>(A.data()), in.data());
std::vector<double> out(static_cast<size_t>(2 * nseq));
for (int64_t i = 0; i < 2 * nseq; i++)
out[static_cast<size_t>(i)] = in[static_cast<size_t>(i)] * norm;
return out;
};
BeamCenterConvSurfaces1D out;
out.C = inverse(accC);
out.S = inverse(accS);
out.Q = inverse(accQ);
out.D = inverse(accD);
{
std::unique_lock lock(FftwPlanMutex());
fftwf_destroy_plan(fwd);
fftwf_destroy_plan(bwd);
}
return out;
}