use std::f64::consts::PI;
pub fn fft(data: &[(f64, f64)]) -> Vec<(f64, f64)> {
let n = data.len();
assert!(n.is_power_of_two(), "FFT: input length must be power of 2, got {n}");
let mut buf = data.to_vec();
let bits = n.trailing_zeros() as usize;
for i in 0..n {
let j = bit_reverse(i, bits);
if i < j {
buf.swap(i, j);
}
}
let mut size = 2;
while size <= n {
let half = size / 2;
let angle = -2.0 * PI / size as f64;
let w_base = (angle.cos(), angle.sin());
for start in (0..n).step_by(size) {
let mut w = (1.0, 0.0);
for k in 0..half {
let i = start + k;
let j = start + k + half;
let t = complex_mul(w, buf[j]);
buf[j] = (buf[i].0 - t.0, buf[i].1 - t.1);
buf[i] = (buf[i].0 + t.0, buf[i].1 + t.1);
w = complex_mul(w, w_base);
}
}
size *= 2;
}
buf
}
pub fn ifft(data: &[(f64, f64)]) -> Vec<(f64, f64)> {
let n = data.len();
let conjugated: Vec<(f64, f64)> = data.iter().map(|&(r, i)| (r, -i)).collect();
let mut result = fft(&conjugated);
let scale = 1.0 / n as f64;
for v in &mut result {
v.0 *= scale;
v.1 = -v.1 * scale;
}
result
}
pub fn rfft(data: &[f64]) -> Vec<(f64, f64)> {
let n = next_power_of_2(data.len());
let mut complex_data: Vec<(f64, f64)> = Vec::with_capacity(n);
for i in 0..n {
let val = if i < data.len() { data[i] } else { 0.0 };
complex_data.push((val, 0.0));
}
fft(&complex_data)
}
pub fn psd(data: &[f64]) -> Vec<f64> {
let spectrum = rfft(data);
spectrum.iter().map(|&(r, i)| r * r + i * i).collect()
}
fn bit_reverse(mut x: usize, bits: usize) -> usize {
let mut result = 0;
for _ in 0..bits {
result = (result << 1) | (x & 1);
x >>= 1;
}
result
}
fn complex_mul(a: (f64, f64), b: (f64, f64)) -> (f64, f64) {
(a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0)
}
fn next_power_of_2(n: usize) -> usize {
let mut p = 1;
while p < n { p <<= 1; }
p
}
pub fn hann_window(n: usize) -> Vec<f64> {
if n <= 1 { return vec![1.0; n]; }
(0..n).map(|k| 0.5 * (1.0 - (2.0 * PI * k as f64 / (n - 1) as f64).cos())).collect()
}
pub fn hamming_window(n: usize) -> Vec<f64> {
if n <= 1 { return vec![1.0; n]; }
(0..n).map(|k| 0.54 - 0.46 * (2.0 * PI * k as f64 / (n - 1) as f64).cos()).collect()
}
pub fn blackman_window(n: usize) -> Vec<f64> {
if n <= 1 { return vec![1.0; n]; }
(0..n).map(|k| {
let frac = k as f64 / (n - 1) as f64;
0.42 - 0.5 * (2.0 * PI * frac).cos() + 0.08 * (4.0 * PI * frac).cos()
}).collect()
}
pub fn fft_arbitrary(data: &[(f64, f64)]) -> Vec<(f64, f64)> {
let n = data.len();
if n == 0 { return vec![]; }
if n == 1 { return data.to_vec(); }
if n.is_power_of_two() {
return fft(data);
}
let chirp: Vec<(f64, f64)> = (0..n).map(|k| {
let angle = -PI * (k * k) as f64 / n as f64;
(angle.cos(), angle.sin())
}).collect();
let a: Vec<(f64, f64)> = data.iter().zip(chirp.iter()).map(|(&x, &w)| complex_mul(x, w)).collect();
let m = next_power_of_2(2 * n - 1);
let mut a_padded = vec![(0.0, 0.0); m];
for (i, &v) in a.iter().enumerate() {
a_padded[i] = v;
}
let mut b_padded = vec![(0.0, 0.0); m];
for i in 0..n {
b_padded[i] = (chirp[i].0, -chirp[i].1); }
for i in 1..n {
b_padded[m - i] = (chirp[i].0, -chirp[i].1); }
let a_fft = fft(&a_padded);
let b_fft = fft(&b_padded);
let c_fft: Vec<(f64, f64)> = a_fft.iter().zip(b_fft.iter()).map(|(&a, &b)| complex_mul(a, b)).collect();
let c = ifft(&c_fft);
(0..n).map(|k| complex_mul(chirp[k], c[k])).collect()
}
pub fn fft_2d(data: &[(f64, f64)], rows: usize, cols: usize) -> Result<Vec<(f64, f64)>, String> {
if data.len() != rows * cols {
return Err(format!("fft_2d: expected {} elements, got {}", rows * cols, data.len()));
}
if !rows.is_power_of_two() || !cols.is_power_of_two() {
return Err("fft_2d: rows and cols must be powers of 2".into());
}
let mut result = data.to_vec();
for r in 0..rows {
let row: Vec<(f64, f64)> = result[r * cols..(r + 1) * cols].to_vec();
let fft_row = fft(&row);
result[r * cols..(r + 1) * cols].copy_from_slice(&fft_row);
}
for c in 0..cols {
let col: Vec<(f64, f64)> = (0..rows).map(|r| result[r * cols + c]).collect();
let fft_col = fft(&col);
for r in 0..rows {
result[r * cols + c] = fft_col[r];
}
}
Ok(result)
}
pub fn ifft_2d(data: &[(f64, f64)], rows: usize, cols: usize) -> Result<Vec<(f64, f64)>, String> {
if data.len() != rows * cols {
return Err(format!("ifft_2d: expected {} elements, got {}", rows * cols, data.len()));
}
if !rows.is_power_of_two() || !cols.is_power_of_two() {
return Err("ifft_2d: rows and cols must be powers of 2".into());
}
let mut result = data.to_vec();
for r in 0..rows {
let row: Vec<(f64, f64)> = result[r * cols..(r + 1) * cols].to_vec();
let ifft_row = ifft(&row);
result[r * cols..(r + 1) * cols].copy_from_slice(&ifft_row);
}
for c in 0..cols {
let col: Vec<(f64, f64)> = (0..rows).map(|r| result[r * cols + c]).collect();
let ifft_col = ifft(&col);
for r in 0..rows {
result[r * cols + c] = ifft_col[r];
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fft_constant() {
let data = vec![(1.0, 0.0); 4];
let result = fft(&data);
assert!((result[0].0 - 4.0).abs() < 1e-12);
for i in 1..4 {
assert!(result[i].0.abs() < 1e-12);
assert!(result[i].1.abs() < 1e-12);
}
}
#[test]
fn test_fft_ifft_roundtrip() {
let data = vec![(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
let spectrum = fft(&data);
let recovered = ifft(&spectrum);
for (orig, rec) in data.iter().zip(recovered.iter()) {
assert!((orig.0 - rec.0).abs() < 1e-10);
assert!((orig.1 - rec.1).abs() < 1e-10);
}
}
#[test]
fn test_rfft_basic() {
let data = [1.0, 0.0, 0.0, 0.0];
let result = rfft(&data);
for &(re, im) in &result {
assert!((re - 1.0).abs() < 1e-12);
assert!(im.abs() < 1e-12);
}
}
#[test]
fn test_psd_basic() {
let data = [1.0, 0.0, 0.0, 0.0];
let power = psd(&data);
for &p in &power {
assert!((p - 1.0).abs() < 1e-12);
}
}
#[test]
fn test_determinism() {
let data = vec![(1.0, 2.0), (3.0, 4.0), (5.0, 6.0), (7.0, 8.0)];
let r1 = fft(&data);
let r2 = fft(&data);
for (a, b) in r1.iter().zip(r2.iter()) {
assert_eq!(a.0.to_bits(), b.0.to_bits());
assert_eq!(a.1.to_bits(), b.1.to_bits());
}
}
#[test]
fn test_hann_endpoints() {
let w = hann_window(8);
assert!(w[0].abs() < 1e-12, "hann[0] = {}", w[0]);
assert!(w[7].abs() < 1e-12, "hann[N-1] = {}", w[7]);
}
#[test]
fn test_hann_midpoint() {
let w = hann_window(9); assert!((w[4] - 1.0).abs() < 1e-12, "hann[4] = {}", w[4]);
}
#[test]
fn test_hann_symmetry() {
let w = hann_window(16);
for k in 0..8 {
assert!((w[k] - w[15 - k]).abs() < 1e-12);
}
}
#[test]
fn test_hamming_endpoints() {
let w = hamming_window(8);
assert!((w[0] - 0.08).abs() < 1e-12, "hamming[0] = {}", w[0]);
assert!((w[7] - 0.08).abs() < 1e-12, "hamming[N-1] = {}", w[7]);
}
#[test]
fn test_blackman_endpoints() {
let w = blackman_window(16);
assert!(w[0].abs() < 1e-12, "blackman[0] = {}", w[0]);
assert!(w[15].abs() < 1e-12, "blackman[N-1] = {}", w[15]);
}
#[test]
fn test_fft_arbitrary_prime() {
let n = 7;
let data: Vec<(f64, f64)> = (0..n).map(|k| ((k + 1) as f64, 0.0)).collect();
let result = fft_arbitrary(&data);
for k in 0..n {
let mut re = 0.0;
let mut im = 0.0;
for j in 0..n {
let angle = -2.0 * PI * (k * j) as f64 / n as f64;
re += data[j].0 * angle.cos() - data[j].1 * angle.sin();
im += data[j].0 * angle.sin() + data[j].1 * angle.cos();
}
assert!((result[k].0 - re).abs() < 1e-8, "re[{k}]: got {} expected {re}", result[k].0);
assert!((result[k].1 - im).abs() < 1e-8, "im[{k}]: got {} expected {im}", result[k].1);
}
}
#[test]
fn test_fft_arbitrary_matches_radix2() {
let data: Vec<(f64, f64)> = vec![(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)];
let r_radix2 = fft(&data);
let r_arb = fft_arbitrary(&data);
for (a, b) in r_radix2.iter().zip(r_arb.iter()) {
assert!((a.0 - b.0).abs() < 1e-10);
assert!((a.1 - b.1).abs() < 1e-10);
}
}
#[test]
fn test_fft_arbitrary_parseval() {
let data: Vec<(f64, f64)> = vec![(1.0, 0.0), (2.0, 1.0), (3.0, -1.0), (0.5, 0.5), (4.0, 0.0)];
let n = data.len();
let time_energy: f64 = data.iter().map(|&(r, i)| r * r + i * i).sum();
let freq = fft_arbitrary(&data);
let freq_energy: f64 = freq.iter().map(|&(r, i)| r * r + i * i).sum::<f64>() / n as f64;
assert!((time_energy - freq_energy).abs() < 1e-8, "time={time_energy} freq={freq_energy}");
}
#[test]
fn test_fft_2d_constant() {
let data = vec![(1.0, 0.0); 4]; let result = fft_2d(&data, 2, 2).unwrap();
assert!((result[0].0 - 4.0).abs() < 1e-10);
for i in 1..4 {
assert!(result[i].0.abs() < 1e-10);
assert!(result[i].1.abs() < 1e-10);
}
}
#[test]
fn test_fft_2d_roundtrip() {
let data: Vec<(f64, f64)> = vec![(1.0, 0.0), (2.0, 0.0), (3.0, 0.0), (4.0, 0.0)]; let freq = fft_2d(&data, 2, 2).unwrap();
let recovered = ifft_2d(&freq, 2, 2).unwrap();
for (orig, rec) in data.iter().zip(recovered.iter()) {
assert!((orig.0 - rec.0).abs() < 1e-10);
assert!((orig.1 - rec.1).abs() < 1e-10);
}
}
#[test]
fn test_b6_fft_determinism() {
let data: Vec<(f64, f64)> = vec![(1.0, 2.0), (3.0, 0.0), (5.0, -1.0)];
let r1 = fft_arbitrary(&data);
let r2 = fft_arbitrary(&data);
for (a, b) in r1.iter().zip(r2.iter()) {
assert_eq!(a.0.to_bits(), b.0.to_bits());
assert_eq!(a.1.to_bits(), b.1.to_bits());
}
}
}