TRIQS/triqs_ctint unstable
A TRIQS application
Loading...
Searching...
No Matches
nfft_buf.cpp
1// Copyright (c) 2017--present, The Simons Foundation
2// This file is part of TRIQS/ctint and is licensed under the terms of GPLv3 or later.
3// SPDX-License-Identifier: GPL-3.0-or-later
4// See LICENSE in the root of this distribution for details.
5
6// Everything that needs FINUFFT or xsimd lives here rather than in nfft_buf.hpp, so that neither
7// library appears in the installed interface of this project. nfft_buf_t is explicitly
8// instantiated for all Ranks it supports at the bottom of this file.
9
10#include "./nfft_buf.hpp"
11
12#include "finufft.h"
13#include <xsimd/xsimd.hpp>
14
15#include <algorithm>
16#include <bit>
17#include <iostream>
18#include <utility>
19
20namespace triqs::utility {
21
22 namespace {
23 void check_finufft(int err) {
24 if (err > 0) NDA_RUNTIME_ERROR << "Error in FINUFFT: " << err << "\n";
25 }
26 } // namespace
27
28 namespace detail {
29
30 // Compile-time unrolled loop over [0, N): invokes f with std::integral_constant<int, I> for each
31 // I. The index is int, matching the int Rank / n_acc bounds and keeping target arithmetic signed.
32 template <int N, typename F> constexpr void static_for(F &&f) {
33 [&]<std::size_t... Is>(std::index_sequence<Is...>) { (f(std::integral_constant<int, Is>{}), ...); }(std::make_index_sequence<N>{});
34 }
35
36 // The opaque handle declared in nfft_buf.hpp: finufft_plan is itself a pointer to an
37 // incomplete type, so it needs a definition of our own to hang the deleter off.
38 struct nfft_plan {
39 finufft_plan p = nullptr;
40 };
41
42 void nfft_plan_deleter::operator()(nfft_plan *ptr) const {
43 if (ptr) {
44 if (ptr->p) finufft_destroy(ptr->p);
45 delete ptr;
46 }
47 }
48
49 } // namespace detail
50
51 // ═══════════════════════════════════════════════════════════════════════════
52 // Exponent decomposition for the direct kernels
53 //
54 // None of this depends on Rank, so it lives here as file-local free functions
55 // rather than as static members of nfft_buf_t. Used only when setting up the
56 // per-target exponent lists in the non-uniform-target constructor.
57 // ═══════════════════════════════════════════════════════════════════════════
58
59 namespace {
60
61 // For fermionic frequencies: omega_n = (2n+1) * pi / beta.
62 // Direct kernels always work with the absolute odd exponent |2n+1|.
63 constexpr unsigned long odd_exponent_abs(long n) {
64 long odd = 2 * n + 1;
65 return static_cast<unsigned long>(odd >= 0 ? odd : -odd);
66 }
67
68 constexpr bool is_prime(long x) {
69 if (x < 2) return false;
70 if (x == 2) return true;
71 if (x % 2 == 0) return false;
72 for (long i = 3; i * i <= x; i += 2)
73 if (x % i == 0) return false;
74 return true;
75 }
76
77 constexpr int max_prime_sum_terms = 8;
78 constexpr int prime_sum_precompute_size = 128;
79
80 // Tunable number of SIMD accumulators for direct kernels.
81 // `n_acc_bitwise` is used by the Rank-1 bitwise power-of-two kernel.
82 // `n_acc_prime` is used by the Rank>1 prime-sum kernel.
83 constexpr int n_acc_bitwise = 4;
84 constexpr int n_acc_prime = 4;
85 // for rank 2 large sizes 8 is better but I think finufft will be faster anyway at that point
86
87 struct prime_sum_entry_t {
88 std::array<int, max_prime_sum_terms> terms{};
89 int size = 0;
90 };
91
92 constexpr prime_sum_entry_t express_as_prime_sum_ct(long n) {
93 prime_sum_entry_t out{};
94 while (n > 0 && out.size < max_prime_sum_terms) {
95 if (n == 1) {
96 out.terms[out.size++] = 1;
97 break;
98 }
99 if (n == 2 || n == 3) {
100 out.terms[out.size++] = static_cast<int>(n);
101 break;
102 }
103 if (n == 4) {
104 out.terms[out.size++] = 2;
105 out.terms[out.size++] = 2;
106 break;
107 }
108
109 long p = n;
110 while (p > 1 && !is_prime(p)) --p;
111 out.terms[out.size++] = static_cast<int>(p);
112 n -= p;
113 }
114 return out;
115 }
116
117 // Compile-time self-check of express_as_prime_sum_ct: every decomposition must sum back to n.
118 // The table exists only to drive the static_assert below; the runtime helper recomputes.
119 constexpr auto precomputed_prime_sums = [] {
120 std::array<prime_sum_entry_t, prime_sum_precompute_size> table{};
121 for (int n = 0; n < prime_sum_precompute_size; ++n) table[n] = express_as_prime_sum_ct(n);
122 return table;
123 }();
124
125 constexpr bool check_precomputed_prime_sums() {
126 for (int n = 0; n < prime_sum_precompute_size; ++n) {
127 long sum = 0;
128 for (int i = 0; i < precomputed_prime_sums[n].size; ++i) sum += precomputed_prime_sums[n].terms[i];
129 if (sum != n) return false;
130 }
131 return true;
132 }
133
134 static_assert(check_precomputed_prime_sums(), "prime-sum precompute table is invalid");
135
136 // Helper: express n as sum of primes with repetition: n = p1 + p2 + ... + pk.
137 std::vector<int> express_as_prime_sum(long n) {
138 if (n < 1) return {};
139 auto entry = express_as_prime_sum_ct(n);
140 return {entry.terms.begin(), entry.terms.begin() + entry.size};
141 }
142
143 } // namespace
144
145 // ═══════════════════════════════════════════════════════════════════════════
146 // Construction and destruction
147 // ═══════════════════════════════════════════════════════════════════════════
148
149 template <int Rank>
150 nfft_buf_t<Rank>::nfft_buf_t(nda::array_view<dcomplex, Rank> fiw_arr_, int buf_size_, double beta_, double tol_)
151 : fiw_arr(std::move(fiw_arr_)),
152 niws(nda::stdutil::make_std_array<int64_t>(fiw_arr.shape())),
153 buf_size(buf_size_),
154 beta(beta_),
155 x_arr(Rank, buf_size),
156 fx_arr(buf_size),
157 fk_arr(fiw_arr.shape()),
158 tol(tol_) {
159
160 // Capture frequency extents from fiw_arr and check that they are even ( i.e. fermionic matsubaras )
161 for (int n : niws) {
162 if (n % 2 != 0) NDA_RUNTIME_ERROR << " dimension with uneven frequency count not allowed in NFFT Buffer \n";
163 common_factor *= (n / 2) % 2 ? -1 : 1; // Additional Minus sign for uneven Matsubara offset
164 }
165
166 // Init nfft_plan. finufft_makeplan copies what it needs from opts, so it is a local.
167 finufft_opts opts{};
168 finufft_default_opts(&opts); // set default opts (must start with this)
169 opts.nthreads = 1; // enforce single-thread
170 auto Ns = std::vector(niws.rbegin(), niws.rend()); // Reverse order for FINUFFT
171 finufft_plan raw_plan = nullptr;
172 check_finufft(finufft_makeplan(/*type =*/1, Rank, Ns.data(), /*iflag=*/1, /*n_transf =*/1, tol, &raw_plan, &opts));
173 plan.reset(new detail::nfft_plan{raw_plan});
174 }
175
176 template <int Rank>
177 nfft_buf_t<Rank>::nfft_buf_t(nda::array_view<dcomplex, 1> fiw_vec_, std::vector<std::array<mesh::matsubara_freq, Rank>> target_mf_, int buf_size_,
178 nfft_type_t type, double tol_)
179 : nfft_type(type),
180 fiw_vec(std::move(fiw_vec_)),
181 buf_size(buf_size_),
182 n_targets(static_cast<int64_t>(target_mf_.size())),
183 x_arr(Rank, buf_size_),
184 fx_arr(buf_size_),
185 tol(tol_) {
186
187 if (type == nfft_type_t::type3) {
188 // Extract frequencies from matsubara_freq for FINUFFT type 3
189 s_arr.resize(Rank, n_targets);
190 for (int r = 0; r < Rank; ++r)
191 for (int64_t d = 0; d < n_targets; ++d) s_arr(r, d) = std::imag(dcomplex(target_mf_[d][r]));
192 fk_vec.resize(n_targets);
193 finufft_opts opts{};
194 finufft_default_opts(&opts);
195 opts.nthreads = 1;
196 finufft_plan raw_plan = nullptr;
197 check_finufft(finufft_makeplan(3, Rank, nullptr, /*iflag=*/1, /*n_transf=*/1, tol, &raw_plan, &opts));
198 plan.reset(new detail::nfft_plan{raw_plan});
199
200 } else if (type == nfft_type_t::direct) {
201 // Extract integer indices and beta from matsubara_freq
202 beta = target_mf_[0][0].beta;
203 target_n.resize(Rank, n_targets);
204 for (int r = 0; r < Rank; ++r)
205 for (int64_t d = 0; d < n_targets; ++d) target_n(r, d) = target_mf_[d][r].n;
206
207 if constexpr (Rank > 1) {
208 // Rank>1: use prime-sum direct kernel.
209 std::vector<int> all_primes;
210 for (int r = 0; r < Rank; ++r) {
211 target_prime_sums[r].resize(n_targets);
212 for (int64_t d = 0; d < n_targets; ++d) {
213 auto exponent = odd_exponent_abs(target_n(r, d));
214 auto prime_list = express_as_prime_sum(static_cast<long>(exponent));
215 target_prime_sums[r][d] = prime_list;
216 for (int prime : prime_list) all_primes.push_back(prime);
217 }
218 }
219
220 std::sort(all_primes.begin(), all_primes.end());
221 all_primes.erase(std::unique(all_primes.begin(), all_primes.end()), all_primes.end());
222 primes = std::move(all_primes);
223
224 for (int r = 0; r < Rank; ++r) {
225 for (int64_t d = 0; d < n_targets; ++d) {
226 for (int &prime : target_prime_sums[r][d]) {
227 prime = static_cast<int>(std::find(primes.begin(), primes.end(), prime) - primes.begin());
228 }
229 }
230 }
231
232 for (int r = 0; r < Rank; ++r) { prime_pow_tbl[r].resize(primes.size(), buf_size_); }
233 } else {
234 // Rank-1: use bitwise power-of-two direct kernel.
235 unsigned long max_exponent = 0;
236 target_pow2_bits.resize(n_targets);
237 for (int64_t d = 0; d < n_targets; ++d) {
238 unsigned long exponent = odd_exponent_abs(target_n(0, d));
239 max_exponent = std::max(max_exponent, exponent);
240
241 std::vector<int> bits;
242 for (int k = 0; exponent > 0; ++k, exponent >>= 1) {
243 if (exponent & 1ul) bits.push_back(k);
244 }
245 target_pow2_bits[d] = std::move(bits);
246 }
247
248 num_power2_levels = std::max(1, static_cast<int>(std::bit_width(max_exponent)));
249 pow2_tbl.resize(num_power2_levels, buf_size_);
250 }
251
252 } else {
253 NDA_RUNTIME_ERROR << "nfft_buf_t: only type3 and direct supported with target frequencies\n";
254 }
255 }
256
257 template <int Rank> nfft_buf_t<Rank>::~nfft_buf_t() {
258 if (buf_counter != 0) std::cout << " WARNING: Points in NFFT Buffer lost \n";
259 // plan automatically destroyed by unique_ptr
260 }
261
262 template <int Rank> nfft_buf_t<Rank> &nfft_buf_t<Rank>::operator=(nfft_buf_t &&rhs) noexcept {
263 // Custom move assignment: array_view::operator= does deep copy,
264 // but we need to rebind views to point to the same underlying data.
265 // Leverage working move constructor via destroy + placement new.
266 if (this != &rhs) {
267 std::destroy_at(this);
268 std::construct_at(this, std::move(rhs));
269 }
270 return *this;
271 }
272
273 // ═══════════════════════════════════════════════════════════════════════════
274 // Unified Target Accumulation Template with Instruction-Level Parallelism
275 // ═══════════════════════════════════════════════════════════════════════════
276 //
277 // This template implements a sophisticated two-level parallelism strategy to maximize CPU throughput:
278 //
279 // 1. **SIMD (Single Instruction Multiple Data) Parallelism:**
280 // - Vectorizes across buffer elements (tau points)
281 // - Process 2-4 complex numbers simultaneously per instruction (hardware dependent)
282 // - Uses xsimd library for portable SIMD abstractions
283 //
284 // 2. **ILP (Instruction-Level Parallelism):**
285 // - Process n_acc independent targets simultaneously
286 // - Each target maintains its own accumulator chain
287 // - Breaks data dependencies, allowing CPU to execute multiple operations in parallel
288 // - Exploits superscalar execution and out-of-order execution in modern CPUs
289 //
290 // **Why this matters:**
291 // Without ILP, the CPU would wait for each FMA (fused multiply-add) to complete before
292 // starting the next one due to data dependencies. With n_acc=4 independent chains,
293 // the CPU can overlap execution, achieving ~4x higher throughput.
294 //
295 // **Parameters:**
296 // - n_acc: Number of independent accumulators (typically 4-8)
297 // - fx: Buffered f(tau_j) values, buf_counter entries
298 // - buf_counter: Number of buffered elements
299 // - buf_counter_simd: buf_counter floored to a multiple of the SIMD width
300 // - compute_simd_pow: Lambda computing exp(iω*τ) for SIMD-aligned buffer indices
301 // - compute_scalar_pow: Lambda computing exp(iω*τ) for scalar tail elements
302 //
303 // Nothing here depends on Rank or on any other nfft_buf_t member, so it is a file-local free
304 // function rather than a member template, keeping the declaration out of the installed header.
305 //
306 namespace {
307
308 template <int n_acc, typename SimdPowFunc, typename ScalarPowFunc>
309 [[gnu::always_inline]] inline void accumulate_targets_ilp(dcomplex const *fx, int buf_counter, int64_t buf_counter_simd, int64_t n_targets_total,
310 dcomplex *fiw_ptr, SimdPowFunc &&compute_simd_pow, ScalarPowFunc &&compute_scalar_pow) {
311 using cbatch = xsimd::batch<dcomplex>; // SIMD type for complex numbers
312 constexpr std::size_t simd_size = cbatch::size; // Typically 2-4 depending on CPU
313
314 // ─────────────────────────────────────────────────────────────────────────
315 // Helper: Single-target accumulation (used for remainder targets)
316 // ─────────────────────────────────────────────────────────────────────────
317 // Computes: fiw[d] += Σ_j f(tau_j) * exp(iω_d*tau_j)
318 auto accumulate_one = [&](int64_t d) {
319 // SIMD loop: process buffer in chunks of simd_size
320 cbatch sum_vec(dcomplex{0, 0}); // Initialize SIMD accumulator to zero
321 for (int j = 0; j < buf_counter_simd; j += simd_size) {
322 cbatch fj = cbatch::load_unaligned(fx + j); // Load f(tau_j) [simd_size elements]
323 cbatch pow = compute_simd_pow(d, j); // Compute exp(iω_d*tau_j) [vectorized]
324 sum_vec = xsimd::fma(fj, pow, sum_vec); // Fused multiply-add: sum += fj * pow
325 }
326 // Reduce SIMD vector to scalar by summing all lanes
327 dcomplex sum = xsimd::reduce_add(sum_vec);
328
329 // Scalar tail: handle remaining buffer elements that don't fit in SIMD
330 for (int j = buf_counter_simd; j < buf_counter; ++j) {
331 dcomplex pow = compute_scalar_pow(d, j);
332 sum += fx[j] * pow;
333 }
334
335 fiw_ptr[d] += sum; // Accumulate into output
336 };
337
338 // ─────────────────────────────────────────────────────────────────────────
339 // Main ILP Loop: Process n_acc targets simultaneously
340 // ─────────────────────────────────────────────────────────────────────────
341 // Round down to nearest multiple of n_acc
342 int64_t const n_targets_main = (n_targets_total / n_acc) * n_acc;
343 int64_t d = 0;
344
345 for (; d < n_targets_main; d += n_acc) {
346 // Initialize n_acc independent SIMD accumulators (one per target in this batch)
347 std::array<cbatch, n_acc> sum_vecs;
348 detail::static_for<n_acc>([&](const auto acc_idx) { sum_vecs[acc_idx] = cbatch(dcomplex{0, 0}); });
349
350 // ═══ SIMD Loop: Vectorize over buffer elements ═══
351 for (int j = 0; j < buf_counter_simd; j += simd_size) {
352 // Load f(tau_j) once - shared across all n_acc targets
353 cbatch fj = cbatch::load_unaligned(fx + j);
354
355 // Unroll over n_acc accumulators at compile time
356 // This creates n_acc independent FMA dependency chains, enabling ILP
357 // The CPU can execute these in parallel pipelines
358 detail::static_for<n_acc>([&](const auto acc_idx) {
359 cbatch pow = compute_simd_pow(d + acc_idx, j); // exp(iω_{d+k}*tau_j)
360 sum_vecs[acc_idx] = xsimd::fma(fj, pow, sum_vecs[acc_idx]); // Independent accumulation
361 });
362 }
363
364 // ═══ Reduce Phase: SIMD vectors → scalars ═══
365 std::array<dcomplex, n_acc> sums;
366 detail::static_for<n_acc>([&](const auto acc_idx) { sums[acc_idx] = xsimd::reduce_add(sum_vecs[acc_idx]); });
367
368 // ═══ Scalar Tail: Process remaining non-SIMD-aligned elements ═══
369 for (int j = buf_counter_simd; j < buf_counter; ++j) {
370 dcomplex fj = fx[j];
371 detail::static_for<n_acc>([&](const auto acc_idx) {
372 dcomplex pow = compute_scalar_pow(d + acc_idx, j);
373 sums[acc_idx] += fj * pow;
374 });
375 }
376
377 // ═══ Write-back Phase: Store results to output array ═══
378 detail::static_for<n_acc>([&](const auto acc_idx) { fiw_ptr[d + acc_idx] += sums[acc_idx]; });
379 }
380
381 // ─────────────────────────────────────────────────────────────────────────
382 // Remainder Loop: Handle final targets when n_targets % n_acc != 0
383 // ─────────────────────────────────────────────────────────────────────────
384 for (; d < n_targets_total; ++d) accumulate_one(d);
385 }
386
387 } // namespace
388
389 // ═══════════════════════════════════════════════════════════════════════════
390 // FINUFFT-backed transforms
391 // ═══════════════════════════════════════════════════════════════════════════
392
393 template <int Rank> void nfft_buf_t<Rank>::set_pts(nda::array<double, 2> *tgt) {
394 auto _ = nda::range::all;
395 auto n_tgt = tgt ? n_targets : int64_t{0};
396 auto t = [&](int r) -> double * { return tgt ? (*tgt)(r, _).data() : nullptr; };
397 if constexpr (Rank == 1)
398 check_finufft(finufft_setpts(plan->p, buf_counter, x_arr(0, _).data(), nullptr, nullptr, n_tgt, t(0), nullptr, nullptr));
399 else if constexpr (Rank == 2)
400 check_finufft(finufft_setpts(plan->p, buf_counter, x_arr(1, _).data(), x_arr(0, _).data(), nullptr, n_tgt, t(1), t(0), nullptr));
401 else // Rank == 3
402 check_finufft(finufft_setpts(plan->p, buf_counter, x_arr(2, _).data(), x_arr(1, _).data(), x_arr(0, _).data(), n_tgt, t(2), t(1), t(0)));
403 }
404
405 template <int Rank> void nfft_buf_t<Rank>::do_nfft_type1() {
406 set_pts();
407 check_finufft(finufft_execute(plan->p, fx_arr.data(), fk_arr.data()));
408
409 // Accumulate results in fiw_arr. Care to normalize results afterwards
410 for (auto idx_tpl : fiw_arr.indices()) {
411 auto idx_sum = std::apply([](auto... idx) { return (idx + ... + 0); }, idx_tpl);
412 int factor = common_factor * (idx_sum % 2 ? -1 : 1);
413 std::apply(fiw_arr, idx_tpl) += std::apply(fk_arr, idx_tpl) * factor;
414 }
415 }
416
417 template <int Rank> void nfft_buf_t<Rank>::do_nfft_type3() {
418 set_pts(&s_arr);
419 check_finufft(finufft_execute(plan->p, fx_arr.data(), fk_vec.data()));
420
421 fiw_vec += fk_vec;
422 }
423
424 // ═══════════════════════════════════════════════════════════════════════════
425 // Rank-1 Direct NUDFT: Bitwise Power-of-Two Decomposition
426 // ═══════════════════════════════════════════════════════════════════════════
427 //
428 // **Algorithm Overview:**
429 // Goal: Compute exp(iω_n*τ) for fermionic Matsubara frequencies ω_n = (2n+1)π/β
430 //
431 // **Key Mathematical Insight:**
432 // exp(iω_n*τ) = exp(i*(2n+1)*π*τ/β)
433 // = [exp(iπτ/β)]^(2n+1)
434 // = z^(2n+1)
435 // where z := exp(iπτ/β) is the "base phase factor"
436 //
437 // **Efficient Exponentiation via Binary Decomposition:**
438 // Instead of computing z^m naively (O(m) multiplications), we use binary exponentiation:
439 //
440 // 1. Express m = |2n+1| in binary: m = Σ b_k * 2^k (where b_k ∈ {0,1} are the bits)
441 // Example: m=13 = 8+4+1 = 2³ + 2² + 2⁰
442 //
443 // 2. Precompute powers-of-two: z, z², z⁴, z⁸, z¹⁶, ... via repeated squaring
444 // This takes O(log m) operations
445 //
446 // 3. Multiply only the powers corresponding to set bits:
447 // z^m = z^(2^k₁) * z^(2^k₂) * ... where k₁, k₂, ... are the bit positions
448 // Example: z¹³ = z⁸ * z⁴ * z¹
449 //
450 // **Complexity:** O(log m) multiplications instead of O(m)
451 // For typical Matsubara indices (|2n+1| ~ 1-1000), this is 10-100x faster!
452 //
453 // **Why this is optimal for Rank=1:**
454 // Binary representation is minimal - every integer has exactly one binary form.
455 // For Rank>1, we use prime-sum decomposition instead (better power sharing).
456 //
457 template <int Rank>
458 void nfft_buf_t<Rank>::do_direct_bitwise()
459 requires(Rank == 1)
460 {
461 using cbatch = xsimd::batch<dcomplex>;
462 constexpr std::size_t simd_size = cbatch::size;
463
464 double const pi_over_beta = M_PI / beta;
465 int64_t const buf_counter_simd = buf_counter & -simd_size; // Floor to SIMD alignment
466 dcomplex *fiw_ptr = fiw_vec.data(); // Output pointer
467
468 // ═══════════════════════════════════════════════════════════════════════
469 // Phase 1: Build Power-of-Two Table via Repeated Squaring
470 // ═══════════════════════════════════════════════════════════════════════
471 // Compute pow2_tbl(k, j) = z_j^(2^k) for all buffer elements j
472 // where z_j = exp(iπτ_j/β)
473
474 // ─── Step 1a: Compute z = exp(iπτ/β) for all buffer elements ───
475 // SIMD path: Process simd_size elements at once
476 for (int j = 0; j < buf_counter_simd; j += simd_size) {
477 using rbatch = xsimd::batch<double>;
478 // Compute angle: θ = πτ/β
479 rbatch theta_vec = rbatch::load_unaligned(&x_arr(0, j)) * pi_over_beta;
480 // Vectorized sincos: compute sin(θ) and cos(θ) simultaneously (hardware optimized)
481 auto [sin_vec, cos_vec] = xsimd::sincos(theta_vec);
482 // Build complex exponential: z = cos(θ) + i*sin(θ) = exp(iθ)
483 cbatch z_vec(cos_vec, sin_vec);
484 // Store z^(2^0) = z in level k=0
485 z_vec.store_unaligned(&pow2_tbl(0, j));
486 }
487 // Scalar tail: handle remaining elements that don't fit in SIMD
488 for (int j = buf_counter_simd; j < buf_counter; ++j) {
489 double const theta = pi_over_beta * x_arr(0, j);
490 pow2_tbl(0, j) = dcomplex{std::cos(theta), std::sin(theta)};
491 }
492
493 // ─── Step 1b: Repeated squaring to build higher levels ───
494 // For each level k: z^(2^k) = [z^(2^(k-1))]²
495 // Example: z² = z*z, z⁴ = z²*z², z⁸ = z⁴*z⁴, ...
496 for (int k = 1; k < num_power2_levels; ++k) {
497 // SIMD path: vectorized squaring
498 for (int j = 0; j < buf_counter_simd; j += simd_size) {
499 cbatch prev = cbatch::load_unaligned(&pow2_tbl(k - 1, j)); // Load z^(2^(k-1))
500 cbatch curr = prev * prev; // Square it
501 curr.store_unaligned(&pow2_tbl(k, j)); // Store z^(2^k)
502 }
503 // Scalar tail
504 for (int j = buf_counter_simd; j < buf_counter; ++j) {
505 dcomplex prev = pow2_tbl(k - 1, j);
506 pow2_tbl(k, j) = prev * prev;
507 }
508 }
509
510 // ═══════════════════════════════════════════════════════════════════════
511 // Phase 2: Accumulate Targets by Multiplying Powers for Set Bits
512 // ═══════════════════════════════════════════════════════════════════════
513 // For each target d with exponent m = |2n_d+1|, we precomputed the bit
514 // positions in target_pow2_bits[d]. Now we multiply those powers together.
515
516 // ─── SIMD Power Computation Lambda ───
517 auto compute_simd_pow = [&](int64_t d, int j) -> cbatch {
518 auto const &bits = target_pow2_bits[d]; // List of set bit positions in |2n_d+1|
519 bool const is_neg = target_n(0, d) < 0; // Is this a negative frequency?
520
521 // Start with identity: z^0 = 1
522 cbatch rank_pow(dcomplex{1.0, 0.0});
523
524 // Multiply powers corresponding to each set bit
525 // Example: if bits = [0, 2, 3], compute z^1 * z^4 * z^8 = z^13
526 for (int k : bits) {
527 rank_pow *= cbatch::load_unaligned(&pow2_tbl(k, j));
528 }
529
530 // For negative frequencies: exp(-iω*τ) = conj(exp(iω*τ))
531 // This uses the identity: exp(-ix) = cos(x) - i*sin(x) = conj(exp(ix))
532 return is_neg ? xsimd::conj(rank_pow) : rank_pow;
533 };
534
535 // ─── Scalar Power Computation Lambda (identical logic, non-vectorized) ───
536 auto compute_scalar_pow = [&](int64_t d, int j) -> dcomplex {
537 auto const &bits = target_pow2_bits[d];
538 bool const is_neg = target_n(0, d) < 0;
539 dcomplex rank_pow{1.0, 0.0};
540 for (int k : bits) rank_pow *= pow2_tbl(k, j);
541 return is_neg ? std::conj(rank_pow) : rank_pow;
542 };
543
544 // ─── Call unified ILP accumulation template ───
545 // This computes: fiw[d] += Σ_j f(tau_j) * exp(iω_d*tau_j) for all targets d
546 accumulate_targets_ilp<n_acc_bitwise>(fx_arr.data(), buf_counter, buf_counter_simd, n_targets, fiw_ptr, compute_simd_pow, compute_scalar_pow);
547 }
548
549 // ═══════════════════════════════════════════════════════════════════════════
550 // Rank>1 Direct NUDFT: Prime-Sum Decomposition
551 // ═══════════════════════════════════════════════════════════════════════════
552 //
553 // **Algorithm Overview:**
554 // Goal: Compute exp(iω_{n1}*τ1 + iω_{n2}*τ2 + ...) for multi-dimensional frequencies
555 // = exp(iω_{n1}*τ1) * exp(iω_{n2}*τ2) * ...
556 // = z_1^{m1} * z_2^{m2} * ...
557 // where z_r = exp(iπτ_r/β) and m_r = |2n_r+1|
558 //
559 // **Why Not Use Binary Exponentiation for Rank>1?**
560 // For Rank>1, we need to compute many distinct exponents (m_r for each rank r and target d).
561 // Binary decomposition would require storing 2^k powers for each distinct exponent,
562 // leading to excessive memory usage and poor cache behavior.
563 //
564 // **Prime-Sum Decomposition Strategy:**
565 // Every positive integer can be expressed as a sum of primes (Goldbach-style):
566 // m = p1 + p2 + ... + pk
567 // Then: z^m = z^(p1+p2+...+pk) = z^p1 * z^p2 * ... * z^pk
568 //
569 // **Key Advantages:**
570 // 1. **Power Sharing:** The set of unique primes needed across ALL targets and ranks
571 // is much smaller than the set of unique exponents. We compute each z^p once and reuse it.
572 //
573 // 2. **Small Exponents:** Primes are typically small (2, 3, 5, 7, 11, ...), making
574 // z^p fast to compute via binary exponentiation.
575 //
576 // 3. **Memory Efficiency:** Store O(#primes * Rank * buf_size) instead of
577 // O(#unique_exponents * Rank * buf_size). For many targets, #primes << #unique_exponents.
578 //
579 // **Example:**
580 // Targets with m = 13, 15, 17 in some rank:
581 // 13 = 13, 15 = 13+2, 17 = 17
582 // Unique primes: {2, 13, 17}
583 // We compute z^2, z^13, z^17 once, then:
584 // z^13 = z^13, z^15 = z^13*z^2, z^17 = z^17
585 //
586 template <int Rank> void nfft_buf_t<Rank>::do_direct_prime() {
587 using cbatch = xsimd::batch<dcomplex>;
588 constexpr std::size_t simd_size = cbatch::size;
589
590 double const pi_over_beta = M_PI / beta;
591 int64_t const buf_counter_simd = buf_counter & -simd_size;
592 dcomplex *fiw_ptr = fiw_vec.data();
593 int const num_primes = static_cast<int>(primes.size()); // # of unique primes across all targets
594
595 // ═══════════════════════════════════════════════════════════════════════
596 // Phase 1: Compute Prime Powers for Each Rank
597 // ═══════════════════════════════════════════════════════════════════════
598 // For each rank r and each unique prime p, compute:
599 // prime_pow_tbl[r](p_idx, j) = z_r^p where z_r = exp(iπτ_r/β)
600 //
601 // This is done once per rank, then reused for all targets.
602
603 // Use compile-time loop over ranks (unrolled at compile time)
604 detail::static_for<Rank>([&](const auto r) {
605 // ─── Step 1: Compute base phase factors z_r for this rank ───
606 std::vector<dcomplex> z_vals(buf_counter);
607 for (int j = 0; j < buf_counter; ++j) {
608 double const theta = pi_over_beta * x_arr(r, j); // θ = πτ_r/β
609 z_vals[j] = dcomplex{std::cos(theta), std::sin(theta)}; // z_r = exp(iθ)
610 }
611
612 // ─── Step 2: For each unique prime, compute z_r^prime ───
613 for (int p_idx = 0; p_idx < num_primes; ++p_idx) {
614 int prime = primes[p_idx];
615
616 // Special case: "prime" = 1 (treated as prime for algorithm simplicity)
617 if (prime == 1) {
618 // z^1 = z (no exponentiation needed)
619 for (int j = 0; j < buf_counter; ++j) {
620 prime_pow_tbl[r](p_idx, j) = z_vals[j];
621 }
622 continue;
623 }
624
625 // ═══ Binary Exponentiation to Compute z^prime ═══
626 // This is O(log prime) instead of O(prime) for naive multiplication
627 // Algorithm: Process bits of exponent from LSB to MSB
628 // result = 1
629 // base = z
630 // for each bit k in prime:
631 // if bit k is set: result *= base
632 // base = base^2 (square for next bit)
633
634 // SIMD path: vectorized binary exponentiation
635 for (int j = 0; j < buf_counter_simd; j += simd_size) {
636 cbatch z_vec = cbatch::load_unaligned(&z_vals[j]); // Load z
637 cbatch zp_vec(dcomplex{1.0, 0.0}); // Result accumulator (starts at 1)
638 cbatch base_vec = z_vec; // Current power of base
639 int exp = prime; // Exponent to process
640
641 // Binary exponentiation loop
642 while (exp > 0) {
643 if (exp & 1) zp_vec *= base_vec; // If current bit is set, multiply into result
644 base_vec *= base_vec; // Square base for next bit position
645 exp >>= 1; // Shift to next bit
646 }
647 zp_vec.store_unaligned(&prime_pow_tbl[r](p_idx, j));
648 }
649
650 // Scalar tail: same algorithm, non-vectorized
651 for (int j = buf_counter_simd; j < buf_counter; ++j) {
652 dcomplex zp{1.0, 0.0};
653 dcomplex base = z_vals[j];
654 int exp = prime;
655 while (exp > 0) {
656 if (exp & 1) zp *= base;
657 base *= base;
658 exp >>= 1;
659 }
660 prime_pow_tbl[r](p_idx, j) = zp;
661 }
662 }
663 });
664
665 // ═══════════════════════════════════════════════════════════════════════
666 // Phase 2: Accumulate Targets by Combining Prime Powers
667 // ═══════════════════════════════════════════════════════════════════════
668 // For each target d with multi-dimensional frequency (ω_{n1}, ω_{n2}, ...):
669 // exp(iω_{n1}*τ1 + ... + iω_{nR}*τR) = Π_r z_r^{m_r}
670 // where m_r = |2n_r+1| and z_r = exp(iπτ_r/β)
671 //
672 // Each m_r is decomposed as sum of primes: m_r = p1 + p2 + ...
673 // So: z_r^{m_r} = z_r^{p1} * z_r^{p2} * ...
674
675 // ─── SIMD Power Computation Lambda ───
676 auto compute_simd_pow = [&](int64_t d, int j) -> cbatch {
677 cbatch pow_prod; // Will accumulate product across all ranks
678
679 // Loop over each dimension/rank (compile-time unroll)
680 detail::static_for<Rank>([&](const auto r) {
681 // Start with identity for this rank
682 cbatch rank_pow(dcomplex{1.0, 0.0});
683
684 // Get list of prime indices that sum to m_r = |2n_r+1|
685 auto const &prime_indices = target_prime_sums[r][d];
686
687 // Multiply prime powers: z_r^{m_r} = z_r^{p1} * z_r^{p2} * ...
688 for (int prime_idx : prime_indices) {
689 cbatch prime_pow = cbatch::load_unaligned(&prime_pow_tbl[r](prime_idx, j));
690 rank_pow *= prime_pow;
691 }
692
693 // Handle negative frequencies via conjugation
694 rank_pow = (target_n(r, d) < 0) ? xsimd::conj(rank_pow) : rank_pow;
695
696 // Accumulate product across ranks: Π_r z_r^{m_r}
697 pow_prod = (r == 0) ? rank_pow : pow_prod * rank_pow;
698 });
699
700 return pow_prod;
701 };
702
703 // ─── Scalar Power Computation Lambda (identical logic, non-vectorized) ───
704 auto compute_scalar_pow = [&](int64_t d, int j) -> dcomplex {
705 dcomplex pow_prod{1.0, 0.0};
706 detail::static_for<Rank>([&](const auto r) {
707 dcomplex rank_pow{1.0, 0.0};
708 auto const &prime_indices = target_prime_sums[r][d];
709 for (int prime_idx : prime_indices) {
710 rank_pow *= prime_pow_tbl[r](prime_idx, j);
711 }
712 rank_pow = (target_n(r, d) < 0) ? std::conj(rank_pow) : rank_pow;
713 pow_prod *= rank_pow;
714 });
715 return pow_prod;
716 };
717
718 // ─── Call unified ILP accumulation template ───
719 accumulate_targets_ilp<n_acc_prime>(fx_arr.data(), buf_counter, buf_counter_simd, n_targets, fiw_ptr, compute_simd_pow, compute_scalar_pow);
720 }
721
722 // ═══════════════════════════════════════════════════════════════════════════
723 // Direct DFT Dispatcher: Choose Algorithm Based on Rank
724 // ═══════════════════════════════════════════════════════════════════════════
725 // Rank-1: Use bitwise power-of-two decomposition (optimal for single dimension)
726 // Rank>1: Use prime-sum decomposition (better power sharing across dimensions)
727 template <int Rank> void nfft_buf_t<Rank>::do_direct() {
728 if constexpr (Rank == 1)
729 do_direct_bitwise();
730 else
731 do_direct_prime();
732 }
733
734 // Perform NFFT transform and accumulate
735 template <int Rank> void nfft_buf_t<Rank>::do_nfft() {
736 if (nfft_type == nfft_type_t::type1)
737 do_nfft_type1();
738 else if (nfft_type == nfft_type_t::type3)
739 do_nfft_type3();
740 else
741 do_direct();
742 }
743
744 // The static_assert on Rank in nfft_buf_t makes this exhaustive.
745 template struct nfft_buf_t<1>;
746 template struct nfft_buf_t<2>;
747 template struct nfft_buf_t<3>;
748
749} // namespace triqs::utility