TRIQS/triqs_ctint 4.0.0
A TRIQS application
Loading...
Searching...
No Matches
nfft_buf.hpp
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#pragma once
7#include <nda/nda.hpp>
8#include <algorithm>
9#include <array>
10#include <cmath>
11#include <memory>
12#include <vector>
13#include <bit>
14#include <triqs/mesh/matsubara_freq.hpp>
15
16#include "finufft.h"
17#include <xsimd/xsimd.hpp>
18#include <poet/poet.hpp>
19
20namespace triqs::utility {
21
22 inline void check_finufft(int err) {
23 if (err > 0) NDA_RUNTIME_ERROR << "Error in FINUFFT: " << err << "\n";
24 }
25
26 // RAII wrapper for finufft_plan (which is finufft_plan_s*)
27 using finufft_plan_ptr = std::unique_ptr<finufft_plan_s, decltype([](finufft_plan p) { if (p) finufft_destroy(p); })>;
28
29 using nda::array_view;
30 using dcomplex = std::complex<double>;
31
32 enum class nfft_type_t { type1, type3, direct };
33
34 // Helper to convert vector<T> to vector<array<T, 1>> for Rank=1 convenience constructor
35 inline std::vector<std::array<mesh::matsubara_freq, 1>> to_array_vector(std::vector<mesh::matsubara_freq> const &v) {
36 std::vector<std::array<mesh::matsubara_freq, 1>> result;
37 result.reserve(v.size());
38 for (auto const &mf : v) result.push_back({mf});
39 return result;
40 }
41
42 template <int Rank> struct nfft_buf_t {
43
44 static_assert(Rank >= 1 and Rank <= 3, "nfft_buf_t only supports Rank 1, 2, and 3");
45
47 nfft_buf_t() = default;
48
62 nfft_buf_t(array_view<dcomplex, Rank> fiw_arr_, int buf_size_, double beta_, double tol_ = 1e-15)
63 : fiw_arr(std::move(fiw_arr_)),
64 niws(nda::stdutil::make_std_array<int64_t>(fiw_arr.shape())),
65 buf_size(buf_size_),
66 beta(beta_),
67 x_arr(Rank, buf_size),
68 fx_arr(buf_size),
69 fk_arr(fiw_arr.shape()),
70 tol(tol_) {
71
72 // Capture frequency extents from fiw_arr and check that they are even ( i.e. fermionic matsubaras )
73 for (int n : niws) {
74 if (n % 2 != 0) NDA_RUNTIME_ERROR << " dimension with uneven frequency count not allowed in NFFT Buffer \n";
75 common_factor *= (n / 2) % 2 ? -1 : 1; // Additional Minus sign for uneven Matsubara offset
76 }
77
78 // Init nfft_plan
79 finufft_default_opts(&opts); // set default opts (must start with this)
80 opts.nthreads = 1; // enforce single-thread
81 auto Ns = std::vector(niws.rbegin(), niws.rend()); // Reverse order for FINUFFT
82 finufft_plan raw_plan = nullptr;
83 check_finufft(finufft_makeplan(/*type =*/1, Rank, Ns.data(), /*iflag=*/1, /*n_transf =*/1, tol, &raw_plan, &opts));
84 plan.reset(raw_plan);
85 }
86
105 nfft_buf_t(nda::array_view<dcomplex, 1> fiw_vec_, std::vector<std::array<mesh::matsubara_freq, Rank>> target_mf_, int buf_size_, nfft_type_t type,
106 double tol_ = 1e-13)
107 : nfft_type(type),
108 fiw_vec(std::move(fiw_vec_)),
109 buf_size(buf_size_),
110 n_targets(static_cast<int64_t>(target_mf_.size())),
111 x_arr(Rank, buf_size_),
112 fx_arr(buf_size_),
113 tol(tol_) {
114
115 if (type == nfft_type_t::type3) {
116 // Extract frequencies from matsubara_freq for FINUFFT type 3
117 s_arr.resize(Rank, n_targets);
118 for (int r = 0; r < Rank; ++r)
119 for (int64_t d = 0; d < n_targets; ++d) s_arr(r, d) = std::imag(dcomplex(target_mf_[d][r]));
120 fk_vec.resize(n_targets);
121 finufft_default_opts(&opts);
122 opts.nthreads = 1;
123 finufft_plan raw_plan = nullptr;
124 check_finufft(finufft_makeplan(3, Rank, nullptr, /*iflag=*/1, /*n_transf=*/1, tol, &raw_plan, &opts));
125 plan.reset(raw_plan);
126
127 } else if (type == nfft_type_t::direct) {
128 // Extract integer indices and beta from matsubara_freq
129 beta = target_mf_[0][0].beta;
130 target_n.resize(Rank, n_targets);
131 for (int r = 0; r < Rank; ++r)
132 for (int64_t d = 0; d < n_targets; ++d) target_n(r, d) = target_mf_[d][r].n;
133
134 if constexpr (Rank > 1) {
135 // Rank>1: use prime-sum direct kernel.
136 std::vector<int> all_primes;
137 for (int r = 0; r < Rank; ++r) {
138 target_prime_sums[r].resize(n_targets);
139 for (int64_t d = 0; d < n_targets; ++d) {
140 auto exponent = odd_exponent_abs(target_n(r, d));
141 auto prime_list = express_as_prime_sum(static_cast<long>(exponent));
142 target_prime_sums[r][d] = prime_list;
143 for (int prime : prime_list) all_primes.push_back(prime);
144 }
145 }
146
147 std::sort(all_primes.begin(), all_primes.end());
148 all_primes.erase(std::unique(all_primes.begin(), all_primes.end()), all_primes.end());
149 primes = std::move(all_primes);
150
151 for (int r = 0; r < Rank; ++r) {
152 for (int64_t d = 0; d < n_targets; ++d) {
153 for (int &prime : target_prime_sums[r][d]) {
154 prime = static_cast<int>(std::find(primes.begin(), primes.end(), prime) - primes.begin());
155 }
156 }
157 }
158
159 for (int r = 0; r < Rank; ++r) { prime_pow_tbl[r].resize(primes.size(), buf_size_); }
160 } else {
161 // Rank-1: use bitwise power-of-two direct kernel.
162 unsigned long max_exponent = 0;
163 target_pow2_bits.resize(n_targets);
164 for (int64_t d = 0; d < n_targets; ++d) {
165 unsigned long exponent = odd_exponent_abs(target_n(0, d));
166 max_exponent = std::max(max_exponent, exponent);
167
168 std::vector<int> bits;
169 for (int k = 0; exponent > 0; ++k, exponent >>= 1) {
170 if (exponent & 1ul) bits.push_back(k);
171 }
172 target_pow2_bits[d] = std::move(bits);
173 }
174
175 num_power2_levels = std::max(1, static_cast<int>(std::bit_width(max_exponent)));
176 pow2_tbl.resize(num_power2_levels, buf_size_);
177 }
178
179 } else {
180 NDA_RUNTIME_ERROR << "nfft_buf_t: only type3 and direct supported with target frequencies\n";
181 }
182 }
183
190 nfft_buf_t(nda::array_view<dcomplex, 1> fiw_vec_, std::vector<mesh::matsubara_freq> const &target_mf_, int buf_size_, nfft_type_t type,
191 double tol_ = 1e-13)
192 requires(Rank == 1)
193 : nfft_buf_t(std::move(fiw_vec_), to_array_vector(target_mf_), buf_size_, type, tol_) {}
194
195 ~nfft_buf_t() {
196 if (buf_counter != 0) std::cout << " WARNING: Points in NFFT Buffer lost \n";
197 // plan automatically destroyed by unique_ptr
198 }
199
200 // nfft_buffer is move-only (unique_ptr member)
201 nfft_buf_t(nfft_buf_t const &) = delete;
202 nfft_buf_t &operator=(nfft_buf_t const &) = delete;
203 nfft_buf_t(nfft_buf_t &&) = default;
204 nfft_buf_t &operator=(nfft_buf_t &&rhs) noexcept {
205 // Custom move assignment: array_view::operator= does deep copy,
206 // but we need to rebind views to point to the same underlying data.
207 // Leverage working move constructor via destroy + placement new.
208 if (this != &rhs) {
209 std::destroy_at(this);
210 std::construct_at(this, std::move(rhs));
211 }
212 return *this;
213 }
214
216 void rebind(array_view<dcomplex, Rank> new_fiw_arr) {
217 flush();
218 TRIQS_ASSERT((new_fiw_arr.shape() == fiw_arr.shape() or fiw_arr.empty())
219 and " Nfft Buffer: Rebind to array of different shape not allowed ");
220 fiw_arr.rebind(new_fiw_arr);
221 }
222
224 void push_back(std::array<double, Rank> const &tau_arr, dcomplex ftau) {
225
226 // Check if buffer has been properly initialized
227 if (x_arr.empty()) NDA_RUNTIME_ERROR << " Using a default-constructed NFFT Buffer is not allowed\n";
228
229 if (nfft_type == nfft_type_t::type1) {
230 // Type 1: normalize tau to [-PI, PI) and apply phase correction
231 double tau_sum = 0.0;
232 for (int r = 0; r < Rank; ++r) {
233 x_arr(r, buf_counter) = 2 * M_PI * (tau_arr[r] / beta - 0.5); // \in [-PI, PI)
234 tau_sum += tau_arr[r];
235 }
236 fx_arr[buf_counter] = std::exp(dcomplex(0, M_PI * tau_sum / beta)) * ftau;
237 } else {
238 // Type 3 / direct: raw tau values, no normalization or phase correction needed
239 for (int r = 0; r < Rank; ++r) x_arr(r, buf_counter) = tau_arr[r];
240 fx_arr[buf_counter] = ftau;
241 }
242
243 ++buf_counter;
244
245 // If buffer is full, perform transform
246 if (is_full()) {
247 do_nfft();
248 buf_counter = 0;
249 }
250 }
251
253 void flush() {
254
255 // Check if buffer has been properly initialized
256 if (x_arr.empty()) NDA_RUNTIME_ERROR << " Using a default-constructed NFFT Buffer is not allowed\n";
257
258 // Don't do anything if buffer is empty
259 if (is_empty()) return;
260
261 // Execute the transform
262 do_nfft();
263 buf_counter = 0;
264 }
265
266 private:
267 // Transform type
268 nfft_type_t nfft_type = nfft_type_t::type1;
269
270 // Type 1: output array in matsubara frequencies (uniform grid)
271 nda::array_view<dcomplex, Rank> fiw_arr;
272
273 // 1D output vector at target frequencies (type 3 and direct)
274 nda::array_view<dcomplex, 1> fiw_vec;
275
276 // Dimensions of the output array (type 1 only)
277 std::array<int64_t, Rank> niws{};
278
279 // Finufft plan (RAII-managed)
280 finufft_plan_ptr plan;
281
282 // Number of tau points for the nfft
283 int buf_size = 0;
284
285 // Inverse temperature (type 1 and direct)
286 double beta = 0;
287
288 // Counter for elements currently in the buffer
289 int buf_counter = 0;
290
291 // Common factor in container assignment (type 1 only)
292 int common_factor = 1;
293
294 // Number of target frequencies (type 3 and direct)
295 int64_t n_targets = 0;
296
297 // FINUFFT options struct
298 finufft_opts opts{};
299
300 // Array containing x values for the NFFT transform
301 nda::array<double, 2> x_arr;
302
303 // Array containing f(x) values for the NFFT transform
304 nda::vector<dcomplex> fx_arr;
305
306 // Array containing the NFFT output h(k) (type 1)
307 nda::array<dcomplex, Rank> fk_arr;
308
309 // Target frequencies, shape (Rank, n_targets) (type 3 only)
310 nda::array<double, 2> s_arr;
311
312 // Type 3 NFFT output buffer
313 nda::vector<dcomplex> fk_vec;
314
315 // Integer Matsubara indices per target, shape (Rank, n_targets) (direct only)
316 nda::array<long, 2> target_n;
317
318 // Binary exponentiation approach: store z^(2^k) for k=0,1,2,...
319 int num_power2_levels = 0;
320
321 // Preallocated power-of-2 table for Rank==1 bitwise direct kernel.
322 // Shape: (num_power2_levels, buf_size).
323 // pow2_tbl(k, j) = z^(2^k) for buffer element j.
324 std::conditional_t<Rank == 1, nda::array<dcomplex, 2>, std::monostate> pow2_tbl;
325
326 // Per-target list of power-of-two exponents for Rank==1 bitwise kernel.
327 // target_pow2_bits[d] contains k such that |2*n_d+1| has bit k set.
328 std::conditional_t<Rank == 1, std::vector<std::vector<int>>, std::monostate> target_pow2_bits;
329
330 // Rank>1 prime-sum direct kernel data
331 std::vector<int> primes;
332 std::array<nda::array<dcomplex, 2>, Rank> prime_pow_tbl;
333 std::array<std::vector<std::vector<int>>, Rank> target_prime_sums;
334
335 // Tolerance for the transformation
336 double tol = 1e-15;
337
338 // Function to check whether buffer is filled
339 bool is_full() const { return buf_counter >= buf_size; }
340
341 // Function to check whether buffer is empty
342 bool is_empty() const { return buf_counter == 0; }
343
344 // For fermionic frequencies: omega_n = (2n+1) * pi / beta.
345 // Direct kernels always work with the absolute odd exponent |2n+1|.
346 static constexpr unsigned long odd_exponent_abs(long n) {
347 long odd = 2 * n + 1;
348 return static_cast<unsigned long>(odd >= 0 ? odd : -odd);
349 }
350
351 static constexpr bool is_prime(long x) {
352 if (x < 2) return false;
353 if (x == 2) return true;
354 if (x % 2 == 0) return false;
355 for (long i = 3; i * i <= x; i += 2)
356 if (x % i == 0) return false;
357 return true;
358 }
359
360 static constexpr int max_prime_sum_terms = 8;
361 static constexpr int prime_sum_precompute_size = 128;
362
363 // Tunable number of SIMD accumulators for direct kernels.
364 // `n_acc_bitwise` is used by the Rank-1 bitwise power-of-two kernel.
365 // `n_acc_prime` is used by the Rank>1 prime-sum kernel.
366 static constexpr int n_acc_bitwise = 4;
367 static constexpr int n_acc_prime = 4;
368 // for rank 2 large sizes 8 is better but I think finufft will be faster anyway at that point
369
370 struct prime_sum_entry_t {
371 std::array<int, max_prime_sum_terms> terms{};
372 int size = 0;
373 };
374
375 static constexpr prime_sum_entry_t express_as_prime_sum_ct(long n) {
376 prime_sum_entry_t out{};
377 while (n > 0 && out.size < max_prime_sum_terms) {
378 if (n == 1) {
379 out.terms[out.size++] = 1;
380 break;
381 }
382 if (n == 2 || n == 3) {
383 out.terms[out.size++] = static_cast<int>(n);
384 break;
385 }
386 if (n == 4) {
387 out.terms[out.size++] = 2;
388 out.terms[out.size++] = 2;
389 break;
390 }
391
392 long p = n;
393 while (p > 1 && !is_prime(p)) --p;
394 out.terms[out.size++] = static_cast<int>(p);
395 n -= p;
396 }
397 return out;
398 }
399
400 static constexpr auto precomputed_prime_sums = [] {
401 std::array<prime_sum_entry_t, prime_sum_precompute_size> table{};
402 for (int n = 0; n < prime_sum_precompute_size; ++n) table[n] = express_as_prime_sum_ct(n);
403 return table;
404 }();
405
406 static constexpr bool check_precomputed_prime_sums() {
407 for (int n = 0; n < prime_sum_precompute_size; ++n) {
408 long sum = 0;
409 for (int i = 0; i < precomputed_prime_sums[n].size; ++i) sum += precomputed_prime_sums[n].terms[i];
410 if (sum != n) return false;
411 }
412 return true;
413 }
414
415 static_assert(check_precomputed_prime_sums(), "prime-sum precompute table is invalid");
416
417 // Helper: express n as sum of primes with repetition: n = p1 + p2 + ... + pk.
418 static std::vector<int> express_as_prime_sum(long n) {
419 if (n < 1) return {};
420 auto entry = express_as_prime_sum_ct(n);
421 return {entry.terms.begin(), entry.terms.begin() + entry.size};
422 }
423
424 // ═══════════════════════════════════════════════════════════════════════════
425 // Unified Target Accumulation Template with Instruction-Level Parallelism
426 // ═══════════════════════════════════════════════════════════════════════════
427 //
428 // This template implements a sophisticated two-level parallelism strategy to maximize CPU throughput:
429 //
430 // 1. **SIMD (Single Instruction Multiple Data) Parallelism:**
431 // - Vectorizes across buffer elements (tau points)
432 // - Process 2-4 complex numbers simultaneously per instruction (hardware dependent)
433 // - Uses xsimd library for portable SIMD abstractions
434 //
435 // 2. **ILP (Instruction-Level Parallelism):**
436 // - Process n_acc independent targets simultaneously
437 // - Each target maintains its own accumulator chain
438 // - Breaks data dependencies, allowing CPU to execute multiple operations in parallel
439 // - Exploits superscalar execution and out-of-order execution in modern CPUs
440 //
441 // **Why this matters:**
442 // Without ILP, the CPU would wait for each FMA (fused multiply-add) to complete before
443 // starting the next one due to data dependencies. With n_acc=4 independent chains,
444 // the CPU can overlap execution, achieving ~4x higher throughput.
445 //
446 // **Parameters:**
447 // - n_acc: Number of independent accumulators (typically 4-8)
448 // - compute_simd_pow: Lambda computing exp(iω*τ) for SIMD-aligned buffer indices
449 // - compute_scalar_pow: Lambda computing exp(iω*τ) for scalar tail elements
450 //
451 template <int n_acc, typename SimdPowFunc, typename ScalarPowFunc>
452 [[gnu::always_inline]] inline void accumulate_targets_ilp(int64_t n_targets_total, int64_t buf_counter_simd, dcomplex *fiw_ptr,
453 SimdPowFunc &&compute_simd_pow, ScalarPowFunc &&compute_scalar_pow) {
454 using cbatch = xsimd::batch<dcomplex>; // SIMD type for complex numbers
455 constexpr std::size_t simd_size = cbatch::size; // Typically 2-4 depending on CPU
456
457 // ─────────────────────────────────────────────────────────────────────────
458 // Helper: Single-target accumulation (used for remainder targets)
459 // ─────────────────────────────────────────────────────────────────────────
460 // Computes: fiw[d] += Σ_j f(tau_j) * exp(iω_d*tau_j)
461 auto accumulate_one = [&](int64_t d) {
462 // SIMD loop: process buffer in chunks of simd_size
463 cbatch sum_vec(dcomplex{0, 0}); // Initialize SIMD accumulator to zero
464 for (int j = 0; j < buf_counter_simd; j += simd_size) {
465 cbatch fj = cbatch::load_unaligned(fx_arr.data() + j); // Load f(tau_j) [simd_size elements]
466 cbatch pow = compute_simd_pow(d, j); // Compute exp(iω_d*tau_j) [vectorized]
467 sum_vec = xsimd::fma(fj, pow, sum_vec); // Fused multiply-add: sum += fj * pow
468 }
469 // Reduce SIMD vector to scalar by summing all lanes
470 dcomplex sum = xsimd::reduce_add(sum_vec);
471
472 // Scalar tail: handle remaining buffer elements that don't fit in SIMD
473 for (int j = buf_counter_simd; j < buf_counter; ++j) {
474 dcomplex pow = compute_scalar_pow(d, j);
475 sum += fx_arr[j] * pow;
476 }
477
478 fiw_ptr[d] += sum; // Accumulate into output
479 };
480
481 // ─────────────────────────────────────────────────────────────────────────
482 // Main ILP Loop: Process n_acc targets simultaneously
483 // ─────────────────────────────────────────────────────────────────────────
484 // Round down to nearest multiple of n_acc
485 int64_t const n_targets_main = (n_targets_total / n_acc) * n_acc;
486 int64_t d = 0;
487
488 for (; d < n_targets_main; d += n_acc) {
489 // Initialize n_acc independent SIMD accumulators (one per target in this batch)
490 std::array<cbatch, n_acc> sum_vecs;
491 poet::static_for<n_acc>([&](const auto acc_idx) {
492 sum_vecs[acc_idx] = cbatch(dcomplex{0, 0});
493 });
494
495 // ═══ SIMD Loop: Vectorize over buffer elements ═══
496 for (int j = 0; j < buf_counter_simd; j += simd_size) {
497 // Load f(tau_j) once - shared across all n_acc targets
498 cbatch fj = cbatch::load_unaligned(fx_arr.data() + j);
499
500 // Unroll over n_acc accumulators at compile time
501 // This creates n_acc independent FMA dependency chains, enabling ILP
502 // The CPU can execute these in parallel pipelines
503 poet::static_for<n_acc>([&](const auto acc_idx) {
504 cbatch pow = compute_simd_pow(d + acc_idx, j); // exp(iω_{d+k}*tau_j)
505 sum_vecs[acc_idx] = xsimd::fma(fj, pow, sum_vecs[acc_idx]); // Independent accumulation
506 });
507 }
508
509 // ═══ Reduce Phase: SIMD vectors → scalars ═══
510 std::array<dcomplex, n_acc> sums;
511 poet::static_for<n_acc>([&](const auto acc_idx) {
512 sums[acc_idx] = xsimd::reduce_add(sum_vecs[acc_idx]);
513 });
514
515 // ═══ Scalar Tail: Process remaining non-SIMD-aligned elements ═══
516 for (int j = buf_counter_simd; j < buf_counter; ++j) {
517 dcomplex fj = fx_arr[j];
518 poet::static_for<n_acc>([&](const auto acc_idx) {
519 dcomplex pow = compute_scalar_pow(d + acc_idx, j);
520 sums[acc_idx] += fj * pow;
521 });
522 }
523
524 // ═══ Write-back Phase: Store results to output array ═══
525 poet::static_for<n_acc>([&](const auto acc_idx) {
526 fiw_ptr[d + acc_idx] += sums[acc_idx];
527 });
528 }
529
530 // ─────────────────────────────────────────────────────────────────────────
531 // Remainder Loop: Handle final targets when n_targets % n_acc != 0
532 // ─────────────────────────────────────────────────────────────────────────
533 for (; d < n_targets_total; ++d) accumulate_one(d);
534 }
535
536 // Perform NFFT transform and accumulate
537 void do_nfft() {
538 if (nfft_type == nfft_type_t::type1)
539 do_nfft_type1();
540 else if (nfft_type == nfft_type_t::type3)
541 do_nfft_type3();
542 else
543 do_direct();
544 }
545
546 // Set source points and optional target points on the FINUFFT plan, dispatching by Rank.
547 // FINUFFT expects coordinates in reverse rank order (x=last dim, y=second-to-last, z=first).
548 // For type 1, pass nullptr for tgt. For type 3, pass target coordinate array.
549 void set_pts(nda::array<double, 2> *tgt = nullptr) {
550 auto _ = nda::range::all;
551 auto n_tgt = tgt ? n_targets : int64_t{0};
552 auto t = [&](int r) -> double * { return tgt ? (*tgt)(r, _).data() : nullptr; };
553 if constexpr (Rank == 1)
554 check_finufft(finufft_setpts(plan.get(), buf_counter, x_arr(0, _).data(), nullptr, nullptr, n_tgt, t(0), nullptr, nullptr));
555 else if constexpr (Rank == 2)
556 check_finufft(finufft_setpts(plan.get(), buf_counter, x_arr(1, _).data(), x_arr(0, _).data(), nullptr, n_tgt, t(1), t(0), nullptr));
557 else // Rank == 3
558 check_finufft(finufft_setpts(plan.get(), buf_counter, x_arr(2, _).data(), x_arr(1, _).data(), x_arr(0, _).data(), n_tgt, t(2), t(1), t(0)));
559 }
560
561 // Type 1: non-uniform tau -> uniform grid
562 void do_nfft_type1() {
563 set_pts();
564 check_finufft(finufft_execute(plan.get(), fx_arr.data(), fk_arr.data()));
565
566 // Accumulate results in fiw_arr. Care to normalize results afterwards
567 for (auto idx_tpl : fiw_arr.indices()) {
568 auto idx_sum = std::apply([](auto... idx) { return (idx + ... + 0); }, idx_tpl);
569 int factor = common_factor * (idx_sum % 2 ? -1 : 1);
570 std::apply(fiw_arr, idx_tpl) += std::apply(fk_arr, idx_tpl) * factor;
571 }
572 }
573
574 // Type 3: non-uniform tau -> non-uniform target frequencies
575 void do_nfft_type3() {
576 set_pts(&s_arr);
577 check_finufft(finufft_execute(plan.get(), fx_arr.data(), fk_vec.data()));
578
579 fiw_vec += fk_vec;
580 }
581
582 // ═══════════════════════════════════════════════════════════════════════════
583 // Rank-1 Direct NUDFT: Bitwise Power-of-Two Decomposition
584 // ═══════════════════════════════════════════════════════════════════════════
585 //
586 // **Algorithm Overview:**
587 // Goal: Compute exp(iω_n*τ) for fermionic Matsubara frequencies ω_n = (2n+1)π/β
588 //
589 // **Key Mathematical Insight:**
590 // exp(iω_n*τ) = exp(i*(2n+1)*π*τ/β)
591 // = [exp(iπτ/β)]^(2n+1)
592 // = z^(2n+1)
593 // where z := exp(iπτ/β) is the "base phase factor"
594 //
595 // **Efficient Exponentiation via Binary Decomposition:**
596 // Instead of computing z^m naively (O(m) multiplications), we use binary exponentiation:
597 //
598 // 1. Express m = |2n+1| in binary: m = Σ b_k * 2^k (where b_k ∈ {0,1} are the bits)
599 // Example: m=13 = 8+4+1 = 2³ + 2² + 2⁰
600 //
601 // 2. Precompute powers-of-two: z, z², z⁴, z⁸, z¹⁶, ... via repeated squaring
602 // This takes O(log m) operations
603 //
604 // 3. Multiply only the powers corresponding to set bits:
605 // z^m = z^(2^k₁) * z^(2^k₂) * ... where k₁, k₂, ... are the bit positions
606 // Example: z¹³ = z⁸ * z⁴ * z¹
607 //
608 // **Complexity:** O(log m) multiplications instead of O(m)
609 // For typical Matsubara indices (|2n+1| ~ 1-1000), this is 10-100x faster!
610 //
611 // **Why this is optimal for Rank=1:**
612 // Binary representation is minimal - every integer has exactly one binary form.
613 // For Rank>1, we use prime-sum decomposition instead (better power sharing).
614 //
615 void do_direct_bitwise() {
616 static_assert(Rank == 1);
617 using cbatch = xsimd::batch<dcomplex>;
618 constexpr std::size_t simd_size = cbatch::size;
619
620 double const pi_over_beta = M_PI / beta;
621 int64_t const buf_counter_simd = buf_counter & -simd_size; // Floor to SIMD alignment
622 dcomplex *fiw_ptr = fiw_vec.data(); // Output pointer
623
624 // ═══════════════════════════════════════════════════════════════════════
625 // Phase 1: Build Power-of-Two Table via Repeated Squaring
626 // ═══════════════════════════════════════════════════════════════════════
627 // Compute pow2_tbl(k, j) = z_j^(2^k) for all buffer elements j
628 // where z_j = exp(iπτ_j/β)
629
630 // ─── Step 1a: Compute z = exp(iπτ/β) for all buffer elements ───
631 // SIMD path: Process simd_size elements at once
632 for (int j = 0; j < buf_counter_simd; j += simd_size) {
633 using rbatch = xsimd::batch<double>;
634 // Compute angle: θ = πτ/β
635 rbatch theta_vec = rbatch::load_unaligned(&x_arr(0, j)) * pi_over_beta;
636 // Vectorized sincos: compute sin(θ) and cos(θ) simultaneously (hardware optimized)
637 auto [sin_vec, cos_vec] = xsimd::sincos(theta_vec);
638 // Build complex exponential: z = cos(θ) + i*sin(θ) = exp(iθ)
639 cbatch z_vec(cos_vec, sin_vec);
640 // Store z^(2^0) = z in level k=0
641 z_vec.store_unaligned(&pow2_tbl(0, j));
642 }
643 // Scalar tail: handle remaining elements that don't fit in SIMD
644 for (int j = buf_counter_simd; j < buf_counter; ++j) {
645 double const theta = pi_over_beta * x_arr(0, j);
646 pow2_tbl(0, j) = dcomplex{std::cos(theta), std::sin(theta)};
647 }
648
649 // ─── Step 1b: Repeated squaring to build higher levels ───
650 // For each level k: z^(2^k) = [z^(2^(k-1))]²
651 // Example: z² = z*z, z⁴ = z²*z², z⁸ = z⁴*z⁴, ...
652 for (int k = 1; k < num_power2_levels; ++k) {
653 // SIMD path: vectorized squaring
654 for (int j = 0; j < buf_counter_simd; j += simd_size) {
655 cbatch prev = cbatch::load_unaligned(&pow2_tbl(k - 1, j)); // Load z^(2^(k-1))
656 cbatch curr = prev * prev; // Square it
657 curr.store_unaligned(&pow2_tbl(k, j)); // Store z^(2^k)
658 }
659 // Scalar tail
660 for (int j = buf_counter_simd; j < buf_counter; ++j) {
661 dcomplex prev = pow2_tbl(k - 1, j);
662 pow2_tbl(k, j) = prev * prev;
663 }
664 }
665
666 // ═══════════════════════════════════════════════════════════════════════
667 // Phase 2: Accumulate Targets by Multiplying Powers for Set Bits
668 // ═══════════════════════════════════════════════════════════════════════
669 // For each target d with exponent m = |2n_d+1|, we precomputed the bit
670 // positions in target_pow2_bits[d]. Now we multiply those powers together.
671
672 // ─── SIMD Power Computation Lambda ───
673 auto compute_simd_pow = [&](int64_t d, int j) -> cbatch {
674 auto const &bits = target_pow2_bits[d]; // List of set bit positions in |2n_d+1|
675 bool const is_neg = target_n(0, d) < 0; // Is this a negative frequency?
676
677 // Start with identity: z^0 = 1
678 cbatch rank_pow(dcomplex{1.0, 0.0});
679
680 // Multiply powers corresponding to each set bit
681 // Example: if bits = [0, 2, 3], compute z^1 * z^4 * z^8 = z^13
682 for (int k : bits) {
683 rank_pow *= cbatch::load_unaligned(&pow2_tbl(k, j));
684 }
685
686 // For negative frequencies: exp(-iω*τ) = conj(exp(iω*τ))
687 // This uses the identity: exp(-ix) = cos(x) - i*sin(x) = conj(exp(ix))
688 return is_neg ? xsimd::conj(rank_pow) : rank_pow;
689 };
690
691 // ─── Scalar Power Computation Lambda (identical logic, non-vectorized) ───
692 auto compute_scalar_pow = [&](int64_t d, int j) -> dcomplex {
693 auto const &bits = target_pow2_bits[d];
694 bool const is_neg = target_n(0, d) < 0;
695 dcomplex rank_pow{1.0, 0.0};
696 for (int k : bits) rank_pow *= pow2_tbl(k, j);
697 return is_neg ? std::conj(rank_pow) : rank_pow;
698 };
699
700 // ─── Call unified ILP accumulation template ───
701 // This computes: fiw[d] += Σ_j f(tau_j) * exp(iω_d*tau_j) for all targets d
702 accumulate_targets_ilp<n_acc_bitwise>(n_targets, buf_counter_simd, fiw_ptr, compute_simd_pow, compute_scalar_pow);
703 }
704
705 // ═══════════════════════════════════════════════════════════════════════════
706 // Rank>1 Direct NUDFT: Prime-Sum Decomposition
707 // ═══════════════════════════════════════════════════════════════════════════
708 //
709 // **Algorithm Overview:**
710 // Goal: Compute exp(iω_{n1}*τ1 + iω_{n2}*τ2 + ...) for multi-dimensional frequencies
711 // = exp(iω_{n1}*τ1) * exp(iω_{n2}*τ2) * ...
712 // = z_1^{m1} * z_2^{m2} * ...
713 // where z_r = exp(iπτ_r/β) and m_r = |2n_r+1|
714 //
715 // **Why Not Use Binary Exponentiation for Rank>1?**
716 // For Rank>1, we need to compute many distinct exponents (m_r for each rank r and target d).
717 // Binary decomposition would require storing 2^k powers for each distinct exponent,
718 // leading to excessive memory usage and poor cache behavior.
719 //
720 // **Prime-Sum Decomposition Strategy:**
721 // Every positive integer can be expressed as a sum of primes (Goldbach-style):
722 // m = p1 + p2 + ... + pk
723 // Then: z^m = z^(p1+p2+...+pk) = z^p1 * z^p2 * ... * z^pk
724 //
725 // **Key Advantages:**
726 // 1. **Power Sharing:** The set of unique primes needed across ALL targets and ranks
727 // is much smaller than the set of unique exponents. We compute each z^p once and reuse it.
728 //
729 // 2. **Small Exponents:** Primes are typically small (2, 3, 5, 7, 11, ...), making
730 // z^p fast to compute via binary exponentiation.
731 //
732 // 3. **Memory Efficiency:** Store O(#primes * Rank * buf_size) instead of
733 // O(#unique_exponents * Rank * buf_size). For many targets, #primes << #unique_exponents.
734 //
735 // **Example:**
736 // Targets with m = 13, 15, 17 in some rank:
737 // 13 = 13, 15 = 13+2, 17 = 17
738 // Unique primes: {2, 13, 17}
739 // We compute z^2, z^13, z^17 once, then:
740 // z^13 = z^13, z^15 = z^13*z^2, z^17 = z^17
741 //
742 void do_direct_prime() {
743 using cbatch = xsimd::batch<dcomplex>;
744 constexpr std::size_t simd_size = cbatch::size;
745
746 double const pi_over_beta = M_PI / beta;
747 int64_t const buf_counter_simd = buf_counter & -simd_size;
748 dcomplex *fiw_ptr = fiw_vec.data();
749 int const num_primes = static_cast<int>(primes.size()); // # of unique primes across all targets
750
751 // ═══════════════════════════════════════════════════════════════════════
752 // Phase 1: Compute Prime Powers for Each Rank
753 // ═══════════════════════════════════════════════════════════════════════
754 // For each rank r and each unique prime p, compute:
755 // prime_pow_tbl[r](p_idx, j) = z_r^p where z_r = exp(iπτ_r/β)
756 //
757 // This is done once per rank, then reused for all targets.
758
759 // Use compile-time loop over ranks (unrolled at compile time)
760 poet::static_for<Rank>([&](const auto r) {
761
762 // ─── Step 1: Compute base phase factors z_r for this rank ───
763 std::vector<dcomplex> z_vals(buf_counter);
764 for (int j = 0; j < buf_counter; ++j) {
765 double const theta = pi_over_beta * x_arr(r, j); // θ = πτ_r/β
766 z_vals[j] = dcomplex{std::cos(theta), std::sin(theta)}; // z_r = exp(iθ)
767 }
768
769 // ─── Step 2: For each unique prime, compute z_r^prime ───
770 for (int p_idx = 0; p_idx < num_primes; ++p_idx) {
771 int prime = primes[p_idx];
772
773 // Special case: "prime" = 1 (treated as prime for algorithm simplicity)
774 if (prime == 1) {
775 // z^1 = z (no exponentiation needed)
776 for (int j = 0; j < buf_counter; ++j) {
777 prime_pow_tbl[r](p_idx, j) = z_vals[j];
778 }
779 continue;
780 }
781
782 // ═══ Binary Exponentiation to Compute z^prime ═══
783 // This is O(log prime) instead of O(prime) for naive multiplication
784 // Algorithm: Process bits of exponent from LSB to MSB
785 // result = 1
786 // base = z
787 // for each bit k in prime:
788 // if bit k is set: result *= base
789 // base = base^2 (square for next bit)
790
791 // SIMD path: vectorized binary exponentiation
792 for (int j = 0; j < buf_counter_simd; j += simd_size) {
793 cbatch z_vec = cbatch::load_unaligned(&z_vals[j]); // Load z
794 cbatch zp_vec(dcomplex{1.0, 0.0}); // Result accumulator (starts at 1)
795 cbatch base_vec = z_vec; // Current power of base
796 int exp = prime; // Exponent to process
797
798 // Binary exponentiation loop
799 while (exp > 0) {
800 if (exp & 1) zp_vec *= base_vec; // If current bit is set, multiply into result
801 base_vec *= base_vec; // Square base for next bit position
802 exp >>= 1; // Shift to next bit
803 }
804 zp_vec.store_unaligned(&prime_pow_tbl[r](p_idx, j));
805 }
806
807 // Scalar tail: same algorithm, non-vectorized
808 for (int j = buf_counter_simd; j < buf_counter; ++j) {
809 dcomplex zp{1.0, 0.0};
810 dcomplex base = z_vals[j];
811 int exp = prime;
812 while (exp > 0) {
813 if (exp & 1) zp *= base;
814 base *= base;
815 exp >>= 1;
816 }
817 prime_pow_tbl[r](p_idx, j) = zp;
818 }
819 }
820 });
821
822 // ═══════════════════════════════════════════════════════════════════════
823 // Phase 2: Accumulate Targets by Combining Prime Powers
824 // ═══════════════════════════════════════════════════════════════════════
825 // For each target d with multi-dimensional frequency (ω_{n1}, ω_{n2}, ...):
826 // exp(iω_{n1}*τ1 + ... + iω_{nR}*τR) = Π_r z_r^{m_r}
827 // where m_r = |2n_r+1| and z_r = exp(iπτ_r/β)
828 //
829 // Each m_r is decomposed as sum of primes: m_r = p1 + p2 + ...
830 // So: z_r^{m_r} = z_r^{p1} * z_r^{p2} * ...
831
832 // ─── SIMD Power Computation Lambda ───
833 auto compute_simd_pow = [&](int64_t d, int j) -> cbatch {
834 cbatch pow_prod; // Will accumulate product across all ranks
835
836 // Loop over each dimension/rank (compile-time unroll)
837 poet::static_for<Rank>([&](const auto r) {
838 // Start with identity for this rank
839 cbatch rank_pow(dcomplex{1.0, 0.0});
840
841 // Get list of prime indices that sum to m_r = |2n_r+1|
842 auto const &prime_indices = target_prime_sums[r][d];
843
844 // Multiply prime powers: z_r^{m_r} = z_r^{p1} * z_r^{p2} * ...
845 for (int prime_idx : prime_indices) {
846 cbatch prime_pow = cbatch::load_unaligned(&prime_pow_tbl[r](prime_idx, j));
847 rank_pow *= prime_pow;
848 }
849
850 // Handle negative frequencies via conjugation
851 rank_pow = (target_n(r, d) < 0) ? xsimd::conj(rank_pow) : rank_pow;
852
853 // Accumulate product across ranks: Π_r z_r^{m_r}
854 pow_prod = (r == 0) ? rank_pow : pow_prod * rank_pow;
855 });
856
857 return pow_prod;
858 };
859
860 // ─── Scalar Power Computation Lambda (identical logic, non-vectorized) ───
861 auto compute_scalar_pow = [&](int64_t d, int j) -> dcomplex {
862 dcomplex pow_prod{1.0, 0.0};
863 poet::static_for<Rank>([&](const auto r) {
864 dcomplex rank_pow{1.0, 0.0};
865 auto const &prime_indices = target_prime_sums[r][d];
866 for (int prime_idx : prime_indices) {
867 rank_pow *= prime_pow_tbl[r](prime_idx, j);
868 }
869 rank_pow = (target_n(r, d) < 0) ? std::conj(rank_pow) : rank_pow;
870 pow_prod *= rank_pow;
871 });
872 return pow_prod;
873 };
874
875 // ─── Call unified ILP accumulation template ───
876 accumulate_targets_ilp<n_acc_prime>(n_targets, buf_counter_simd, fiw_ptr, compute_simd_pow, compute_scalar_pow);
877 }
878
879 // ═══════════════════════════════════════════════════════════════════════════
880 // Direct DFT Dispatcher: Choose Algorithm Based on Rank
881 // ═══════════════════════════════════════════════════════════════════════════
882 // Rank-1: Use bitwise power-of-two decomposition (optimal for single dimension)
883 // Rank>1: Use prime-sum decomposition (better power sharing across dimensions)
884 void do_direct() {
885 if constexpr (Rank == 1)
886 do_direct_bitwise();
887 else
888 do_direct_prime();
889 }
890 };
891
892 // Deduction guides for nfft_buf_t
893
894 // Type 1: deduce Rank from output array
895 template <int Rank>
896 nfft_buf_t(nda::array_view<dcomplex, Rank>, int, double, double) -> nfft_buf_t<Rank>;
897
898 // Type 3/direct: deduce Rank from target frequency array
899 template <std::size_t N>
900 nfft_buf_t(nda::array_view<dcomplex, 1>, std::vector<std::array<mesh::matsubara_freq, N>>, int, nfft_type_t, double) -> nfft_buf_t<static_cast<int>(N)>;
901
902 // Convenience: vector<matsubara_freq> implies Rank=1
903 nfft_buf_t(nda::array_view<dcomplex, 1>, std::vector<mesh::matsubara_freq> const &, int, nfft_type_t, double) -> nfft_buf_t<1>;
904
905} // namespace triqs::utility