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 negative or NaN.
174pub fn kaiser(m: usize, beta: f64) -> FerrayResult<Array<f64, Ix1>> {
175    if beta.is_nan() || beta < 0.0 {
176        return Err(FerrayError::invalid_value(format!(
177            "kaiser: beta must be non-negative, got {beta}"
178        )));
179    }
180
181    if m == 0 {
182        return Array::from_vec(Ix1::new([0]), vec![]);
183    }
184    if m == 1 {
185        return Array::from_vec(Ix1::new([1]), vec![1.0]);
186    }
187
188    let i0_beta = bessel_i0_scalar(beta);
189    let alpha = (m as f64 - 1.0) / 2.0;
190    let mut data = Vec::with_capacity(m);
191    for n in 0..m {
192        let t = (n as f64 - alpha) / alpha;
193        let arg = beta * (1.0 - t * t).max(0.0).sqrt();
194        data.push(bessel_i0_scalar(arg) / i0_beta);
195    }
196    Array::from_vec(Ix1::new([m]), data)
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    // Helper: compare two slices within tolerance
204    fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
205        assert_eq!(
206            actual.len(),
207            expected.len(),
208            "length mismatch: {} vs {}",
209            actual.len(),
210            expected.len()
211        );
212        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
213            assert!(
214                (a - e).abs() <= tol,
215                "element {i}: {a} vs {e} (diff = {})",
216                (a - e).abs()
217            );
218        }
219    }
220
221    // -----------------------------------------------------------------------
222    // Edge cases: M=0 and M=1
223    // -----------------------------------------------------------------------
224
225    #[test]
226    fn bartlett_m0() {
227        let w = bartlett(0).unwrap();
228        assert_eq!(w.shape(), &[0]);
229    }
230
231    #[test]
232    fn bartlett_m1() {
233        let w = bartlett(1).unwrap();
234        assert_eq!(w.as_slice().unwrap(), &[1.0]);
235    }
236
237    #[test]
238    fn blackman_m0() {
239        let w = blackman(0).unwrap();
240        assert_eq!(w.shape(), &[0]);
241    }
242
243    #[test]
244    fn blackman_m1() {
245        let w = blackman(1).unwrap();
246        assert_eq!(w.as_slice().unwrap(), &[1.0]);
247    }
248
249    #[test]
250    fn hamming_m0() {
251        let w = hamming(0).unwrap();
252        assert_eq!(w.shape(), &[0]);
253    }
254
255    #[test]
256    fn hamming_m1() {
257        let w = hamming(1).unwrap();
258        assert_eq!(w.as_slice().unwrap(), &[1.0]);
259    }
260
261    #[test]
262    fn hanning_m0() {
263        let w = hanning(0).unwrap();
264        assert_eq!(w.shape(), &[0]);
265    }
266
267    #[test]
268    fn hanning_m1() {
269        let w = hanning(1).unwrap();
270        assert_eq!(w.as_slice().unwrap(), &[1.0]);
271    }
272
273    #[test]
274    fn kaiser_m0() {
275        let w = kaiser(0, 14.0).unwrap();
276        assert_eq!(w.shape(), &[0]);
277    }
278
279    #[test]
280    fn kaiser_m1() {
281        let w = kaiser(1, 14.0).unwrap();
282        assert_eq!(w.as_slice().unwrap(), &[1.0]);
283    }
284
285    #[test]
286    fn kaiser_negative_beta() {
287        assert!(kaiser(5, -1.0).is_err());
288    }
289
290    #[test]
291    fn kaiser_nan_beta() {
292        assert!(kaiser(5, f64::NAN).is_err());
293    }
294
295    // -----------------------------------------------------------------------
296    // AC-1: bartlett(5) matches np.bartlett(5) to within 4 ULPs
297    // -----------------------------------------------------------------------
298    // np.bartlett(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
299    #[test]
300    fn bartlett_5_ac1() {
301        let w = bartlett(5).unwrap();
302        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
303        assert_close(w.as_slice().unwrap(), &expected, 1e-14);
304    }
305
306    // -----------------------------------------------------------------------
307    // AC-2: kaiser(5, 14.0) matches np.kaiser(5, 14.0) to within 4 ULPs
308    // -----------------------------------------------------------------------
309    // np.kaiser(5, 14.0) = [7.72686684e-06, 1.64932188e-01, 1.0, 1.64932188e-01, 7.72686684e-06]
310    #[test]
311    fn kaiser_5_14_ac2() {
312        let w = kaiser(5, 14.0).unwrap();
313        let s = w.as_slice().unwrap();
314        assert_eq!(s.len(), 5);
315        // NumPy reference values (verified via np.kaiser(5, 14.0))
316        let expected: [f64; 5] = [
317            7.72686684e-06,
318            1.64932188e-01,
319            1.0,
320            1.64932188e-01,
321            7.72686684e-06,
322        ];
323        // Bessel polynomial approximation has limited precision (~6 digits)
324        for (i, (&a, &e)) in s.iter().zip(expected.iter()).enumerate() {
325            let tol = if e.abs() < 1e-4 { 1e-8 } else { 1e-6 };
326            assert!(
327                (a - e).abs() <= tol,
328                "kaiser element {i}: {a} vs {e} (diff = {})",
329                (a - e).abs()
330            );
331        }
332    }
333
334    // -----------------------------------------------------------------------
335    // AC-3: All 5 window functions return correct length and match NumPy fixtures
336    // -----------------------------------------------------------------------
337
338    // np.blackman(5) = [-1.38777878e-17, 3.40000000e-01, 1.00000000e+00, 3.40000000e-01, -1.38777878e-17]
339    #[test]
340    fn blackman_5() {
341        let w = blackman(5).unwrap();
342        assert_eq!(w.shape(), &[5]);
343        let s = w.as_slice().unwrap();
344        let expected = [
345            -1.38777878e-17,
346            3.40000000e-01,
347            1.00000000e+00,
348            3.40000000e-01,
349            -1.38777878e-17,
350        ];
351        assert_close(s, &expected, 1e-10);
352    }
353
354    // np.hamming(5) = [0.08, 0.54, 1.0, 0.54, 0.08]
355    #[test]
356    fn hamming_5() {
357        let w = hamming(5).unwrap();
358        assert_eq!(w.shape(), &[5]);
359        let s = w.as_slice().unwrap();
360        let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
361        assert_close(s, &expected, 1e-14);
362    }
363
364    // np.hanning(5) = [0.0, 0.5, 1.0, 0.5, 0.0]
365    #[test]
366    fn hanning_5() {
367        let w = hanning(5).unwrap();
368        assert_eq!(w.shape(), &[5]);
369        let s = w.as_slice().unwrap();
370        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
371        assert_close(s, &expected, 1e-14);
372    }
373
374    // Larger window: np.bartlett(12)
375    #[test]
376    fn bartlett_12() {
377        let w = bartlett(12).unwrap();
378        assert_eq!(w.shape(), &[12]);
379        let s = w.as_slice().unwrap();
380        // First and last should be 0, peak near center
381        assert!((s[0] - 0.0).abs() < 1e-14);
382        assert!((s[11] - 0.0).abs() < 1e-14);
383        // Symmetric
384        for i in 0..6 {
385            assert!((s[i] - s[11 - i]).abs() < 1e-14, "symmetry at {i}");
386        }
387    }
388
389    // Symmetry test for all windows
390    #[test]
391    fn all_windows_symmetric() {
392        let m = 7;
393        let windows: Vec<Array<f64, Ix1>> = vec![
394            bartlett(m).unwrap(),
395            blackman(m).unwrap(),
396            hamming(m).unwrap(),
397            hanning(m).unwrap(),
398            kaiser(m, 5.0).unwrap(),
399        ];
400        for (idx, w) in windows.iter().enumerate() {
401            let s = w.as_slice().unwrap();
402            for i in 0..m / 2 {
403                assert!(
404                    (s[i] - s[m - 1 - i]).abs() < 1e-12,
405                    "window {idx} not symmetric at {i}"
406                );
407            }
408        }
409    }
410
411    // All windows peak at center
412    #[test]
413    fn all_windows_peak_at_center() {
414        let m = 9;
415        let windows: Vec<Array<f64, Ix1>> = vec![
416            bartlett(m).unwrap(),
417            blackman(m).unwrap(),
418            hamming(m).unwrap(),
419            hanning(m).unwrap(),
420            kaiser(m, 5.0).unwrap(),
421        ];
422        for (idx, w) in windows.iter().enumerate() {
423            let s = w.as_slice().unwrap();
424            let center = s[m / 2];
425            assert!(
426                (center - 1.0).abs() < 1e-10,
427                "window {idx} center = {center}, expected 1.0"
428            );
429        }
430    }
431
432    // Kaiser with beta=0 should be a rectangular window (all ones)
433    #[test]
434    fn kaiser_beta_zero() {
435        let w = kaiser(5, 0.0).unwrap();
436        let s = w.as_slice().unwrap();
437        for &v in s {
438            assert!((v - 1.0).abs() < 1e-10, "expected 1.0, got {v}");
439        }
440    }
441
442    #[test]
443    fn bessel_i0_scalar_zero() {
444        assert!((bessel_i0_scalar(0.0) - 1.0).abs() < 1e-6);
445    }
446
447    #[test]
448    fn bessel_i0_scalar_known() {
449        // I0(1) ~ 1.2660658
450        assert!((bessel_i0_scalar(1.0) - 1.2660658).abs() < 1e-4);
451        // I0(5) ~ 27.2398718 (tests the asymptotic branch)
452        assert!((bessel_i0_scalar(5.0) - 27.2398718).abs() < 1e-2);
453    }
454}