use realfft::{ComplexToReal, RealFftPlanner, RealToComplex};
pub use rustfft::num_complex::Complex64;
use rustfft::{Fft, FftPlanner};
use std::{cell::RefCell, sync::Arc};
pub(crate) const PLAN_CACHE: usize = 16;
type RealPlans =
Plans<RealFftPlanner<f64>, Arc<dyn RealToComplex<f64>>, Arc<dyn ComplexToReal<f64>>>;
type ComplexPlans = Plans<FftPlanner<f64>, Arc<dyn Fft<f64>>, Arc<dyn Fft<f64>>>;
thread_local! {
static REAL_PLANS: RefCell<RealPlans> = RefCell::new(Plans::new(RealFftPlanner::new));
static REAL_WORK: RefCell<RealWork> = RefCell::new(RealWork::default());
static COMPLEX_PLANS: RefCell<ComplexPlans> = RefCell::new(Plans::new(FftPlanner::new));
}
struct Plans<Planner, F, I> {
planner: Planner,
renew: fn() -> Planner,
held: usize,
forward: Vec<(usize, F)>,
inverse: Vec<(usize, I)>,
}
impl<Planner, F: Clone, I: Clone> Plans<Planner, F, I> {
fn new(renew: fn() -> Planner) -> Self {
Self {
planner: renew(),
renew,
held: 0,
forward: Vec::new(),
inverse: Vec::new(),
}
}
fn forward(&mut self, len: usize, make: impl FnOnce(&mut Planner, usize) -> F) -> F {
let Self {
planner,
renew,
held,
forward,
..
} = self;
cached(forward, len, planner, *renew, held, make)
}
fn inverse(&mut self, len: usize, make: impl FnOnce(&mut Planner, usize) -> I) -> I {
let Self {
planner,
renew,
held,
inverse,
..
} = self;
cached(inverse, len, planner, *renew, held, make)
}
}
fn cached<Planner, P: Clone>(
recent: &mut Vec<(usize, P)>,
len: usize,
planner: &mut Planner,
renew: fn() -> Planner,
held: &mut usize,
make: impl FnOnce(&mut Planner, usize) -> P,
) -> P {
if let Some(i) = recent.iter().position(|(n, _)| *n == len) {
let entry = recent.remove(i);
let plan = entry.1.clone();
recent.push(entry);
return plan;
}
if recent.len() >= PLAN_CACHE {
recent.remove(0);
*planner = renew();
*held = 0;
}
let plan = make(planner, len);
*held += 1;
recent.push((len, plan.clone()));
plan
}
#[derive(Default)]
struct RealWork {
input: Vec<f64>,
spectrum: Vec<Complex64>,
scratch: Vec<Complex64>,
}
pub fn rfft(x: &[f64]) -> Vec<Complex64> {
assert!(!x.is_empty(), "FFT length must be positive");
let plan = real_forward(x.len());
REAL_WORK.with(|work| {
let mut work = work.borrow_mut();
let RealWork { input, scratch, .. } = &mut *work;
input.resize(x.len(), 0.0);
input.copy_from_slice(x);
scratch.resize(plan.get_scratch_len(), Complex64::default());
let mut output = plan.make_output_vec();
plan.process_with_scratch(input, &mut output, scratch)
.expect("valid FFT buffers");
output
})
}
#[allow(dead_code)]
pub(crate) fn rfft_owned(mut x: Vec<f64>) -> Vec<Complex64> {
assert!(!x.is_empty(), "FFT length must be positive");
let plan = real_forward(x.len());
let mut output = plan.make_output_vec();
REAL_WORK.with(|work| {
let mut work = work.borrow_mut();
work.scratch
.resize(plan.get_scratch_len(), Complex64::default());
plan.process_with_scratch(&mut x, &mut output, &mut work.scratch)
.expect("valid FFT buffers");
});
output
}
pub fn irfft(spec: &[Complex64], n: usize) -> Vec<f64> {
assert!(n > 0, "FFT length must be positive");
assert!(!spec.is_empty(), "inverse FFT spectrum must be nonempty");
let plan = REAL_PLANS.with(|p| {
p.borrow_mut()
.inverse(n, |planner, n| planner.plan_fft_inverse(n))
});
let mut output = plan.make_output_vec();
REAL_WORK.with(|work| {
let mut work = work.borrow_mut();
let RealWork {
spectrum, scratch, ..
} = &mut *work;
spectrum.resize(n / 2 + 1, Complex64::default());
let len = spectrum.len().min(spec.len());
spectrum[..len].copy_from_slice(&spec[..len]);
spectrum[len..].fill(Complex64::default());
spectrum[0].im = 0.0;
if n.is_multiple_of(2) {
spectrum[n / 2].im = 0.0;
}
scratch.resize(plan.get_scratch_len(), Complex64::default());
plan.process_with_scratch(spectrum, &mut output, scratch)
.expect("valid Hermitian spectrum");
});
output.iter_mut().for_each(|v| *v /= n as f64);
output
}
pub fn fft(x: &[Complex64]) -> Vec<Complex64> {
assert!(!x.is_empty(), "FFT length must be positive");
let mut result = x.to_vec();
COMPLEX_PLANS
.with(|p| {
p.borrow_mut()
.forward(x.len(), |planner, n| planner.plan_fft_forward(n))
})
.process(&mut result);
result
}
pub fn ifft(x: &[Complex64]) -> Vec<Complex64> {
assert!(!x.is_empty(), "FFT length must be positive");
let mut result = x.to_vec();
COMPLEX_PLANS
.with(|p| {
p.borrow_mut()
.inverse(x.len(), |planner, n| planner.plan_fft_inverse(n))
})
.process(&mut result);
result.iter_mut().for_each(|v| *v /= x.len() as f64);
result
}
fn real_forward(len: usize) -> Arc<dyn RealToComplex<f64>> {
REAL_PLANS.with(|p| {
p.borrow_mut()
.forward(len, |planner, n| planner.plan_fft_forward(n))
})
}
#[cfg(test)]
fn plan_counts() -> [usize; 6] {
let real = REAL_PLANS.with(|p| {
let p = p.borrow();
[p.forward.len(), p.inverse.len(), p.held]
});
let complex = COMPLEX_PLANS.with(|p| {
let p = p.borrow();
[p.forward.len(), p.inverse.len(), p.held]
});
[
real[0], real[1], complex[0], complex[1], real[2], complex[2],
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rfft_irfft_round_trip_even_and_odd() {
for n in [1_usize, 2, 7, 8, 1000, 1001] {
let x: Vec<f64> = (0..n).map(|i| ((i * 37 % 11) as f64 - 5.0) / 3.0).collect();
let spectrum = rfft(&x);
assert_eq!(spectrum.len(), n / 2 + 1);
let back = irfft(&spectrum, n);
for (a, b) in x.iter().zip(&back) {
assert!((a - b).abs() < 1e-12, "n={n}: {a} vs {b}");
}
}
}
fn bits(x: &[Complex64]) -> Vec<(u64, u64)> {
x.iter().map(|z| (z.re.to_bits(), z.im.to_bits())).collect()
}
fn uncached(
x: &[f64],
z: &[Complex64],
) -> (Vec<Complex64>, Vec<f64>, Vec<Complex64>, Vec<Complex64>) {
let n = x.len();
let mut real = RealFftPlanner::<f64>::new();
let forward = real.plan_fft_forward(n);
let mut spectrum = forward.make_output_vec();
forward.process(&mut x.to_vec(), &mut spectrum).unwrap();
let inverse = real.plan_fft_inverse(n);
let mut hermitian = spectrum.clone();
hermitian[0].im = 0.0;
if n.is_multiple_of(2) {
hermitian[n / 2].im = 0.0;
}
let mut back = inverse.make_output_vec();
inverse.process(&mut hermitian, &mut back).unwrap();
back.iter_mut().for_each(|v| *v /= n as f64);
let mut complex = FftPlanner::<f64>::new();
let mut forward = z.to_vec();
complex.plan_fft_forward(n).process(&mut forward);
let mut inverse = z.to_vec();
complex.plan_fft_inverse(n).process(&mut inverse);
inverse.iter_mut().for_each(|v| *v /= n as f64);
(spectrum, back, forward, inverse)
}
#[test]
fn plan_cache_is_bounded_and_bit_identical() {
std::thread::spawn(|| {
let signal = |n: usize| -> (Vec<f64>, Vec<Complex64>) {
let x: Vec<f64> = (0..n).map(|i| ((i * 37 % 11) as f64 - 5.0) / 3.0).collect();
let z = x.iter().map(|&v| Complex64::new(v, 0.5 - v)).collect();
(x, z)
};
for n in (2..102).chain(2..6) {
let (x, z) = signal(n);
let spectrum = rfft(&x);
let (expected, back, forward, inverse) = uncached(&x, &z);
assert_eq!(bits(&spectrum), bits(&expected), "rfft {n}");
let got: Vec<_> = irfft(&spectrum, n).iter().map(|v| v.to_bits()).collect();
assert_eq!(
got,
back.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
"irfft {n}"
);
assert_eq!(bits(&fft(&z)), bits(&forward), "fft {n}");
assert_eq!(bits(&ifft(&z)), bits(&inverse), "ifft {n}");
let counts = plan_counts();
assert!(
counts[..4].iter().all(|&c| c <= PLAN_CACHE)
&& counts[4] <= counts[0] + counts[1]
&& counts[5] <= counts[2] + counts[3],
"after length {n}: plans {counts:?}, bound {PLAN_CACHE} per kind"
);
}
})
.join()
.unwrap();
}
}