use std::sync::Arc;
use num_traits::Float;
use rayon::prelude::*;
use rustfft::{Fft, FftNum, FftPlanner, num_complex::Complex};
use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
const TR_BLOCK: usize = 32;
#[derive(Clone)]
pub(crate) struct FftGrid<T: FftNum> {
len: usize,
spectrum_cols: usize,
r2c: Arc<dyn RealToComplex<T>>,
c2r: Arc<dyn ComplexToReal<T>>,
col_forward: Arc<dyn Fft<T>>,
col_inverse: Arc<dyn Fft<T>>,
transpose: Vec<Complex<T>>,
scratch: Vec<Complex<T>>,
scratch_len: usize,
}
impl<T: FftNum + Float> FftGrid<T> {
pub(crate) fn new(len: usize) -> Self {
let mut real_planner = RealFftPlanner::<T>::new();
let mut planner = FftPlanner::<T>::new();
let spectrum_cols = len / 2 + 1;
let r2c = real_planner.plan_fft_forward(len);
let c2r = real_planner.plan_fft_inverse(len);
let col_forward = planner.plan_fft_forward(len);
let col_inverse = planner.plan_fft_inverse(len);
let scratch_len = r2c
.get_scratch_len()
.max(c2r.get_scratch_len())
.max(col_forward.get_inplace_scratch_len())
.max(col_inverse.get_inplace_scratch_len())
.max(1);
let workers = rayon::current_num_threads().max(1);
Self {
len,
spectrum_cols,
r2c,
c2r,
col_forward,
col_inverse,
transpose: vec![Complex::new(T::zero(), T::zero()); len * spectrum_cols],
scratch: vec![Complex::new(T::zero(), T::zero()); workers * scratch_len],
scratch_len,
}
}
#[inline]
fn batch_rows(&self, n_rows: usize) -> usize {
let workers = self.scratch.len() / self.scratch_len;
n_rows.div_ceil(workers.max(1)).max(1)
}
fn resize_scratch_to_pool(&mut self) {
let needed = rayon::current_num_threads().max(1) * self.scratch_len;
if self.scratch.len() != needed {
self.scratch
.resize(needed, Complex::new(T::zero(), T::zero()));
}
}
pub(crate) const fn spectrum_len<const DIMS: usize>(&self) -> usize {
self.len.pow((DIMS - 1) as u32) * self.spectrum_cols
}
pub(crate) fn forward<const DIMS: usize>(
&mut self,
grid: &mut [T],
spectrum: &mut [Complex<T>],
) {
self.resize_scratch_to_pool();
self.rows_r2c(grid, spectrum);
if DIMS == 2 {
self.column_pass(true, spectrum);
}
}
pub(crate) fn inverse<const DIMS: usize>(
&mut self,
spectrum: &mut [Complex<T>],
grid: &mut [T],
) {
self.resize_scratch_to_pool();
if DIMS == 2 {
self.column_pass(false, spectrum);
}
clear_real_axis_imag(spectrum, self.len, self.spectrum_cols);
self.rows_c2r(spectrum, grid);
}
fn column_pass(&mut self, forward: bool, spectrum: &mut [Complex<T>]) {
let (m, h) = (self.len, self.spectrum_cols);
transpose(spectrum, &mut self.transpose, m, h);
self.column_rows(m, forward);
transpose(&self.transpose, spectrum, h, m);
}
fn rows_r2c(&mut self, grid: &mut [T], spectrum: &mut [Complex<T>]) {
let (m, h, slot_len) = (self.len, self.spectrum_cols, self.scratch_len);
let batch = self.batch_rows(grid.len() / m);
let r2c = &self.r2c;
let scratch = &mut self.scratch;
grid.par_chunks_mut(batch * m)
.zip(spectrum.par_chunks_mut(batch * h))
.zip(scratch.par_chunks_mut(slot_len))
.for_each(|((grid_batch, spectrum_batch), slot)| {
for (grid_row, spectrum_row) in
grid_batch.chunks_mut(m).zip(spectrum_batch.chunks_mut(h))
{
r2c.process_with_scratch(grid_row, spectrum_row, slot)
.expect("real-to-complex transform");
}
});
}
fn rows_c2r(&mut self, spectrum: &mut [Complex<T>], grid: &mut [T]) {
let (m, h, slot_len) = (self.len, self.spectrum_cols, self.scratch_len);
let batch = self.batch_rows(grid.len() / m);
let c2r = &self.c2r;
let scratch = &mut self.scratch;
grid.par_chunks_mut(batch * m)
.zip(spectrum.par_chunks_mut(batch * h))
.zip(scratch.par_chunks_mut(slot_len))
.for_each(|((grid_batch, spectrum_batch), slot)| {
for (grid_row, spectrum_row) in
grid_batch.chunks_mut(m).zip(spectrum_batch.chunks_mut(h))
{
c2r.process_with_scratch(spectrum_row, grid_row, slot)
.expect("complex-to-real transform");
}
});
}
fn column_rows(&mut self, m: usize, forward: bool) {
let slot_len = self.scratch_len;
let batch = self.batch_rows(self.transpose.len() / m);
let fft = if forward {
&self.col_forward
} else {
&self.col_inverse
};
let buffer = &mut self.transpose;
let scratch = &mut self.scratch;
buffer
.par_chunks_mut(batch * m)
.zip(scratch.par_chunks_mut(slot_len))
.for_each(|(batch_rows, slot)| {
for row in batch_rows.chunks_mut(m) {
fft.process_with_scratch(row, slot);
}
});
}
}
#[inline]
fn clear_real_axis_imag<T: FftNum + Float>(spectrum: &mut [Complex<T>], m: usize, h: usize) {
let even = m.is_multiple_of(2);
spectrum.chunks_mut(h).for_each(|row| {
row[0].im = T::zero();
if even {
row[h - 1].im = T::zero();
}
});
}
#[inline]
fn transpose<T: FftNum>(src: &[Complex<T>], dst: &mut [Complex<T>], rows: usize, cols: usize) {
dst.par_chunks_mut(TR_BLOCK * rows)
.enumerate()
.for_each(|(block, dst_block)| {
let col0 = block * TR_BLOCK;
let block_cols = dst_block.len() / rows;
let mut row0 = 0;
while row0 < rows {
let row1 = (row0 + TR_BLOCK).min(rows);
for r in row0..row1 {
let src_tile = &src[r * cols + col0..r * cols + col0 + block_cols];
dst_block[r..]
.iter_mut()
.step_by(rows)
.zip(src_tile)
.for_each(|(slot, &value)| *slot = value);
}
row0 = row1;
}
});
}
#[cfg(test)]
mod tests {
use rand::{Rng, SeedableRng, rngs::StdRng};
use super::*;
#[test]
fn round_trip_2d() {
let m = 12;
let total = m * m;
let mut rng = StdRng::seed_from_u64(0xABCD);
let original: Vec<f64> = (0..total).map(|_| rng.random_range(-0.5..0.5)).collect();
let mut grid = FftGrid::<f64>::new(m);
let mut real = original.clone();
let mut spectrum = vec![Complex::new(0.0, 0.0); grid.spectrum_len::<2>()];
grid.forward::<2>(&mut real, &mut spectrum);
grid.inverse::<2>(&mut spectrum, &mut real);
let inv_total = 1.0 / total as f64;
for (got, want) in real.iter().zip(original.iter()) {
assert!(
(got * inv_total - want).abs() < 1e-12,
"got {got}, want {want}"
);
}
}
#[test]
fn round_trip_1d() {
let m = 16;
let mut rng = StdRng::seed_from_u64(0xBEEF);
let original: Vec<f64> = (0..m).map(|_| rng.random_range(-0.5..0.5)).collect();
let mut grid = FftGrid::<f64>::new(m);
let mut real = original.clone();
let mut spectrum = vec![Complex::new(0.0, 0.0); grid.spectrum_len::<1>()];
grid.forward::<1>(&mut real, &mut spectrum);
grid.inverse::<1>(&mut spectrum, &mut real);
let inv_total = 1.0 / m as f64;
for (got, want) in real.iter().zip(original.iter()) {
assert!((got * inv_total - want).abs() < 1e-12);
}
}
#[test]
fn fft_circular_convolution_2d_matches_direct() {
let m = 8;
let total = m * m;
let mut rng = StdRng::seed_from_u64(0x5151);
let a: Vec<f64> = (0..total).map(|_| rng.random_range(-0.5..0.5)).collect();
let b: Vec<f64> = (0..total).map(|_| rng.random_range(-0.5..0.5)).collect();
let mut direct = vec![0.0; total];
for ki in 0..m {
for kj in 0..m {
let mut acc = 0.0;
for ji in 0..m {
for jj in 0..m {
let ai = (ki + m - ji) % m;
let aj = (kj + m - jj) % m;
acc += a[ai * m + aj] * b[ji * m + jj];
}
}
direct[ki * m + kj] = acc;
}
}
let mut grid = FftGrid::<f64>::new(m);
let len = grid.spectrum_len::<2>();
let mut fa = a.clone();
let mut sa = vec![Complex::new(0.0, 0.0); len];
grid.forward::<2>(&mut fa, &mut sa);
let mut fb = b.clone();
let mut sb = vec![Complex::new(0.0, 0.0); len];
grid.forward::<2>(&mut fb, &mut sb);
let mut prod: Vec<Complex<f64>> = sa.iter().zip(sb.iter()).map(|(x, y)| x * y).collect();
let mut out = vec![0.0; total];
grid.inverse::<2>(&mut prod, &mut out);
let inv_total = 1.0 / total as f64;
for k in 0..total {
assert!((out[k] * inv_total - direct[k]).abs() < 1e-10, "k={k}");
}
}
#[test]
fn round_trip_across_thread_pools() {
let m = 20;
let total = m * m;
let mut rng = StdRng::seed_from_u64(0xD00D);
let original: Vec<f64> = (0..total).map(|_| rng.random_range(-0.5..0.5)).collect();
let mut grid = FftGrid::<f64>::new(m);
for threads in [1usize, 3, 8] {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.unwrap();
pool.install(|| {
let mut real = original.clone();
let mut spectrum = vec![Complex::new(0.0, 0.0); grid.spectrum_len::<2>()];
grid.forward::<2>(&mut real, &mut spectrum);
grid.inverse::<2>(&mut spectrum, &mut real);
let inv_total = 1.0 / total as f64;
for (got, want) in real.iter().zip(original.iter()) {
assert!(
(got * inv_total - want).abs() < 1e-12,
"threads={threads}: got {got}, want {want}"
);
}
});
}
}
}