Files
Jungfraujoch/rugnux/ModelFFT.cpp
T
leonarski_fandClaude Opus 5 440e19343c rugnux --model: MapToFPhi returns the conjugate, as gemmi's transform does
gemmi::transform_map_to_f_phi conjugates the forward FFT at the end (a
structure factor is the sum over exp(+2 pi i h.x), a forward FFT the sum over
exp(-2 pi i h.x)); MapToFPhi did not, so every model-path Fcalc and Fmask came
out with its phase negated. Amplitudes, and so the R-factors and the scale,
were unaffected; the phases of the maps and everything read off them (map
coefficients, density at atom centres) were not. Caught by
ModelValidation_MapToFPhiMatchesGemmi, which now passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
2026-09-20 18:45:03 +02:00

78 lines
3.0 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 <complex>
#include <map>
#include <mutex>
#include <fftw3.h>
#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<std::array<int, 3>, fftwf_plan> plans;
std::lock_guard lock(FFTWPlannerMutex());
const std::array<int, 3> key{nu, nv, nw};
if (const auto it = plans.find(key); it != plans.end())
return it->second;
float *in = fftwf_alloc_real(static_cast<size_t>(nu) * nv * nw);
fftwf_complex *out = 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 = (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<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);
// 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;
}