Files
Jungfraujoch/rugnux/ModelFFT.cpp
T
leonarski_f 6dfe065365
Build Packages / Create release (push) Successful in 16s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m27s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m15s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m11s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m6s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m44s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m1s
Build Packages / build:windows:nocuda (push) Successful in 17m29s
Build Packages / build:windows:cuda (push) Successful in 19m58s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m7s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m8s
Build Packages / build:rugnux:windows (push) Successful in 10m58s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m46s
Build Packages / Generate python client (push) Successful in 53s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m13s
Build Packages / Build documentation (push) Successful in 1m36s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m57s
Build Packages / build:rpm (rocky8) (push) Successful in 18m7s
Build Packages / build:rpm (rocky9) (push) Successful in 18m54s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 19m32s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 17m30s
Build Packages / Unit tests (push) Successful in 1h39m2s
v1.0.0-rc.172 (#82)
* Fixed `jfjoch_broker` cancelling every data collection with a CUDA "out of memory" error after long operation: GPU memory no longer leaks with each collection.
* Rugnux scales a rotation sweep until the per-frame scales settle instead of for a fixed three rounds, and says so when they did not - merged intensities, and the space group, resolution cut and frame rejection read off them, change accordingly; `--scaling-iterations` is now the cap on that loop (default 100).
* Rugnux places every frame of a marCCD, SMV or miniCBF series at the spindle angle its own header states, so a series with missing frames, or with angles written modulo 360, is no longer read at the wrong geometry or refused.
* Every rotation run writes two diagnostic files beside its reflections: `<prefix>_detector.jpg`, the detector projection with the pixel mask and the detected beam-stop shadow drawn on it, and `<prefix>_plot.txt`, one row per image.

Reviewed-on: #82
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-22 06:48:37 +02:00

113 lines
4.6 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ModelFFT.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <complex>
#include <map>
#include <mutex>
#include <fftw3.h>
#include "gemmi/fail.hpp"
#include "../common/FFTWPlannerLock.h"
namespace {
// The r2c plan (or, inverse, the c2r plan) for an (nu, nv, nw) map stored u fastest, halving w - the
// layout gemmi's own transforms read and write. FFTW's planner is not thread-safe, so plans are made
// under the process-wide planner lock (which also guards this cache); executing one on new arrays
// (fftwf_execute_dft_r2c / _c2r) is, which is how every caller uses it.
fftwf_plan PlanFor(int nu, int nv, int nw, bool inverse) {
static std::map<std::array<int, 4>, fftwf_plan> plans;
std::lock_guard lock(FFTWPlannerMutex());
const std::array<int, 4> key{nu, nv, nw, inverse};
if (const auto it = plans.find(key); it != plans.end())
return it->second;
float *real = fftwf_alloc_real(static_cast<size_t>(nu) * nv * nw);
fftwf_complex *cplx = fftwf_alloc_complex(static_cast<size_t>(nu) * nv * (nw / 2 + 1));
// The last dimension is the one r2c halves; the strides put u fastest on both sides.
fftwf_iodim dims[3] = {{nu, 1, 1}, {nv, nu, nu}, {nw, nu * nv, nu * nv}};
fftwf_plan plan = nullptr;
if (real != nullptr && cplx != nullptr)
plan = inverse ? fftwf_plan_guru_dft_c2r(3, dims, 0, nullptr, cplx, real, FFTW_ESTIMATE)
: fftwf_plan_guru_dft_r2c(3, dims, 0, nullptr, real, cplx, FFTW_ESTIMATE);
fftwf_free(real);
fftwf_free(cplx);
if (plan == nullptr)
gemmi::fail("ModelFFT: no FFTW plan for the grid");
plans.emplace(key, plan);
return plan;
}
} // namespace
gemmi::FPhiGrid<float> MapToFPhi(const gemmi::Grid<float> &map) {
if (map.axis_order == gemmi::AxisOrder::ZYX)
gemmi::fail("MapToFPhi(): ZYX order is not supported");
gemmi::FPhiGrid<float> hkl;
hkl.unit_cell = map.unit_cell;
hkl.spacegroup = map.spacegroup;
hkl.axis_order = map.axis_order;
hkl.half_l = true;
hkl.set_size_without_checking(map.nu, map.nv, map.nw / 2 + 1);
const float norm = static_cast<float>(map.unit_cell.volume / map.point_count());
const fftwf_plan plan = PlanFor(map.nu, map.nv, map.nw, false);
// Buffers from fftwf_malloc: a plan may only be executed on arrays aligned as the ones it was made
// on were, which the vectors' own allocations do not promise.
float *in = fftwf_alloc_real(map.data.size());
fftwf_complex *out = fftwf_alloc_complex(hkl.data.size());
if (in == nullptr || out == nullptr) {
fftwf_free(in);
fftwf_free(out);
gemmi::fail("MapToFPhi(): cannot allocate the FFT buffers");
}
std::copy(map.data.begin(), map.data.end(), in);
fftwf_execute_dft_r2c(plan, in, out);
// Conjugated, as gemmi does: FFTW's forward transform carries exp(-2 pi i h.x), and a structure
// factor is the sum over exp(+2 pi i h.x).
for (size_t i = 0; i < hkl.data.size(); ++i)
hkl.data[i] = std::complex<float>(out[i][0] * norm, -out[i][1] * norm);
fftwf_free(in);
fftwf_free(out);
return hkl;
}
gemmi::Grid<float> MapFromFPhi(const gemmi::FPhiGrid<float> &hkl) {
if (hkl.axis_order == gemmi::AxisOrder::ZYX || !hkl.half_l)
gemmi::fail("MapFromFPhi(): only an XYZ half-l grid is supported");
gemmi::Grid<float> map;
map.spacegroup = hkl.spacegroup;
map.unit_cell = hkl.unit_cell;
map.set_size(hkl.nu, hkl.nv, 2 * (hkl.nw - 1));
map.axis_order = hkl.axis_order;
const float norm = static_cast<float>(1.0 / hkl.unit_cell.volume);
const fftwf_plan plan = PlanFor(map.nu, map.nv, map.nw, true);
fftwf_complex *in = fftwf_alloc_complex(hkl.data.size());
float *out = fftwf_alloc_real(map.data.size());
if (in == nullptr || out == nullptr) {
fftwf_free(in);
fftwf_free(out);
gemmi::fail("MapFromFPhi(): cannot allocate the FFT buffers");
}
// Conjugated, as gemmi does: rho(x) = 1/V sum F exp(-2 pi i h.x), and FFTW's backward transform
// carries exp(+2 pi i h.x). A missing coefficient (NaN) counts as zero, also as gemmi does.
for (size_t i = 0; i < hkl.data.size(); ++i) {
const std::complex<float> x = hkl.data[i];
in[i][0] = std::isnan(x.imag()) ? 0.0f : x.real();
in[i][1] = std::isnan(x.imag()) ? 0.0f : -x.imag();
}
fftwf_execute_dft_c2r(plan, in, out);
for (size_t i = 0; i < map.data.size(); ++i)
map.data[i] = out[i] * norm;
fftwf_free(in);
fftwf_free(out);
return map;
}