fpm_rs/metrics/complex_field/
statistics.rs1use ndarray::ArrayView2;
4use num_complex::Complex64;
5
6use crate::{
7 Result,
8 backend::{Backend, CpuBackend, FftDirection},
9};
10
11#[derive(Clone, Debug, PartialEq)]
23pub struct RadialFourierSpectrum {
24 pub radius_px: Vec<f64>,
26 pub power: Vec<f64>,
28 pub sample_count: Vec<usize>,
30}
31
32pub fn radial_fourier_spectrum(field: ArrayView2<'_, Complex64>) -> Result<RadialFourierSpectrum> {
37 let shape = field.dim();
38 let mut spectrum: Vec<_> = field.iter().copied().collect();
39 let backend = CpuBackend::new(shape, shape)?;
40 let mut column_scratch = vec![Complex64::default(); shape.0];
41 backend.fft2(
42 &mut spectrum,
43 shape,
44 FftDirection::Forward,
45 &mut column_scratch,
46 )?;
47
48 let center_row = shape.0 / 2;
49 let center_column = shape.1 / 2;
50 let max_radius = ((center_row as f64).hypot(center_column as f64)).floor() as usize;
51 let mut power = vec![0.0; max_radius + 1];
52 let mut sample_count = vec![0_usize; max_radius + 1];
53 let normalization = field.len() as f64;
54
55 for row in 0..shape.0 {
56 let frequency_row = if row <= shape.0 / 2 {
57 row as isize
58 } else {
59 row as isize - shape.0 as isize
60 };
61 for column in 0..shape.1 {
62 let frequency_column = if column <= shape.1 / 2 {
63 column as isize
64 } else {
65 column as isize - shape.1 as isize
66 };
67 let radius = ((frequency_row * frequency_row + frequency_column * frequency_column)
68 as f64)
69 .sqrt()
70 .floor() as usize;
71 power[radius] += spectrum[row * shape.1 + column].norm_sqr() * normalization;
72 sample_count[radius] += 1;
73 }
74 }
75
76 for (value, count) in power.iter_mut().zip(&sample_count) {
77 *value /= *count as f64;
78 }
79
80 Ok(RadialFourierSpectrum {
81 radius_px: (0..power.len()).map(|bin| bin as f64).collect(),
82 power,
83 sample_count,
84 })
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use ndarray::Array2;
91
92 #[test]
93 fn constant_field_has_power_only_at_dc() {
94 let field = Array2::from_elem((4, 4), Complex64::new(1.0, 0.0));
95
96 let spectrum = radial_fourier_spectrum(field.view()).unwrap();
97
98 assert_eq!(spectrum.radius_px, vec![0.0, 1.0, 2.0]);
99 assert_eq!(spectrum.sample_count, vec![1, 8, 7]);
100 assert!((spectrum.power[0] - 16.0).abs() < 1e-12);
101 assert!(spectrum.power[1..].iter().all(|value| value.abs() < 1e-12));
102 }
103}