// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include "ModelFFT.h" #include #include #include #include #include #include #include "gemmi/fail.hpp" #include "../common/FFTWPlannerLock.h" namespace { // The r2c plan for an (nu, nv, nw) map stored u fastest, halving w - the layout gemmi's own transform // reads and writes. 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) is, which is // how every caller uses it. fftwf_plan PlanFor(int nu, int nv, int nw) { static std::map, fftwf_plan> plans; std::lock_guard lock(FFTWPlannerMutex()); const std::array key{nu, nv, nw}; if (const auto it = plans.find(key); it != plans.end()) return it->second; float *in = fftwf_alloc_real(static_cast(nu) * nv * nw); fftwf_complex *out = fftwf_alloc_complex(static_cast(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 = (in != nullptr && out != nullptr) ? fftwf_plan_guru_dft_r2c(3, dims, 0, nullptr, in, out, FFTW_ESTIMATE) : nullptr; fftwf_free(in); fftwf_free(out); if (plan == nullptr) gemmi::fail("MapToFPhi(): no FFTW plan for the grid"); plans.emplace(key, plan); return plan; } } // namespace gemmi::FPhiGrid MapToFPhi(const gemmi::Grid &map) { if (map.axis_order == gemmi::AxisOrder::ZYX) gemmi::fail("MapToFPhi(): ZYX order is not supported"); gemmi::FPhiGrid 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(map.unit_cell.volume / map.point_count()); const fftwf_plan plan = PlanFor(map.nu, map.nv, map.nw); // 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(out[i][0] * norm, -out[i][1] * norm); fftwf_free(in); fftwf_free(out); return hkl; }