Skip to main content

u_numflow/
fourier.rs

1//! Discrete Fourier transform.
2//!
3//! Provides the forward and inverse DFT of a complex sequence of **any
4//! length**, and a real-input convenience. A power-of-two length is
5//! transformed by the iterative radix-2 Cooley-Tukey algorithm; any other
6//! length by Bluestein's chirp-z algorithm, which expresses the DFT as a
7//! convolution and evaluates that with a power-of-two FFT of size ≥ 2n − 1.
8//! Both are O(n log n); the naive DFT is O(n²).
9//!
10//! Conventions: the forward transform is `X_k = Σ_j x_j · exp(−2πi jk/n)`
11//! (no scaling) and the inverse divides by `n`, so `ifft(fft(x)) == x`.
12//!
13//! # References
14//! - Cooley, J. W. & Tukey, J. W. (1965). "An algorithm for the machine
15//!   calculation of complex Fourier series." *Mathematics of Computation*,
16//!   19(90), 297–301.
17//! - Bluestein, L. I. (1970). "A linear filtering approach to the computation
18//!   of discrete Fourier transform." *IEEE Transactions on Audio and
19//!   Electroacoustics*, 18(4), 451–455.
20
21use std::f64::consts::PI;
22use std::ops::{Add, Mul, Sub};
23
24/// A complex number with `f64` parts.
25#[derive(Debug, Clone, Copy, PartialEq, Default)]
26pub struct Complex {
27    /// Real part.
28    pub re: f64,
29    /// Imaginary part.
30    pub im: f64,
31}
32
33impl Complex {
34    /// Creates `re + im·i`.
35    pub const fn new(re: f64, im: f64) -> Self {
36        Self { re, im }
37    }
38
39    /// Creates a real number (imaginary part 0).
40    pub const fn real(re: f64) -> Self {
41        Self { re, im: 0.0 }
42    }
43
44    /// `exp(i·theta)`.
45    pub fn from_polar_unit(theta: f64) -> Self {
46        Self {
47            re: theta.cos(),
48            im: theta.sin(),
49        }
50    }
51
52    /// Complex conjugate.
53    pub fn conj(self) -> Self {
54        Self {
55            re: self.re,
56            im: -self.im,
57        }
58    }
59
60    /// Modulus `|z|`.
61    pub fn norm(self) -> f64 {
62        self.re.hypot(self.im)
63    }
64
65    /// Squared modulus `|z|²`.
66    pub fn norm_sqr(self) -> f64 {
67        self.re * self.re + self.im * self.im
68    }
69
70    /// Argument in radians, in `(−π, π]`.
71    pub fn arg(self) -> f64 {
72        self.im.atan2(self.re)
73    }
74
75    /// Multiplies by a real scalar.
76    pub fn scale(self, k: f64) -> Self {
77        Self {
78            re: self.re * k,
79            im: self.im * k,
80        }
81    }
82}
83
84impl Add for Complex {
85    type Output = Self;
86    fn add(self, rhs: Self) -> Self {
87        Self {
88            re: self.re + rhs.re,
89            im: self.im + rhs.im,
90        }
91    }
92}
93
94impl Sub for Complex {
95    type Output = Self;
96    fn sub(self, rhs: Self) -> Self {
97        Self {
98            re: self.re - rhs.re,
99            im: self.im - rhs.im,
100        }
101    }
102}
103
104impl Mul for Complex {
105    type Output = Self;
106    fn mul(self, rhs: Self) -> Self {
107        Self {
108            re: self.re * rhs.re - self.im * rhs.im,
109            im: self.re * rhs.im + self.im * rhs.re,
110        }
111    }
112}
113
114/// Forward DFT of a complex sequence of any length.
115///
116/// Returns `X_k = Σ_j x_j · exp(−2πi jk/n)` for `k = 0..n`. An empty input
117/// returns an empty output.
118///
119/// # Complexity
120/// O(n log n) — radix-2 for a power-of-two `n`, Bluestein otherwise.
121///
122/// # Examples
123///
124/// ```
125/// use u_numflow::fourier::{fft, Complex};
126///
127/// // A constant sequence has all its energy in the zero frequency.
128/// let x = vec![Complex::real(1.0); 4];
129/// let spectrum = fft(&x);
130/// assert!((spectrum[0].re - 4.0).abs() < 1e-12);
131/// assert!(spectrum[1..].iter().all(|z| z.norm() < 1e-12));
132/// ```
133pub fn fft(input: &[Complex]) -> Vec<Complex> {
134    transform(input, false)
135}
136
137/// Inverse DFT: `x_j = (1/n) Σ_k X_k · exp(+2πi jk/n)`.
138///
139/// `ifft(fft(x))` returns `x` up to floating-point rounding.
140///
141/// # Examples
142///
143/// ```
144/// use u_numflow::fourier::{fft, ifft, Complex};
145///
146/// let x: Vec<Complex> = (0..5).map(|j| Complex::new(j as f64, -(j as f64))).collect();
147/// let back = ifft(&fft(&x));
148/// for (a, b) in x.iter().zip(&back) {
149///     assert!((a.re - b.re).abs() < 1e-12 && (a.im - b.im).abs() < 1e-12);
150/// }
151/// ```
152pub fn ifft(input: &[Complex]) -> Vec<Complex> {
153    let n = input.len();
154    if n == 0 {
155        return Vec::new();
156    }
157    let scale = 1.0 / n as f64;
158    transform(input, true)
159        .into_iter()
160        .map(|z| z.scale(scale))
161        .collect()
162}
163
164/// Forward DFT of a real sequence.
165///
166/// Returns all `n` bins; for real input the upper half mirrors the lower
167/// (`X_{n−k} = conj(X_k)`), so callers that only need the spectrum can stop
168/// at `n / 2`.
169///
170/// # Examples
171///
172/// ```
173/// use u_numflow::fourier::rfft;
174///
175/// // One cycle of a cosine over 8 samples: energy at bins 1 and 7.
176/// let x: Vec<f64> = (0..8).map(|j| (2.0 * std::f64::consts::PI * j as f64 / 8.0).cos()).collect();
177/// let spectrum = rfft(&x);
178/// assert!((spectrum[1].re - 4.0).abs() < 1e-12);
179/// assert!((spectrum[7].re - 4.0).abs() < 1e-12);
180/// ```
181pub fn rfft(input: &[f64]) -> Vec<Complex> {
182    let complex: Vec<Complex> = input.iter().map(|&x| Complex::real(x)).collect();
183    fft(&complex)
184}
185
186/// Dispatches on the length: radix-2 for powers of two, Bluestein otherwise.
187fn transform(input: &[Complex], inverse: bool) -> Vec<Complex> {
188    let n = input.len();
189    if n <= 1 {
190        return input.to_vec();
191    }
192    let mut data = input.to_vec();
193    if n.is_power_of_two() {
194        radix2_in_place(&mut data, inverse);
195        data
196    } else {
197        bluestein(&data, inverse)
198    }
199}
200
201/// Iterative radix-2 Cooley-Tukey; `data.len()` must be a power of two.
202fn radix2_in_place(data: &mut [Complex], inverse: bool) {
203    let n = data.len();
204    debug_assert!(n.is_power_of_two());
205
206    // Bit-reversal permutation.
207    let bits = n.trailing_zeros();
208    for i in 0..n {
209        let j = i.reverse_bits() >> (usize::BITS - bits);
210        if j > i {
211            data.swap(i, j);
212        }
213    }
214
215    let sign = if inverse { 1.0 } else { -1.0 };
216    let mut len = 2;
217    while len <= n {
218        let step = Complex::from_polar_unit(sign * 2.0 * PI / len as f64);
219        for start in (0..n).step_by(len) {
220            let mut w = Complex::real(1.0);
221            for k in 0..len / 2 {
222                let a = data[start + k];
223                let b = data[start + k + len / 2] * w;
224                data[start + k] = a + b;
225                data[start + k + len / 2] = a - b;
226                w = w * step;
227            }
228        }
229        len <<= 1;
230    }
231}
232
233/// Bluestein's chirp-z algorithm for an arbitrary length `n`.
234///
235/// With the chirp `w_j = exp(∓πi j²/n)`, `X_k = w_k · Σ_j (x_j w_j) · conj(w_{k−j})`
236/// is a linear convolution, evaluated as a circular one of power-of-two size
237/// `m ≥ 2n − 1`.
238fn bluestein(input: &[Complex], inverse: bool) -> Vec<Complex> {
239    let n = input.len();
240    let m = (2 * n - 1).next_power_of_two();
241    let sign = if inverse { 1.0 } else { -1.0 };
242
243    // Chirp: the angle is π·j²/n; j² is reduced modulo 2n first so the
244    // argument stays exact for large j.
245    let chirp: Vec<Complex> = (0..n)
246        .map(|j| {
247            let jj = (j * j) % (2 * n);
248            Complex::from_polar_unit(sign * PI * jj as f64 / n as f64)
249        })
250        .collect();
251
252    let mut a = vec![Complex::default(); m];
253    for j in 0..n {
254        a[j] = input[j] * chirp[j];
255    }
256    let mut b = vec![Complex::default(); m];
257    b[0] = chirp[0].conj();
258    for j in 1..n {
259        let c = chirp[j].conj();
260        b[j] = c;
261        b[m - j] = c;
262    }
263
264    radix2_in_place(&mut a, false);
265    radix2_in_place(&mut b, false);
266    let mut c: Vec<Complex> = a.iter().zip(&b).map(|(&x, &y)| x * y).collect();
267    radix2_in_place(&mut c, true);
268    let scale = 1.0 / m as f64;
269
270    (0..n).map(|k| c[k].scale(scale) * chirp[k]).collect()
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use proptest::prelude::*;
277
278    fn naive_dft(x: &[Complex], inverse: bool) -> Vec<Complex> {
279        let n = x.len();
280        let sign = if inverse { 1.0 } else { -1.0 };
281        (0..n)
282            .map(|k| {
283                let mut acc = Complex::default();
284                for (j, &xj) in x.iter().enumerate() {
285                    let angle = sign * 2.0 * PI * (j * k) as f64 / n as f64;
286                    acc = acc + xj * Complex::from_polar_unit(angle);
287                }
288                if inverse {
289                    acc.scale(1.0 / n as f64)
290                } else {
291                    acc
292                }
293            })
294            .collect()
295    }
296
297    fn assert_close(a: &[Complex], b: &[Complex], tol: f64) {
298        assert_eq!(a.len(), b.len());
299        for (i, (x, y)) in a.iter().zip(b).enumerate() {
300            assert!(
301                (*x - *y).norm() < tol,
302                "bin {i}: {x:?} vs {y:?} (|Δ| = {})",
303                (*x - *y).norm()
304            );
305        }
306    }
307
308    #[test]
309    fn empty_and_singleton() {
310        assert!(fft(&[]).is_empty());
311        assert!(ifft(&[]).is_empty());
312        let one = [Complex::new(2.0, -3.0)];
313        assert_eq!(fft(&one), one.to_vec());
314        assert_eq!(ifft(&one), one.to_vec());
315    }
316
317    #[test]
318    fn impulse_is_flat() {
319        let mut x = vec![Complex::default(); 7];
320        x[0] = Complex::real(1.0);
321        for z in fft(&x) {
322            assert!((z.re - 1.0).abs() < 1e-12 && z.im.abs() < 1e-12);
323        }
324    }
325
326    #[test]
327    fn sine_lands_in_its_bin_for_every_length_kind() {
328        for n in [8usize, 12, 30, 64, 97] {
329            let x: Vec<f64> = (0..n)
330                .map(|j| (2.0 * PI * 3.0 * j as f64 / n as f64).sin())
331                .collect();
332            let spectrum = rfft(&x);
333            let power: Vec<f64> = spectrum.iter().map(|z| z.norm_sqr()).collect();
334            let (argmax, _) = power[..n / 2 + 1]
335                .iter()
336                .enumerate()
337                .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
338                .unwrap();
339            assert_eq!(argmax, 3, "n = {n}");
340            assert!((spectrum[3].im + n as f64 / 2.0).abs() < 1e-9, "n = {n}");
341        }
342    }
343
344    proptest! {
345        #![proptest_config(ProptestConfig::with_cases(128))]
346
347        #[test]
348        fn matches_the_naive_dft_for_any_length(
349            data in prop::collection::vec((-100.0f64..100.0, -100.0f64..100.0), 1..=64)
350        ) {
351            let x: Vec<Complex> = data.iter().map(|&(re, im)| Complex::new(re, im)).collect();
352            let n = x.len() as f64;
353            let tol = 1e-9 * n * 100.0;
354            let forward = fft(&x);
355            assert_close(&forward, &naive_dft(&x, false), tol);
356            assert_close(&ifft(&forward), &x, tol);
357            assert_close(&ifft(&x), &naive_dft(&x, true), tol);
358        }
359
360        /// Parseval: Σ|x_j|² = (1/n) Σ|X_k|².
361        #[test]
362        fn parseval_holds(
363            data in prop::collection::vec(-100.0f64..100.0, 1..=100)
364        ) {
365            let n = data.len() as f64;
366            let time: f64 = data.iter().map(|x| x * x).sum();
367            let freq: f64 = rfft(&data).iter().map(|z| z.norm_sqr()).sum::<f64>() / n;
368            prop_assert!((time - freq).abs() < 1e-7 * (1.0 + time), "{time} vs {freq}");
369        }
370
371        /// Real input: the upper half of the spectrum is the conjugate mirror.
372        #[test]
373        fn real_input_has_hermitian_spectrum(
374            data in prop::collection::vec(-100.0f64..100.0, 2..=100)
375        ) {
376            let n = data.len();
377            let spectrum = rfft(&data);
378            for k in 1..n {
379                let mirror = spectrum[n - k].conj();
380                prop_assert!((spectrum[k] - mirror).norm() < 1e-7 * (1.0 + spectrum[k].norm()));
381            }
382            prop_assert!(spectrum[0].im.abs() < 1e-9);
383        }
384    }
385}