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/// Scalar modified Bessel function I_0(x) using polynomial approximation.
13///
14/// Uses the Abramowitz and Stegun approximation for |x| <= 3.75 and
15/// an asymptotic expansion for |x| > 3.75.
16fn bessel_i0_scalar(x: f64) -> f64 {
17    let ax = x.abs();
18
19    if ax <= 3.75 {
20        let t = (ax / 3.75).powi(2);
21        1.0 + t
22            * (3.5156229
23                + t * (3.0899424
24                    + t * (1.2067492 + t * (0.2659732 + t * (0.0360768 + t * 0.0045813)))))
25    } else {
26        let t = 3.75 / ax;
27        let poly = 0.39894228
28            + t * (0.01328592
29                + t * (0.00225319
30                    + t * (-0.00157565
31                        + t * (0.00916281
32                            + t * (-0.02057706
33                                + t * (0.02635537 + t * (-0.01647633 + t * 0.00392377)))))));
34        poly * ax.exp() / ax.sqrt()
35    }
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/// # Edge Cases
46/// - `m == 0`: returns an empty array.
47/// - `m == 1`: returns `[1.0]`.
48///
49/// # Errors
50/// Returns an error only if internal array construction fails.
51pub fn bartlett(m: usize) -> FerrayResult<Array<f64, Ix1>> {
52    if m == 0 {
53        return Array::from_vec(Ix1::new([0]), vec![]);
54    }
55    if m == 1 {
56        return Array::from_vec(Ix1::new([1]), vec![1.0]);
57    }
58
59    let half = (m - 1) as f64 / 2.0;
60    let mut data = Vec::with_capacity(m);
61    for n in 0..m {
62        let val = 1.0 - ((n as f64 - half) / half).abs();
63        data.push(val);
64    }
65    Array::from_vec(Ix1::new([m]), data)
66}
67
68/// Return the Blackman window of length `m`.
69///
70/// The Blackman window is defined as:
71///   w(n) = 0.42 - 0.5 * cos(2*pi*n/(M-1)) + 0.08 * cos(4*pi*n/(M-1))
72///
73/// This is equivalent to `numpy.blackman(M)`.
74///
75/// # Edge Cases
76/// - `m == 0`: returns an empty array.
77/// - `m == 1`: returns `[1.0]`.
78///
79/// # Errors
80/// Returns an error only if internal array construction fails.
81pub fn blackman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
82    if m == 0 {
83        return Array::from_vec(Ix1::new([0]), vec![]);
84    }
85    if m == 1 {
86        return Array::from_vec(Ix1::new([1]), vec![1.0]);
87    }
88
89    let denom = (m - 1) as f64;
90    let mut data = Vec::with_capacity(m);
91    for n in 0..m {
92        let x = n as f64;
93        let val = 0.42 - 0.5 * (2.0 * PI * x / denom).cos() + 0.08 * (4.0 * PI * x / denom).cos();
94        data.push(val);
95    }
96    Array::from_vec(Ix1::new([m]), data)
97}
98
99/// Return the Hamming window of length `m`.
100///
101/// The Hamming window is defined as:
102///   w(n) = 0.54 - 0.46 * cos(2*pi*n/(M-1))
103///
104/// This is equivalent to `numpy.hamming(M)`.
105///
106/// # Edge Cases
107/// - `m == 0`: returns an empty array.
108/// - `m == 1`: returns `[1.0]`.
109///
110/// # Errors
111/// Returns an error only if internal array construction fails.
112pub fn hamming(m: usize) -> FerrayResult<Array<f64, Ix1>> {
113    if m == 0 {
114        return Array::from_vec(Ix1::new([0]), vec![]);
115    }
116    if m == 1 {
117        return Array::from_vec(Ix1::new([1]), vec![1.0]);
118    }
119
120    let denom = (m - 1) as f64;
121    let mut data = Vec::with_capacity(m);
122    for n in 0..m {
123        let val = 0.54 - 0.46 * (2.0 * PI * n as f64 / denom).cos();
124        data.push(val);
125    }
126    Array::from_vec(Ix1::new([m]), data)
127}
128
129/// Return the Hann (Hanning) window of length `m`.
130///
131/// The Hann window is defined as:
132///   w(n) = 0.5 * (1 - cos(2*pi*n/(M-1)))
133///
134/// NumPy calls this function `hanning`. This is equivalent to `numpy.hanning(M)`.
135///
136/// # Edge Cases
137/// - `m == 0`: returns an empty array.
138/// - `m == 1`: returns `[1.0]`.
139///
140/// # Errors
141/// Returns an error only if internal array construction fails.
142pub fn hanning(m: usize) -> FerrayResult<Array<f64, Ix1>> {
143    if m == 0 {
144        return Array::from_vec(Ix1::new([0]), vec![]);
145    }
146    if m == 1 {
147        return Array::from_vec(Ix1::new([1]), vec![1.0]);
148    }
149
150    let denom = (m - 1) as f64;
151    let mut data = Vec::with_capacity(m);
152    for n in 0..m {
153        let val = 0.5 * (1.0 - (2.0 * PI * n as f64 / denom).cos());
154        data.push(val);
155    }
156    Array::from_vec(Ix1::new([m]), data)
157}
158
159/// Return the Kaiser window of length `m` with shape parameter `beta`.
160///
161/// The Kaiser window is defined as:
162///   w(n) = I_0(beta * sqrt(1 - ((2n/(M-1)) - 1)^2)) / I_0(beta)
163///
164/// where I_0 is the modified Bessel function of the first kind, order 0.
165///
166/// This is equivalent to `numpy.kaiser(M, beta)`.
167///
168/// # Edge Cases
169/// - `m == 0`: returns an empty array.
170/// - `m == 1`: returns `[1.0]`.
171///
172/// # Errors
173/// Returns `FerrayError::InvalidValue` if `beta` is NaN.
174pub fn kaiser(m: usize, beta: f64) -> FerrayResult<Array<f64, Ix1>> {
175    if beta.is_nan() {
176        return Err(FerrayError::invalid_value(
177            "kaiser: beta must not be NaN",
178        ));
179    }
180    // I_0 is an even function, so kaiser(m, -beta) == kaiser(m, beta).
181    // Accept negative beta for NumPy compatibility.
182    let beta = beta.abs();
183
184    if m == 0 {
185        return Array::from_vec(Ix1::new([0]), vec![]);
186    }
187    if m == 1 {
188        return Array::from_vec(Ix1::new([1]), vec![1.0]);
189    }
190
191    let i0_beta = bessel_i0_scalar(beta);
192    let alpha = (m as f64 - 1.0) / 2.0;
193    let mut data = Vec::with_capacity(m);
194    for n in 0..m {
195        let t = (n as f64 - alpha) / alpha;
196        let arg = beta * (1.0 - t * t).max(0.0).sqrt();
197        data.push(bessel_i0_scalar(arg) / i0_beta);
198    }
199    Array::from_vec(Ix1::new([m]), data)
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    // Helper: compare two slices within tolerance
207    fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
208        assert_eq!(
209            actual.len(),
210            expected.len(),
211            "length mismatch: {} vs {}",
212            actual.len(),
213            expected.len()
214        );
215        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
216            assert!(
217                (a - e).abs() <= tol,
218                "element {i}: {a} vs {e} (diff = {})",
219                (a - e).abs()
220            );
221        }
222    }
223
224    // -----------------------------------------------------------------------
225    // Edge cases: M=0 and M=1
226    // -----------------------------------------------------------------------
227
228    #[test]
229    fn bartlett_m0() {
230        let w = bartlett(0).unwrap();
231        assert_eq!(w.shape(), &[0]);
232    }
233
234    #[test]
235    fn bartlett_m1() {
236        let w = bartlett(1).unwrap();
237        assert_eq!(w.as_slice().unwrap(), &[1.0]);
238    }
239
240    #[test]
241    fn blackman_m0() {
242        let w = blackman(0).unwrap();
243        assert_eq!(w.shape(), &[0]);
244    }
245
246    #[test]
247    fn blackman_m1() {
248        let w = blackman(1).unwrap();
249        assert_eq!(w.as_slice().unwrap(), &[1.0]);
250    }
251
252    #[test]
253    fn hamming_m0() {
254        let w = hamming(0).unwrap();
255        assert_eq!(w.shape(), &[0]);
256    }
257
258    #[test]
259    fn hamming_m1() {
260        let w = hamming(1).unwrap();
261        assert_eq!(w.as_slice().unwrap(), &[1.0]);
262    }
263
264    #[test]
265    fn hanning_m0() {
266        let w = hanning(0).unwrap();
267        assert_eq!(w.shape(), &[0]);
268    }
269
270    #[test]
271    fn hanning_m1() {
272        let w = hanning(1).unwrap();
273        assert_eq!(w.as_slice().unwrap(), &[1.0]);
274    }
275
276    #[test]
277    fn kaiser_m0() {
278        let w = kaiser(0, 14.0).unwrap();
279        assert_eq!(w.shape(), &[0]);
280    }
281
282    #[test]
283    fn kaiser_m1() {
284        let w = kaiser(1, 14.0).unwrap();
285        assert_eq!(w.as_slice().unwrap(), &[1.0]);
286    }
287
288    #[test]
289    fn kaiser_negative_beta() {
290        // Negative beta should produce the same result as positive beta (I0 is even)
291        let pos = kaiser(5, 1.0).unwrap();
292        let neg = kaiser(5, -1.0).unwrap();
293        assert_eq!(pos.as_slice().unwrap(), neg.as_slice().unwrap());
294    }
295
296    #[test]
297    fn kaiser_nan_beta() {
298        assert!(kaiser(5, f64::NAN).is_err());
299    }
300
301    // -----------------------------------------------------------------------
302    // AC-1: bartlett(5) matches np.bartlett(5) to within 4 ULPs
303    // -----------------------------------------------------------------------
304    // np.bartlett(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
305    #[test]
306    fn bartlett_5_ac1() {
307        let w = bartlett(5).unwrap();
308        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
309        assert_close(w.as_slice().unwrap(), &expected, 1e-14);
310    }
311
312    // -----------------------------------------------------------------------
313    // AC-2: kaiser(5, 14.0) matches np.kaiser(5, 14.0) to within 4 ULPs
314    // -----------------------------------------------------------------------
315    // np.kaiser(5, 14.0) = [7.72686684e-06, 1.64932188e-01, 1.0, 1.64932188e-01, 7.72686684e-06]
316    #[test]
317    fn kaiser_5_14_ac2() {
318        let w = kaiser(5, 14.0).unwrap();
319        let s = w.as_slice().unwrap();
320        assert_eq!(s.len(), 5);
321        // NumPy reference values (verified via np.kaiser(5, 14.0))
322        let expected: [f64; 5] = [
323            7.72686684e-06,
324            1.64932188e-01,
325            1.0,
326            1.64932188e-01,
327            7.72686684e-06,
328        ];
329        // Bessel polynomial approximation has limited precision (~6 digits)
330        for (i, (&a, &e)) in s.iter().zip(expected.iter()).enumerate() {
331            let tol = if e.abs() < 1e-4 { 1e-8 } else { 1e-6 };
332            assert!(
333                (a - e).abs() <= tol,
334                "kaiser element {i}: {a} vs {e} (diff = {})",
335                (a - e).abs()
336            );
337        }
338    }
339
340    // -----------------------------------------------------------------------
341    // AC-3: All 5 window functions return correct length and match NumPy fixtures
342    // -----------------------------------------------------------------------
343
344    // np.blackman(5) = [-1.38777878e-17, 3.40000000e-01, 1.00000000e+00, 3.40000000e-01, -1.38777878e-17]
345    #[test]
346    fn blackman_5() {
347        let w = blackman(5).unwrap();
348        assert_eq!(w.shape(), &[5]);
349        let s = w.as_slice().unwrap();
350        let expected = [
351            -1.38777878e-17,
352            3.40000000e-01,
353            1.00000000e+00,
354            3.40000000e-01,
355            -1.38777878e-17,
356        ];
357        assert_close(s, &expected, 1e-10);
358    }
359
360    // np.hamming(5) = [0.08, 0.54, 1.0, 0.54, 0.08]
361    #[test]
362    fn hamming_5() {
363        let w = hamming(5).unwrap();
364        assert_eq!(w.shape(), &[5]);
365        let s = w.as_slice().unwrap();
366        let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
367        assert_close(s, &expected, 1e-14);
368    }
369
370    // np.hanning(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
371    #[test]
372    fn hanning_5() {
373        let w = hanning(5).unwrap();
374        assert_eq!(w.shape(), &[5]);
375        let s = w.as_slice().unwrap();
376        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
377        assert_close(s, &expected, 1e-14);
378    }
379
380    // Larger window: np.bartlett(12)
381    #[test]
382    fn bartlett_12() {
383        let w = bartlett(12).unwrap();
384        assert_eq!(w.shape(), &[12]);
385        let s = w.as_slice().unwrap();
386        // First and last should be 0, peak near center
387        assert!((s[0] - 0.0).abs() < 1e-14);
388        assert!((s[11] - 0.0).abs() < 1e-14);
389        // Symmetric
390        for i in 0..6 {
391            assert!((s[i] - s[11 - i]).abs() < 1e-14, "symmetry at {i}");
392        }
393    }
394
395    // Symmetry test for all windows
396    #[test]
397    fn all_windows_symmetric() {
398        let m = 7;
399        let windows: Vec<Array<f64, Ix1>> = vec![
400            bartlett(m).unwrap(),
401            blackman(m).unwrap(),
402            hamming(m).unwrap(),
403            hanning(m).unwrap(),
404            kaiser(m, 5.0).unwrap(),
405        ];
406        for (idx, w) in windows.iter().enumerate() {
407            let s = w.as_slice().unwrap();
408            for i in 0..m / 2 {
409                assert!(
410                    (s[i] - s[m - 1 - i]).abs() < 1e-12,
411                    "window {idx} not symmetric at {i}"
412                );
413            }
414        }
415    }
416
417    // All windows peak at center
418    #[test]
419    fn all_windows_peak_at_center() {
420        let m = 9;
421        let windows: Vec<Array<f64, Ix1>> = vec![
422            bartlett(m).unwrap(),
423            blackman(m).unwrap(),
424            hamming(m).unwrap(),
425            hanning(m).unwrap(),
426            kaiser(m, 5.0).unwrap(),
427        ];
428        for (idx, w) in windows.iter().enumerate() {
429            let s = w.as_slice().unwrap();
430            let center = s[m / 2];
431            assert!(
432                (center - 1.0).abs() < 1e-10,
433                "window {idx} center = {center}, expected 1.0"
434            );
435        }
436    }
437
438    // Kaiser with beta=0 should be a rectangular window (all ones)
439    #[test]
440    fn kaiser_beta_zero() {
441        let w = kaiser(5, 0.0).unwrap();
442        let s = w.as_slice().unwrap();
443        for &v in s {
444            assert!((v - 1.0).abs() < 1e-10, "expected 1.0, got {v}");
445        }
446    }
447
448    #[test]
449    fn bessel_i0_scalar_zero() {
450        assert!((bessel_i0_scalar(0.0) - 1.0).abs() < 1e-6);
451    }
452
453    #[test]
454    fn bessel_i0_scalar_known() {
455        // I0(1) ~ 1.2660658
456        assert!((bessel_i0_scalar(1.0) - 1.2660658).abs() < 1e-4);
457        // I0(5) ~ 27.2398718 (tests the asymptotic branch)
458        assert!((bessel_i0_scalar(5.0) - 27.2398718).abs() < 1e-2);
459    }
460}