1use num_complex::Complex;
22use crate::det_math::{det_sincos, det_hypot};
23use std::f64::consts::PI;
24
25pub type Complex32 = Complex<f32>;
27
28pub struct Spectrum2D {
30 pub data: Vec<Complex32>,
31 pub width: usize,
32 pub height: usize,
33}
34
35struct BluesteinPlan {
44 n: usize,
45 m: usize, chirp: Vec<Complex32>,
47 b_hat: Vec<Complex32>, }
49
50impl BluesteinPlan {
51 fn new(n: usize, sign: f64) -> Self {
53 let m = next_pow2(2 * n - 1);
54
55 let mut chirp = vec![Complex32::new(0.0, 0.0); n];
57 for k in 0..n {
58 let angle = sign * PI * (k as f64 * k as f64) / n as f64;
59 let (s, c) = det_sincos(angle);
60 chirp[k] = Complex32::new(c as f32, s as f32);
61 }
62
63 let mut b = vec![Complex32::new(0.0, 0.0); m];
65 b[0] = chirp[0];
66 for k in 1..n {
67 b[k] = chirp[k];
68 b[m - k] = chirp[k];
69 }
70
71 fft_radix2_f32(&mut b, -1.0);
73
74 BluesteinPlan { n, m, chirp, b_hat: b }
75 }
76
77 fn execute(&self, input: &[Complex32]) -> Vec<Complex32> {
79 debug_assert_eq!(input.len(), self.n);
80
81 let mut a = vec![Complex32::new(0.0, 0.0); self.m];
83 for k in 0..self.n {
84 a[k] = input[k] * self.chirp[k].conj();
85 }
86
87 fft_radix2_f32(&mut a, -1.0);
89 for i in 0..self.m {
90 a[i] *= self.b_hat[i];
91 }
92 fft_radix2_f32(&mut a, 1.0);
93
94 let inv_m = 1.0 / self.m as f32;
96 let mut result = vec![Complex32::new(0.0, 0.0); self.n];
97 for k in 0..self.n {
98 result[k] = a[k] * inv_m * self.chirp[k].conj();
99 }
100
101 result
102 }
103}
104
105fn next_pow2(n: usize) -> usize {
111 let mut p = 1;
112 while p < n {
113 p <<= 1;
114 }
115 p
116}
117
118fn fft_radix2_f32(data: &mut [Complex32], sign: f64) {
121 let n = data.len();
122 debug_assert!(n.is_power_of_two());
123 if n <= 1 {
124 return;
125 }
126
127 let mut j = 0usize;
129 for i in 1..n {
130 let mut bit = n >> 1;
131 while j & bit != 0 {
132 j ^= bit;
133 bit >>= 1;
134 }
135 j ^= bit;
136 if i < j {
137 data.swap(i, j);
138 }
139 }
140
141 let mut twiddles: Vec<Complex32> = Vec::with_capacity(n / 2);
143
144 let mut len = 2;
146 while len <= n {
147 let half = len / 2;
148 let angle_step = sign * PI / half as f64;
149
150 twiddles.clear();
158 twiddles.extend((0..half).map(|k| {
159 let angle = angle_step * k as f64;
160 let (s, c) = det_sincos(angle);
161 Complex32::new(c as f32, s as f32)
162 }));
163
164 for start in (0..n).step_by(len) {
165 for k in 0..half {
166 let w = twiddles[k];
167 let u = data[start + k];
168 let v = data[start + k + half] * w;
169 data[start + k] = u + v;
170 data[start + k + half] = u - v;
171 }
172 }
173 len <<= 1;
174 }
175}
176
177fn fft1d_f32_with_plan(input: &[Complex32], sign: f64, plan: Option<&BluesteinPlan>) -> Vec<Complex32> {
181 let n = input.len();
182 if n == 0 {
183 return vec![];
184 }
185 if n == 1 {
186 return input.to_vec();
187 }
188 if n.is_power_of_two() {
189 let mut buf = input.to_vec();
190 fft_radix2_f32(&mut buf, sign);
191 return buf;
192 }
193
194 if let Some(p) = plan {
196 debug_assert_eq!(p.n, n);
197 return p.execute(input);
198 }
199
200 let temp_plan = BluesteinPlan::new(n, sign);
202 temp_plan.execute(input)
203}
204
205#[allow(dead_code)]
207fn fft1d_f32(data: &[Complex32]) -> Vec<Complex32> {
208 fft1d_f32_with_plan(data, -1.0, None)
209}
210
211#[allow(dead_code)]
213fn ifft1d_f32(data: &[Complex32]) -> Vec<Complex32> {
214 fft1d_f32_with_plan(data, 1.0, None)
215}
216
217pub fn fft2d(pixels: &[f64], width: usize, height: usize) -> Spectrum2D {
227 assert_eq!(pixels.len(), width * height);
228
229 let mut data: Vec<Complex32> = pixels.iter().map(|&v| Complex32::new(v as f32, 0.0)).collect();
230
231 let row_plan = if !width.is_power_of_two() && width > 1 {
233 Some(BluesteinPlan::new(width, -1.0))
234 } else {
235 None
236 };
237 let col_plan = if !height.is_power_of_two() && height > 1 {
238 Some(BluesteinPlan::new(height, -1.0))
239 } else {
240 None
241 };
242
243 for row in 0..height {
245 let start = row * width;
246 let row_data = &data[start..start + width];
247 let transformed = fft1d_f32_with_plan(row_data, -1.0, row_plan.as_ref());
248 data[start..start + width].copy_from_slice(&transformed);
249 }
250
251 let mut col_buf = vec![Complex32::new(0.0, 0.0); height];
254 for col in 0..width {
255 for r in 0..height {
257 col_buf[r] = data[r * width + col];
258 }
259 let transformed = fft1d_f32_with_plan(&col_buf, -1.0, col_plan.as_ref());
261 for r in 0..height {
263 data[r * width + col] = transformed[r];
264 }
265 }
266
267 Spectrum2D { data, width, height }
268}
269
270pub fn ifft2d(spectrum: &Spectrum2D) -> Vec<f64> {
276 let width = spectrum.width;
277 let height = spectrum.height;
278 let mut data = spectrum.data.clone();
279
280 let row_plan = if !width.is_power_of_two() && width > 1 {
282 Some(BluesteinPlan::new(width, 1.0))
283 } else {
284 None
285 };
286 let col_plan = if !height.is_power_of_two() && height > 1 {
287 Some(BluesteinPlan::new(height, 1.0))
288 } else {
289 None
290 };
291
292 for row in 0..height {
294 let start = row * width;
295 let row_data = &data[start..start + width];
296 let transformed = fft1d_f32_with_plan(row_data, 1.0, row_plan.as_ref());
297 data[start..start + width].copy_from_slice(&transformed);
298 }
299
300 let mut col_buf = vec![Complex32::new(0.0, 0.0); height];
302 for col in 0..width {
303 for r in 0..height {
305 col_buf[r] = data[r * width + col];
306 }
307 let transformed = fft1d_f32_with_plan(&col_buf, 1.0, col_plan.as_ref());
309 for r in 0..height {
311 data[r * width + col] = transformed[r];
312 }
313 }
314
315 let norm = 1.0 / (width * height) as f64;
317 let mut result = vec![0.0f64; width * height];
318 for i in 0..data.len() {
319 result[i] = data[i].re as f64 * norm;
320 }
321
322 result
323}
324
325pub fn magnitude_spectrum(spectrum: &Spectrum2D) -> Vec<f32> {
327 spectrum.data.iter().map(|c| {
328 det_hypot(c.re as f64, c.im as f64) as f32
330 }).collect()
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn fft_ifft_roundtrip() {
339 let width = 16;
340 let height = 16;
341 let pixels: Vec<f64> = (0..width * height).map(|i| (i as f64) * 0.1 + 50.0).collect();
342
343 let spectrum = fft2d(&pixels, width, height);
344 let recovered = ifft2d(&spectrum);
345
346 for i in 0..pixels.len() {
347 assert!(
348 (pixels[i] - recovered[i]).abs() < 1e-3,
349 "Mismatch at {i}: expected {}, got {}",
350 pixels[i],
351 recovered[i]
352 );
353 }
354 }
355
356 #[test]
357 fn fft_ifft_roundtrip_non_pow2() {
358 let width = 12;
360 let height = 10;
361 let pixels: Vec<f64> = (0..width * height).map(|i| (i as f64) * 0.3 + 20.0).collect();
362
363 let spectrum = fft2d(&pixels, width, height);
364 let recovered = ifft2d(&spectrum);
365
366 for i in 0..pixels.len() {
367 assert!(
368 (pixels[i] - recovered[i]).abs() < 0.1,
369 "Mismatch at {i}: expected {}, got {}",
370 pixels[i],
371 recovered[i]
372 );
373 }
374 }
375
376 #[test]
377 fn parseval_theorem() {
378 let width = 8;
379 let height = 8;
380 let pixels: Vec<f64> = (0..width * height).map(|i| ((i * 7 + 3) % 256) as f64).collect();
381
382 let spatial_energy: f64 = pixels.iter().map(|v| v * v).sum();
383
384 let spectrum = fft2d(&pixels, width, height);
385 let freq_energy: f64 = spectrum.data.iter().map(|c| {
386 let re = c.re as f64;
387 let im = c.im as f64;
388 re * re + im * im
389 }).sum();
390
391 let n = (width * height) as f64;
392 assert!(
394 (spatial_energy - freq_energy / n).abs() < 10.0,
395 "Parseval's theorem violated: spatial={spatial_energy}, freq/N={}", freq_energy / n
396 );
397 }
398
399 #[test]
400 fn dc_component_is_sum() {
401 let width = 4;
402 let height = 4;
403 let pixels = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0,
404 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0];
405
406 let spectrum = fft2d(&pixels, width, height);
407
408 let expected_dc: f64 = pixels.iter().sum();
409 assert!(
411 (spectrum.data[0].re as f64 - expected_dc).abs() < 0.1,
412 "DC component should be sum of all pixels: expected {expected_dc}, got {}",
413 spectrum.data[0].re
414 );
415 assert!((spectrum.data[0].im as f64).abs() < 0.1);
416 }
417
418 #[test]
419 fn fft1d_basic() {
420 let input = vec![
422 Complex32::new(1.0, 0.0),
423 Complex32::new(0.0, 0.0),
424 Complex32::new(0.0, 0.0),
425 Complex32::new(0.0, 0.0),
426 ];
427 let output = fft1d_f32(&input);
428 for k in 0..4 {
429 assert!((output[k].re - 1.0).abs() < 1e-5, "Re[{k}]={}", output[k].re);
430 assert!(output[k].im.abs() < 1e-5, "Im[{k}]={}", output[k].im);
431 }
432 }
433
434 #[test]
435 fn bluestein_matches_radix2() {
436 let n = 8;
438 let input: Vec<Complex32> = (0..n).map(|i| Complex32::new((i * 3 + 1) as f32, (i * 2) as f32)).collect();
439
440 let mut radix2_buf = input.clone();
441 fft_radix2_f32(&mut radix2_buf, -1.0);
442
443 let _plan = BluesteinPlan::new(n, -1.0);
445 let n2 = 7;
448 let input2: Vec<Complex32> = (0..n2).map(|i| Complex32::new((i * 3 + 1) as f32, (i * 2) as f32)).collect();
449 let plan2 = BluesteinPlan::new(n2, -1.0);
450 let result_plan = plan2.execute(&input2);
451 let result_direct = fft1d_f32(&input2);
452 for k in 0..n2 {
453 assert!(
454 (result_plan[k].re - result_direct[k].re).abs() < 1e-3 &&
455 (result_plan[k].im - result_direct[k].im).abs() < 1e-3,
456 "Plan vs direct mismatch at {k}: plan={}, direct={}",
457 result_plan[k], result_direct[k]
458 );
459 }
460
461 let result_r2 = fft1d_f32(&input);
463 for k in 0..n {
464 assert!(
465 (radix2_buf[k].re - result_r2[k].re).abs() < 1e-3 &&
466 (radix2_buf[k].im - result_r2[k].im).abs() < 1e-3,
467 "Mismatch at {k}: radix2={}, fft1d={}",
468 radix2_buf[k], result_r2[k]
469 );
470 }
471 }
472
473 #[test]
474 fn bluestein_plan_reuse() {
475 let n = 13; let plan = BluesteinPlan::new(n, -1.0);
478
479 let input1: Vec<Complex32> = (0..n).map(|i| Complex32::new(i as f32, 0.0)).collect();
480 let input2: Vec<Complex32> = (0..n).map(|i| Complex32::new(0.0, i as f32)).collect();
481
482 let r1a = plan.execute(&input1);
483 let r2 = plan.execute(&input2);
484 let r1b = plan.execute(&input1);
485
486 for k in 0..n {
487 assert!(
488 (r1a[k].re - r1b[k].re).abs() < 1e-5 &&
489 (r1a[k].im - r1b[k].im).abs() < 1e-5,
490 "Plan reuse gave different results at {k}: first={}, second={}",
491 r1a[k], r1b[k]
492 );
493 }
494 assert_eq!(r2.len(), n);
496 }
497}