Skip to main content

ferray_window/windows/
mod.rs

1// ferray-window: Window functions for signal processing and spectral analysis
2//
3// Implements NumPy-equivalent window functions: bartlett, blackman, hamming,
4// hanning, and kaiser. Each returns an Array1<f64> of the specified length M.
5
6use ferray_core::Array;
7use ferray_core::dimension::Ix1;
8use ferray_core::error::{FerrayError, FerrayResult};
9
10use std::f64::consts::PI;
11
12// Re-use the Abramowitz & Stegun Bessel I_0 polynomial approximation
13// from ferray-ufunc instead of shipping a second copy (see #530).
14// The previous copy had already drifted in whitespace; funnelling
15// through the canonical crate keeps both the window code and the
16// ufunc in sync.
17use ferray_ufunc::ops::special::bessel_i0_scalar;
18
19/// Build a window array of length `m` by evaluating `f` at every sample
20/// `0..m`. Handles the shared `m == 0` / `m == 1` edge cases that every
21/// NumPy-equivalent window has (see issue #292 for the original
22/// five-way copy of this pattern).
23#[inline]
24fn gen_window<F: FnMut(usize) -> f64>(m: usize, mut f: F) -> FerrayResult<Array<f64, Ix1>> {
25    if m == 0 {
26        return Array::from_vec(Ix1::new([0]), vec![]);
27    }
28    if m == 1 {
29        return Array::from_vec(Ix1::new([1]), vec![1.0]);
30    }
31    let mut data = Vec::with_capacity(m);
32    for n in 0..m {
33        data.push(f(n));
34    }
35    Array::from_vec(Ix1::new([m]), data)
36}
37
38/// Return the Bartlett (triangular) window of length `m`.
39///
40/// The Bartlett window is defined as:
41///   w(n) = 2/(M-1) * ((M-1)/2 - |n - (M-1)/2|)
42///
43/// This is equivalent to `numpy.bartlett(M)`.
44///
45/// # Errors
46/// Returns an error only if internal array construction fails.
47pub fn bartlett(m: usize) -> FerrayResult<Array<f64, Ix1>> {
48    let half = (m.saturating_sub(1)) as f64 / 2.0;
49    gen_window(m, |n| 1.0 - ((n as f64 - half) / half).abs())
50}
51
52/// Return the Blackman window of length `m`.
53///
54/// The Blackman window is defined as:
55///   w(n) = 0.42 - 0.5 * cos(2*pi*n/(M-1)) + 0.08 * cos(4*pi*n/(M-1))
56///
57/// This is equivalent to `numpy.blackman(M)`.
58///
59/// # Errors
60/// Returns an error only if internal array construction fails.
61pub fn blackman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
62    let denom = (m.saturating_sub(1)) as f64;
63    gen_window(m, |n| {
64        let x = n as f64;
65        0.42 - 0.5 * (2.0 * PI * x / denom).cos() + 0.08 * (4.0 * PI * x / denom).cos()
66    })
67}
68
69/// Return the Hamming window of length `m`.
70///
71/// The Hamming window is defined as:
72///   w(n) = 0.54 - 0.46 * cos(2*pi*n/(M-1))
73///
74/// This is equivalent to `numpy.hamming(M)`.
75///
76/// # Errors
77/// Returns an error only if internal array construction fails.
78pub fn hamming(m: usize) -> FerrayResult<Array<f64, Ix1>> {
79    let denom = (m.saturating_sub(1)) as f64;
80    gen_window(m, |n| 0.54 - 0.46 * (2.0 * PI * n as f64 / denom).cos())
81}
82
83/// Return the Hann (Hanning) window of length `m`.
84///
85/// The Hann window is defined as:
86///   w(n) = 0.5 * (1 - cos(2*pi*n/(M-1)))
87///
88/// NumPy calls this function `hanning`. This is equivalent to `numpy.hanning(M)`.
89///
90/// # Errors
91/// Returns an error only if internal array construction fails.
92pub fn hanning(m: usize) -> FerrayResult<Array<f64, Ix1>> {
93    let denom = (m.saturating_sub(1)) as f64;
94    gen_window(m, |n| 0.5 * (1.0 - (2.0 * PI * n as f64 / denom).cos()))
95}
96
97/// Return the Kaiser window of length `m` with shape parameter `beta`.
98///
99/// The Kaiser window is defined as:
100///   w(n) = I_0(beta * sqrt(1 - ((2n/(M-1)) - 1)^2)) / I_0(beta)
101///
102/// where I_0 is the modified Bessel function of the first kind, order 0.
103///
104/// This is equivalent to `numpy.kaiser(M, beta)`.
105///
106/// # Edge Cases
107/// - `m == 0`: returns an empty array.
108/// - `m == 1`: returns `[1.0]`.
109///
110/// # Errors
111/// Returns `FerrayError::InvalidValue` if `beta` is NaN or if `beta`
112/// exceeds the range where `I_0(beta)` can be computed in f64. The
113/// asymptotic branch uses `exp(beta) / sqrt(beta)` internally, so
114/// `|beta| > ~709` overflows to infinity and the normalized kaiser
115/// window would reduce to `Inf / Inf = NaN` for every sample
116/// (#294).
117pub fn kaiser(m: usize, beta: f64) -> FerrayResult<Array<f64, Ix1>> {
118    if beta.is_nan() {
119        return Err(FerrayError::invalid_value("kaiser: beta must not be NaN"));
120    }
121    // I_0 is an even function, so kaiser(m, -beta) == kaiser(m, beta).
122    // Accept negative beta for NumPy compatibility.
123    let beta = beta.abs();
124    // Reject beta values where `exp(beta)` overflows f64 (ln(f64::MAX)
125    // ≈ 709.78). We use a slightly conservative threshold so the
126    // clipped output stays finite even after the asymptotic-series
127    // polynomial corrections.
128    const BETA_OVERFLOW_THRESHOLD: f64 = 708.0;
129    if beta > BETA_OVERFLOW_THRESHOLD {
130        return Err(FerrayError::invalid_value(format!(
131            "kaiser: |beta| = {beta} exceeds the safe range ({BETA_OVERFLOW_THRESHOLD}) \
132             for f64 I_0; the window would be NaN everywhere"
133        )));
134    }
135    let i0_beta = bessel_i0_scalar::<f64>(beta);
136    let alpha = (m.saturating_sub(1)) as f64 / 2.0;
137    gen_window(m, |n| {
138        let t = (n as f64 - alpha) / alpha;
139        let arg = beta * (1.0 - t * t).max(0.0).sqrt();
140        bessel_i0_scalar::<f64>(arg) / i0_beta
141    })
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    // Helper: compare two slices within tolerance
149    fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
150        assert_eq!(
151            actual.len(),
152            expected.len(),
153            "length mismatch: {} vs {}",
154            actual.len(),
155            expected.len()
156        );
157        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
158            assert!(
159                (a - e).abs() <= tol,
160                "element {i}: {a} vs {e} (diff = {})",
161                (a - e).abs()
162            );
163        }
164    }
165
166    // -----------------------------------------------------------------------
167    // Edge cases: M=0 and M=1
168    // -----------------------------------------------------------------------
169
170    #[test]
171    fn bartlett_m0() {
172        let w = bartlett(0).unwrap();
173        assert_eq!(w.shape(), &[0]);
174    }
175
176    #[test]
177    fn bartlett_m1() {
178        let w = bartlett(1).unwrap();
179        assert_eq!(w.as_slice().unwrap(), &[1.0]);
180    }
181
182    #[test]
183    fn blackman_m0() {
184        let w = blackman(0).unwrap();
185        assert_eq!(w.shape(), &[0]);
186    }
187
188    #[test]
189    fn blackman_m1() {
190        let w = blackman(1).unwrap();
191        assert_eq!(w.as_slice().unwrap(), &[1.0]);
192    }
193
194    #[test]
195    fn hamming_m0() {
196        let w = hamming(0).unwrap();
197        assert_eq!(w.shape(), &[0]);
198    }
199
200    #[test]
201    fn hamming_m1() {
202        let w = hamming(1).unwrap();
203        assert_eq!(w.as_slice().unwrap(), &[1.0]);
204    }
205
206    #[test]
207    fn hanning_m0() {
208        let w = hanning(0).unwrap();
209        assert_eq!(w.shape(), &[0]);
210    }
211
212    #[test]
213    fn hanning_m1() {
214        let w = hanning(1).unwrap();
215        assert_eq!(w.as_slice().unwrap(), &[1.0]);
216    }
217
218    #[test]
219    fn kaiser_m0() {
220        let w = kaiser(0, 14.0).unwrap();
221        assert_eq!(w.shape(), &[0]);
222    }
223
224    #[test]
225    fn kaiser_m1() {
226        let w = kaiser(1, 14.0).unwrap();
227        assert_eq!(w.as_slice().unwrap(), &[1.0]);
228    }
229
230    #[test]
231    fn kaiser_negative_beta() {
232        // Negative beta should produce the same result as positive beta (I0 is even)
233        let pos = kaiser(5, 1.0).unwrap();
234        let neg = kaiser(5, -1.0).unwrap();
235        assert_eq!(pos.as_slice().unwrap(), neg.as_slice().unwrap());
236    }
237
238    #[test]
239    fn kaiser_nan_beta() {
240        assert!(kaiser(5, f64::NAN).is_err());
241    }
242
243    // -----------------------------------------------------------------------
244    // AC-1: bartlett(5) matches np.bartlett(5) to within 4 ULPs
245    // -----------------------------------------------------------------------
246    // np.bartlett(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
247    #[test]
248    fn bartlett_5_ac1() {
249        let w = bartlett(5).unwrap();
250        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
251        assert_close(w.as_slice().unwrap(), &expected, 1e-14);
252    }
253
254    // -----------------------------------------------------------------------
255    // AC-2: kaiser(5, 14.0) matches np.kaiser(5, 14.0) to within 4 ULPs
256    // -----------------------------------------------------------------------
257    // np.kaiser(5, 14.0) = [7.72686684e-06, 1.64932188e-01, 1.0, 1.64932188e-01, 7.72686684e-06]
258    #[test]
259    fn kaiser_5_14_ac2() {
260        let w = kaiser(5, 14.0).unwrap();
261        let s = w.as_slice().unwrap();
262        assert_eq!(s.len(), 5);
263        // NumPy reference values (verified via np.kaiser(5, 14.0))
264        let expected: [f64; 5] = [
265            7.72686684e-06,
266            1.64932188e-01,
267            1.0,
268            1.64932188e-01,
269            7.72686684e-06,
270        ];
271        // Bessel polynomial approximation has limited precision (~6 digits)
272        for (i, (&a, &e)) in s.iter().zip(expected.iter()).enumerate() {
273            let tol = if e.abs() < 1e-4 { 1e-8 } else { 1e-6 };
274            assert!(
275                (a - e).abs() <= tol,
276                "kaiser element {i}: {a} vs {e} (diff = {})",
277                (a - e).abs()
278            );
279        }
280    }
281
282    // -----------------------------------------------------------------------
283    // AC-3: All 5 window functions return correct length and match NumPy fixtures
284    // -----------------------------------------------------------------------
285
286    // np.blackman(5) = [-1.38777878e-17, 3.40000000e-01, 1.00000000e+00, 3.40000000e-01, -1.38777878e-17]
287    #[test]
288    fn blackman_5() {
289        let w = blackman(5).unwrap();
290        assert_eq!(w.shape(), &[5]);
291        let s = w.as_slice().unwrap();
292        let expected = [
293            -1.38777878e-17,
294            3.40000000e-01,
295            1.00000000e+00,
296            3.40000000e-01,
297            -1.38777878e-17,
298        ];
299        assert_close(s, &expected, 1e-10);
300    }
301
302    // np.hamming(5) = [0.08, 0.54, 1.0, 0.54, 0.08]
303    #[test]
304    fn hamming_5() {
305        let w = hamming(5).unwrap();
306        assert_eq!(w.shape(), &[5]);
307        let s = w.as_slice().unwrap();
308        let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
309        assert_close(s, &expected, 1e-14);
310    }
311
312    // np.hanning(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
313    #[test]
314    fn hanning_5() {
315        let w = hanning(5).unwrap();
316        assert_eq!(w.shape(), &[5]);
317        let s = w.as_slice().unwrap();
318        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
319        assert_close(s, &expected, 1e-14);
320    }
321
322    // Larger window: np.bartlett(12)
323    #[test]
324    fn bartlett_12() {
325        let w = bartlett(12).unwrap();
326        assert_eq!(w.shape(), &[12]);
327        let s = w.as_slice().unwrap();
328        // First and last should be 0, peak near center
329        assert!((s[0] - 0.0).abs() < 1e-14);
330        assert!((s[11] - 0.0).abs() < 1e-14);
331        // Symmetric
332        for i in 0..6 {
333            assert!((s[i] - s[11 - i]).abs() < 1e-14, "symmetry at {i}");
334        }
335    }
336
337    // Symmetry test for all windows
338    #[test]
339    fn all_windows_symmetric() {
340        let m = 7;
341        let windows: Vec<Array<f64, Ix1>> = vec![
342            bartlett(m).unwrap(),
343            blackman(m).unwrap(),
344            hamming(m).unwrap(),
345            hanning(m).unwrap(),
346            kaiser(m, 5.0).unwrap(),
347        ];
348        for (idx, w) in windows.iter().enumerate() {
349            let s = w.as_slice().unwrap();
350            for i in 0..m / 2 {
351                assert!(
352                    (s[i] - s[m - 1 - i]).abs() < 1e-12,
353                    "window {idx} not symmetric at {i}"
354                );
355            }
356        }
357    }
358
359    // All windows peak at center
360    #[test]
361    fn all_windows_peak_at_center() {
362        let m = 9;
363        let windows: Vec<Array<f64, Ix1>> = vec![
364            bartlett(m).unwrap(),
365            blackman(m).unwrap(),
366            hamming(m).unwrap(),
367            hanning(m).unwrap(),
368            kaiser(m, 5.0).unwrap(),
369        ];
370        for (idx, w) in windows.iter().enumerate() {
371            let s = w.as_slice().unwrap();
372            let center = s[m / 2];
373            assert!(
374                (center - 1.0).abs() < 1e-10,
375                "window {idx} center = {center}, expected 1.0"
376            );
377        }
378    }
379
380    // Kaiser with beta=0 should be a rectangular window (all ones)
381    #[test]
382    fn kaiser_beta_zero() {
383        let w = kaiser(5, 0.0).unwrap();
384        let s = w.as_slice().unwrap();
385        for &v in s {
386            assert!((v - 1.0).abs() < 1e-10, "expected 1.0, got {v}");
387        }
388    }
389
390    #[test]
391    fn bessel_i0_scalar_zero() {
392        assert!((bessel_i0_scalar::<f64>(0.0) - 1.0).abs() < 1e-6);
393    }
394
395    #[test]
396    fn bessel_i0_scalar_known() {
397        // Issue #296: tighten the tolerances. The Abramowitz & Stegun
398        // approximation is rated ~1e-7 in the polynomial branch and
399        // ~1e-7 in the asymptotic branch per its documentation.
400        // I0(1) ~ 1.2660658777520082
401        assert!((bessel_i0_scalar::<f64>(1.0) - 1.266_065_877_752_008_2).abs() < 1e-7);
402        // I0(5) ~ 27.239871823604443 (asymptotic branch)
403        assert!((bessel_i0_scalar::<f64>(5.0) - 27.239_871_823_604_443).abs() < 1e-5);
404        // I0(10) ~ 2815.7166284662544
405        let expected_i0_10 = 2_815.716_628_466_254;
406        assert!((bessel_i0_scalar::<f64>(10.0) - expected_i0_10).abs() / expected_i0_10 < 1e-5);
407    }
408
409    // ----- Edge-length window coverage (#295) -----
410
411    #[test]
412    fn bartlett_m2_is_zeros() {
413        // Bartlett of length 2: w(n) = 1 - |2n/(M-1) - 1|
414        // n=0: 1 - |0 - 1| = 0
415        // n=1: 1 - |2 - 1| = 0
416        let w = bartlett(2).unwrap();
417        assert_eq!(w.as_slice().unwrap(), &[0.0, 0.0]);
418    }
419
420    #[test]
421    fn hanning_m2_is_zeros() {
422        // Hann of length 2: w(n) = 0.5 * (1 - cos(2*pi*n/(M-1)))
423        // n=0: 0.5 * (1 - cos(0)) = 0
424        // n=1: 0.5 * (1 - cos(2*pi)) = 0
425        let w = hanning(2).unwrap();
426        let s = w.as_slice().unwrap();
427        assert!(s[0].abs() < 1e-14);
428        assert!(s[1].abs() < 1e-14);
429    }
430
431    #[test]
432    fn bartlett_even_length_is_symmetric() {
433        // Even-length windows have no single "center"; symmetry holds
434        // around the midpoint between indices (M/2 - 1) and (M/2).
435        for &m in &[4usize, 6, 8, 10] {
436            let w = bartlett(m).unwrap();
437            let s = w.as_slice().unwrap();
438            for i in 0..m / 2 {
439                assert!(
440                    (s[i] - s[m - 1 - i]).abs() < 1e-14,
441                    "bartlett({m}) not symmetric at {i}"
442                );
443            }
444        }
445    }
446
447    #[test]
448    fn blackman_even_length_is_symmetric() {
449        for &m in &[4usize, 6, 8, 10] {
450            let w = blackman(m).unwrap();
451            let s = w.as_slice().unwrap();
452            for i in 0..m / 2 {
453                assert!(
454                    (s[i] - s[m - 1 - i]).abs() < 1e-14,
455                    "blackman({m}) not symmetric at {i}"
456                );
457            }
458        }
459    }
460
461    #[test]
462    fn kaiser_large_beta_errors() {
463        // Issue #294: beta above the f64 safe range should error,
464        // not silently produce NaN.
465        assert!(kaiser(8, 800.0).is_err());
466        assert!(kaiser(8, 1000.0).is_err());
467        // 700 is still safe.
468        assert!(kaiser(8, 700.0).is_ok());
469    }
470}