use num_complex::Complex;
use crate::det_math::{det_sincos, det_hypot};
use std::f64::consts::PI;
pub type Complex32 = Complex<f32>;
pub struct Spectrum2D {
pub data: Vec<Complex32>,
pub width: usize,
pub height: usize,
}
struct BluesteinPlan {
n: usize,
m: usize, chirp: Vec<Complex32>,
b_hat: Vec<Complex32>, }
impl BluesteinPlan {
fn new(n: usize, sign: f64) -> Self {
let m = next_pow2(2 * n - 1);
let mut chirp = vec![Complex32::new(0.0, 0.0); n];
for k in 0..n {
let angle = sign * PI * (k as f64 * k as f64) / n as f64;
let (s, c) = det_sincos(angle);
chirp[k] = Complex32::new(c as f32, s as f32);
}
let mut b = vec![Complex32::new(0.0, 0.0); m];
b[0] = chirp[0];
for k in 1..n {
b[k] = chirp[k];
b[m - k] = chirp[k];
}
fft_radix2_f32(&mut b, -1.0);
BluesteinPlan { n, m, chirp, b_hat: b }
}
fn execute(&self, input: &[Complex32]) -> Vec<Complex32> {
debug_assert_eq!(input.len(), self.n);
let mut a = vec![Complex32::new(0.0, 0.0); self.m];
for k in 0..self.n {
a[k] = input[k] * self.chirp[k].conj();
}
fft_radix2_f32(&mut a, -1.0);
for i in 0..self.m {
a[i] *= self.b_hat[i];
}
fft_radix2_f32(&mut a, 1.0);
let inv_m = 1.0 / self.m as f32;
let mut result = vec![Complex32::new(0.0, 0.0); self.n];
for k in 0..self.n {
result[k] = a[k] * inv_m * self.chirp[k].conj();
}
result
}
}
fn next_pow2(n: usize) -> usize {
let mut p = 1;
while p < n {
p <<= 1;
}
p
}
fn fft_radix2_f32(data: &mut [Complex32], sign: f64) {
let n = data.len();
debug_assert!(n.is_power_of_two());
if n <= 1 {
return;
}
let mut j = 0usize;
for i in 1..n {
let mut bit = n >> 1;
while j & bit != 0 {
j ^= bit;
bit >>= 1;
}
j ^= bit;
if i < j {
data.swap(i, j);
}
}
let mut twiddles: Vec<Complex32> = Vec::with_capacity(n / 2);
let mut len = 2;
while len <= n {
let half = len / 2;
let angle_step = sign * PI / half as f64;
twiddles.clear();
twiddles.extend((0..half).map(|k| {
let angle = angle_step * k as f64;
let (s, c) = det_sincos(angle);
Complex32::new(c as f32, s as f32)
}));
for start in (0..n).step_by(len) {
for k in 0..half {
let w = twiddles[k];
let u = data[start + k];
let v = data[start + k + half] * w;
data[start + k] = u + v;
data[start + k + half] = u - v;
}
}
len <<= 1;
}
}
fn fft1d_f32_with_plan(input: &[Complex32], sign: f64, plan: Option<&BluesteinPlan>) -> Vec<Complex32> {
let n = input.len();
if n == 0 {
return vec![];
}
if n == 1 {
return input.to_vec();
}
if n.is_power_of_two() {
let mut buf = input.to_vec();
fft_radix2_f32(&mut buf, sign);
return buf;
}
if let Some(p) = plan {
debug_assert_eq!(p.n, n);
return p.execute(input);
}
let temp_plan = BluesteinPlan::new(n, sign);
temp_plan.execute(input)
}
#[allow(dead_code)]
fn fft1d_f32(data: &[Complex32]) -> Vec<Complex32> {
fft1d_f32_with_plan(data, -1.0, None)
}
#[allow(dead_code)]
fn ifft1d_f32(data: &[Complex32]) -> Vec<Complex32> {
fft1d_f32_with_plan(data, 1.0, None)
}
pub fn fft2d(pixels: &[f64], width: usize, height: usize) -> Spectrum2D {
assert_eq!(pixels.len(), width * height);
let mut data: Vec<Complex32> = pixels.iter().map(|&v| Complex32::new(v as f32, 0.0)).collect();
let row_plan = if !width.is_power_of_two() && width > 1 {
Some(BluesteinPlan::new(width, -1.0))
} else {
None
};
let col_plan = if !height.is_power_of_two() && height > 1 {
Some(BluesteinPlan::new(height, -1.0))
} else {
None
};
for row in 0..height {
let start = row * width;
let row_data = &data[start..start + width];
let transformed = fft1d_f32_with_plan(row_data, -1.0, row_plan.as_ref());
data[start..start + width].copy_from_slice(&transformed);
}
let mut col_buf = vec![Complex32::new(0.0, 0.0); height];
for col in 0..width {
for r in 0..height {
col_buf[r] = data[r * width + col];
}
let transformed = fft1d_f32_with_plan(&col_buf, -1.0, col_plan.as_ref());
for r in 0..height {
data[r * width + col] = transformed[r];
}
}
Spectrum2D { data, width, height }
}
pub fn ifft2d(spectrum: &Spectrum2D) -> Vec<f64> {
let width = spectrum.width;
let height = spectrum.height;
let mut data = spectrum.data.clone();
let row_plan = if !width.is_power_of_two() && width > 1 {
Some(BluesteinPlan::new(width, 1.0))
} else {
None
};
let col_plan = if !height.is_power_of_two() && height > 1 {
Some(BluesteinPlan::new(height, 1.0))
} else {
None
};
for row in 0..height {
let start = row * width;
let row_data = &data[start..start + width];
let transformed = fft1d_f32_with_plan(row_data, 1.0, row_plan.as_ref());
data[start..start + width].copy_from_slice(&transformed);
}
let mut col_buf = vec![Complex32::new(0.0, 0.0); height];
for col in 0..width {
for r in 0..height {
col_buf[r] = data[r * width + col];
}
let transformed = fft1d_f32_with_plan(&col_buf, 1.0, col_plan.as_ref());
for r in 0..height {
data[r * width + col] = transformed[r];
}
}
let norm = 1.0 / (width * height) as f64;
let mut result = vec![0.0f64; width * height];
for i in 0..data.len() {
result[i] = data[i].re as f64 * norm;
}
result
}
pub fn magnitude_spectrum(spectrum: &Spectrum2D) -> Vec<f32> {
spectrum.data.iter().map(|c| {
det_hypot(c.re as f64, c.im as f64) as f32
}).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fft_ifft_roundtrip() {
let width = 16;
let height = 16;
let pixels: Vec<f64> = (0..width * height).map(|i| (i as f64) * 0.1 + 50.0).collect();
let spectrum = fft2d(&pixels, width, height);
let recovered = ifft2d(&spectrum);
for i in 0..pixels.len() {
assert!(
(pixels[i] - recovered[i]).abs() < 1e-3,
"Mismatch at {i}: expected {}, got {}",
pixels[i],
recovered[i]
);
}
}
#[test]
fn fft_ifft_roundtrip_non_pow2() {
let width = 12;
let height = 10;
let pixels: Vec<f64> = (0..width * height).map(|i| (i as f64) * 0.3 + 20.0).collect();
let spectrum = fft2d(&pixels, width, height);
let recovered = ifft2d(&spectrum);
for i in 0..pixels.len() {
assert!(
(pixels[i] - recovered[i]).abs() < 0.1,
"Mismatch at {i}: expected {}, got {}",
pixels[i],
recovered[i]
);
}
}
#[test]
fn parseval_theorem() {
let width = 8;
let height = 8;
let pixels: Vec<f64> = (0..width * height).map(|i| ((i * 7 + 3) % 256) as f64).collect();
let spatial_energy: f64 = pixels.iter().map(|v| v * v).sum();
let spectrum = fft2d(&pixels, width, height);
let freq_energy: f64 = spectrum.data.iter().map(|c| {
let re = c.re as f64;
let im = c.im as f64;
re * re + im * im
}).sum();
let n = (width * height) as f64;
assert!(
(spatial_energy - freq_energy / n).abs() < 10.0,
"Parseval's theorem violated: spatial={spatial_energy}, freq/N={}", freq_energy / n
);
}
#[test]
fn dc_component_is_sum() {
let width = 4;
let height = 4;
let pixels = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0];
let spectrum = fft2d(&pixels, width, height);
let expected_dc: f64 = pixels.iter().sum();
assert!(
(spectrum.data[0].re as f64 - expected_dc).abs() < 0.1,
"DC component should be sum of all pixels: expected {expected_dc}, got {}",
spectrum.data[0].re
);
assert!((spectrum.data[0].im as f64).abs() < 0.1);
}
#[test]
fn fft1d_basic() {
let input = vec![
Complex32::new(1.0, 0.0),
Complex32::new(0.0, 0.0),
Complex32::new(0.0, 0.0),
Complex32::new(0.0, 0.0),
];
let output = fft1d_f32(&input);
for k in 0..4 {
assert!((output[k].re - 1.0).abs() < 1e-5, "Re[{k}]={}", output[k].re);
assert!(output[k].im.abs() < 1e-5, "Im[{k}]={}", output[k].im);
}
}
#[test]
fn bluestein_matches_radix2() {
let n = 8;
let input: Vec<Complex32> = (0..n).map(|i| Complex32::new((i * 3 + 1) as f32, (i * 2) as f32)).collect();
let mut radix2_buf = input.clone();
fft_radix2_f32(&mut radix2_buf, -1.0);
let _plan = BluesteinPlan::new(n, -1.0);
let n2 = 7;
let input2: Vec<Complex32> = (0..n2).map(|i| Complex32::new((i * 3 + 1) as f32, (i * 2) as f32)).collect();
let plan2 = BluesteinPlan::new(n2, -1.0);
let result_plan = plan2.execute(&input2);
let result_direct = fft1d_f32(&input2);
for k in 0..n2 {
assert!(
(result_plan[k].re - result_direct[k].re).abs() < 1e-3 &&
(result_plan[k].im - result_direct[k].im).abs() < 1e-3,
"Plan vs direct mismatch at {k}: plan={}, direct={}",
result_plan[k], result_direct[k]
);
}
let result_r2 = fft1d_f32(&input);
for k in 0..n {
assert!(
(radix2_buf[k].re - result_r2[k].re).abs() < 1e-3 &&
(radix2_buf[k].im - result_r2[k].im).abs() < 1e-3,
"Mismatch at {k}: radix2={}, fft1d={}",
radix2_buf[k], result_r2[k]
);
}
}
#[test]
fn bluestein_plan_reuse() {
let n = 13; let plan = BluesteinPlan::new(n, -1.0);
let input1: Vec<Complex32> = (0..n).map(|i| Complex32::new(i as f32, 0.0)).collect();
let input2: Vec<Complex32> = (0..n).map(|i| Complex32::new(0.0, i as f32)).collect();
let r1a = plan.execute(&input1);
let r2 = plan.execute(&input2);
let r1b = plan.execute(&input1);
for k in 0..n {
assert!(
(r1a[k].re - r1b[k].re).abs() < 1e-5 &&
(r1a[k].im - r1b[k].im).abs() < 1e-5,
"Plan reuse gave different results at {k}: first={}, second={}",
r1a[k], r1b[k]
);
}
assert_eq!(r2.len(), n);
}
}