1#![allow(clippy::unreadable_literal)]
13
14use ferray_core::Array;
15use ferray_core::dimension::Ix1;
16use ferray_core::error::{FerrayError, FerrayResult};
17
18use std::f64::consts::PI;
19
20use ferray_ufunc::ops::special::bessel_i0_scalar;
26
27#[inline]
32fn gen_window<F: FnMut(usize) -> f64>(m: usize, mut f: F) -> FerrayResult<Array<f64, Ix1>> {
33 if m == 0 {
34 return Array::from_vec(Ix1::new([0]), vec![]);
35 }
36 if m == 1 {
37 return Array::from_vec(Ix1::new([1]), vec![1.0]);
38 }
39 let mut data = Vec::with_capacity(m);
40 for n in 0..m {
41 data.push(f(n));
42 }
43 Array::from_vec(Ix1::new([m]), data)
44}
45
46fn cast_to_f32(arr: &Array<f64, Ix1>) -> FerrayResult<Array<f32, Ix1>> {
51 let data: Vec<f32> = arr.iter().map(|&x| x as f32).collect();
52 Array::<f32, Ix1>::from_vec(Ix1::new([data.len()]), data)
53}
54
55pub fn bartlett_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
60 cast_to_f32(&bartlett(m)?)
61}
62
63pub fn blackman_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
68 cast_to_f32(&blackman(m)?)
69}
70
71pub fn hamming_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
76 cast_to_f32(&hamming(m)?)
77}
78
79pub fn hanning_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
84 cast_to_f32(&hanning(m)?)
85}
86
87pub fn kaiser_f32(m: usize, beta: f64) -> FerrayResult<Array<f32, Ix1>> {
92 cast_to_f32(&kaiser(m, beta)?)
93}
94
95pub fn bartlett(m: usize) -> FerrayResult<Array<f64, Ix1>> {
105 let half = (m.saturating_sub(1)) as f64 / 2.0;
106 gen_window(m, |n| 1.0 - ((n as f64 - half) / half).abs())
107}
108
109pub fn blackman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
119 let denom = (m.saturating_sub(1)) as f64;
120 gen_window(m, |n| {
121 let x = n as f64;
122 0.08f64.mul_add(
123 (4.0 * PI * x / denom).cos(),
124 0.5f64.mul_add(-(2.0 * PI * x / denom).cos(), 0.42),
125 )
126 })
127}
128
129pub fn hamming(m: usize) -> FerrayResult<Array<f64, Ix1>> {
139 let denom = (m.saturating_sub(1)) as f64;
140 gen_window(m, |n| {
141 0.46f64.mul_add(-(2.0 * PI * n as f64 / denom).cos(), 0.54)
142 })
143}
144
145pub fn hanning(m: usize) -> FerrayResult<Array<f64, Ix1>> {
155 let denom = (m.saturating_sub(1)) as f64;
156 gen_window(m, |n| 0.5 * (1.0 - (2.0 * PI * n as f64 / denom).cos()))
157}
158
159pub fn kaiser(m: usize, beta: f64) -> FerrayResult<Array<f64, Ix1>> {
180 if beta.is_nan() {
181 return Err(FerrayError::invalid_value("kaiser: beta must not be NaN"));
182 }
183 let beta = beta.abs();
186 const BETA_OVERFLOW_THRESHOLD: f64 = 708.0;
191 if beta > BETA_OVERFLOW_THRESHOLD {
192 return Err(FerrayError::invalid_value(format!(
193 "kaiser: |beta| = {beta} exceeds the safe range ({BETA_OVERFLOW_THRESHOLD}) \
194 for f64 I_0; the window would be NaN everywhere"
195 )));
196 }
197 let i0_beta = bessel_i0_scalar::<f64>(beta);
198 let alpha = (m.saturating_sub(1)) as f64 / 2.0;
199 gen_window(m, |n| {
200 let t = (n as f64 - alpha) / alpha;
201 let arg = beta * (1.0 - t * t).max(0.0).sqrt();
202 bessel_i0_scalar::<f64>(arg) / i0_beta
203 })
204}
205
206pub fn cosine(m: usize) -> FerrayResult<Array<f64, Ix1>> {
222 if m == 0 {
223 return Array::from_vec(Ix1::new([0]), vec![]);
224 }
225 if m == 1 {
226 return Array::from_vec(Ix1::new([1]), vec![1.0]);
227 }
228 let mf = m as f64;
229 gen_window(m, |n| (PI * (n as f64 + 0.5) / mf).sin())
230}
231
232pub fn exponential(m: usize, center: Option<f64>, tau: f64) -> FerrayResult<Array<f64, Ix1>> {
240 if !tau.is_finite() || tau <= 0.0 {
241 return Err(FerrayError::invalid_value(format!(
242 "exponential: tau must be positive and finite, got {tau}"
243 )));
244 }
245 let centre = center.unwrap_or((m.saturating_sub(1)) as f64 / 2.0);
246 gen_window(m, |n| (-((n as f64) - centre).abs() / tau).exp())
247}
248
249pub fn gaussian(m: usize, std: f64) -> FerrayResult<Array<f64, Ix1>> {
256 if !std.is_finite() || std <= 0.0 {
257 return Err(FerrayError::invalid_value(format!(
258 "gaussian: std must be positive and finite, got {std}"
259 )));
260 }
261 let centre = (m.saturating_sub(1)) as f64 / 2.0;
262 gen_window(m, |n| {
263 let z = ((n as f64) - centre) / std;
264 (-0.5 * z * z).exp()
265 })
266}
267
268pub fn general_cosine(m: usize, coeffs: &[f64]) -> FerrayResult<Array<f64, Ix1>> {
276 if coeffs.is_empty() {
277 return Err(FerrayError::invalid_value(
278 "general_cosine: coeffs must not be empty",
279 ));
280 }
281 let denom = (m.saturating_sub(1)) as f64;
282 let coeffs = coeffs.to_vec();
283 gen_window(m, |n| {
284 let nf = n as f64;
285 let mut sum = 0.0_f64;
286 for (k, &a) in coeffs.iter().enumerate() {
287 let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
288 sum += sign * a * (2.0 * PI * (k as f64) * nf / denom).cos();
289 }
290 sum
291 })
292}
293
294pub fn general_hamming(m: usize, alpha: f64) -> FerrayResult<Array<f64, Ix1>> {
302 if !alpha.is_finite() {
303 return Err(FerrayError::invalid_value(format!(
304 "general_hamming: alpha must be finite, got {alpha}"
305 )));
306 }
307 let denom = (m.saturating_sub(1)) as f64;
308 gen_window(m, |n| {
309 alpha - (1.0 - alpha) * (2.0 * PI * (n as f64) / denom).cos()
310 })
311}
312
313pub fn nuttall(m: usize) -> FerrayResult<Array<f64, Ix1>> {
321 general_cosine(m, &[0.3635819, 0.4891775, 0.1365995, 0.0106411])
322}
323
324pub fn parzen(m: usize) -> FerrayResult<Array<f64, Ix1>> {
328 if m == 0 {
329 return Array::from_vec(Ix1::new([0]), vec![]);
330 }
331 if m == 1 {
332 return Array::from_vec(Ix1::new([1]), vec![1.0]);
333 }
334 let half = m as f64 / 2.0;
337 gen_window(m, |n| {
338 let x = (n as f64) - (m as f64 - 1.0) / 2.0;
339 let r = x.abs() / half;
340 if r <= 0.5 {
341 1.0 - 6.0 * r * r + 6.0 * r * r * r
342 } else if r <= 1.0 {
343 let one_minus_r = 1.0 - r;
344 2.0 * one_minus_r * one_minus_r * one_minus_r
345 } else {
346 0.0
347 }
348 })
349}
350
351pub fn chebwin(m: usize, at: f64) -> FerrayResult<Array<f64, Ix1>> {
367 if !at.is_finite() || at <= 0.0 {
368 return Err(FerrayError::invalid_value(format!(
369 "chebwin: at must be a positive finite dB attenuation, got {at}"
370 )));
371 }
372 if m == 0 {
373 return Array::from_vec(Ix1::new([0]), vec![]);
374 }
375 if m == 1 {
376 return Array::from_vec(Ix1::new([1]), vec![1.0]);
377 }
378
379 let order = (m - 1) as f64;
380 let r = 10.0_f64.powf(at / 20.0);
381 let beta = (r.acosh() / order).cosh();
382
383 let mut spectrum = Vec::with_capacity(m);
385 for k in 0..m {
386 let x = beta * (PI * k as f64 / m as f64).cos();
387 spectrum.push(chebyshev_t_extended(x, order));
388 }
389
390 let mf = m as f64;
402 let half_dft = |idx: usize| -> f64 {
403 if m % 2 == 1 {
404 spectrum
405 .iter()
406 .enumerate()
407 .map(|(k, &pk)| pk * (-2.0 * PI * k as f64 * idx as f64 / mf).cos())
408 .sum()
409 } else {
410 spectrum
414 .iter()
415 .enumerate()
416 .map(|(k, &pk)| {
417 let phase = PI * k as f64 * (1.0 - 2.0 * idx as f64) / mf;
418 pk * phase.cos()
419 })
420 .sum()
421 }
422 };
423
424 let half_len = m / 2 + 1;
425 let mut half: Vec<f64> = (0..half_len).map(half_dft).collect();
426 let denom = if m % 2 == 1 { half[0] } else { half[1] };
427 if denom != 0.0 {
428 for v in &mut half {
429 *v /= denom;
430 }
431 }
432
433 let mut w = Vec::with_capacity(m);
434 if m % 2 == 1 {
435 for &v in half.iter().skip(1).rev() {
437 w.push(v);
438 }
439 w.extend_from_slice(&half);
440 } else {
441 let n = m / 2 + 1;
444 for &v in half[1..n].iter().rev() {
445 w.push(v);
446 }
447 w.extend_from_slice(&half[1..n]);
448 }
449
450 Array::from_vec(Ix1::new([m]), w)
451}
452
453pub fn dpss(m: usize, nw: f64) -> FerrayResult<Array<f64, Ix1>> {
485 if !nw.is_finite() || nw <= 0.0 {
486 return Err(FerrayError::invalid_value(format!(
487 "dpss: nw must be a positive finite half bandwidth, got {nw}"
488 )));
489 }
490 if m == 0 {
491 return Array::from_vec(Ix1::new([0]), vec![]);
492 }
493 if m == 1 {
494 return Array::from_vec(Ix1::new([1]), vec![1.0]);
495 }
496 if nw >= (m as f64) / 2.0 {
497 return Err(FerrayError::invalid_value(format!(
498 "dpss: nw = {nw} must be < M/2 = {} for a non-degenerate band",
499 (m as f64) / 2.0
500 )));
501 }
502
503 let mf = m as f64;
504 let cos_w = (2.0 * PI * nw / mf).cos();
505 let diag: Vec<f64> = (0..m)
507 .map(|i| {
508 let centre = (mf - 1.0) / 2.0 - i as f64;
509 centre * centre * cos_w
510 })
511 .collect();
512 let off: Vec<f64> = (0..m.saturating_sub(1))
514 .map(|i| (i as f64 + 1.0) * (mf - i as f64 - 1.0) / 2.0)
515 .collect();
516
517 let mut v: Vec<f64> = (0..m)
520 .map(|i| {
521 let centred = (i as f64 - (mf - 1.0) / 2.0) / mf;
522 (-2.0 * centred * centred).exp()
523 })
524 .collect();
525 let n0 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
526 for x in &mut v {
527 *x /= n0.max(f64::MIN_POSITIVE);
528 }
529
530 let mut prev_eig = 0.0_f64;
531 for _ in 0..500 {
532 let mut next = vec![0.0_f64; m];
533 for i in 0..m {
534 next[i] = diag[i] * v[i];
535 if i > 0 {
536 next[i] += off[i - 1] * v[i - 1];
537 }
538 if i + 1 < m {
539 next[i] += off[i] * v[i + 1];
540 }
541 }
542 let eig: f64 = v.iter().zip(next.iter()).map(|(a, b)| a * b).sum();
544 let norm = next.iter().map(|x| x * x).sum::<f64>().sqrt();
545 if norm < f64::MIN_POSITIVE {
546 return Err(FerrayError::invalid_value(
547 "dpss: power iteration produced a zero vector",
548 ));
549 }
550 for x in &mut next {
551 *x /= norm;
552 }
553 v = next;
554 if (eig - prev_eig).abs() < 1e-12 * eig.abs().max(1.0) {
555 break;
556 }
557 prev_eig = eig;
558 }
559
560 let centre_val = if m % 2 == 1 {
563 v[m / 2]
564 } else {
565 v[m / 2 - 1] + v[m / 2]
566 };
567 if centre_val < 0.0 {
568 for x in &mut v {
569 *x = -*x;
570 }
571 }
572
573 let peak = v.iter().copied().fold(f64::NEG_INFINITY, f64::max);
578 if peak > 0.0 {
579 for x in &mut v {
580 *x /= peak;
581 }
582 }
583
584 Array::from_vec(Ix1::new([m]), v)
585}
586
587fn chebyshev_t_extended(x: f64, n: f64) -> f64 {
592 if x.abs() <= 1.0 {
593 (n * x.acos()).cos()
594 } else {
595 let mag = (n * x.abs().acosh()).cosh();
596 if x < 0.0 && (n as i64) % 2 != 0 {
597 -mag
598 } else {
599 mag
600 }
601 }
602}
603
604pub fn boxcar(m: usize) -> FerrayResult<Array<f64, Ix1>> {
611 gen_window(m, |_| 1.0)
612}
613
614pub fn triang(m: usize) -> FerrayResult<Array<f64, Ix1>> {
623 if m == 0 {
624 return Array::from_vec(Ix1::new([0]), vec![]);
625 }
626 if m == 1 {
627 return Array::from_vec(Ix1::new([1]), vec![1.0]);
628 }
629 let mf = m as f64;
630 let centre = (mf - 1.0) / 2.0;
631 if m % 2 == 0 {
632 gen_window(m, |n| 1.0 - (2.0 * n as f64 - centre - centre).abs() / mf)
634 } else {
635 gen_window(m, |n| {
637 1.0 - (2.0 * n as f64 - centre - centre).abs() / (mf + 1.0)
638 })
639 }
640}
641
642pub fn bohman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
651 if m == 0 {
652 return Array::from_vec(Ix1::new([0]), vec![]);
653 }
654 if m == 1 {
655 return Array::from_vec(Ix1::new([1]), vec![1.0]);
656 }
657 let mf = (m - 1) as f64;
659 let mut data = Vec::with_capacity(m);
660 for n in 0..m {
661 if n == 0 || n == m - 1 {
662 data.push(0.0);
663 continue;
664 }
665 let r = (2.0 * n as f64 / mf - 1.0).abs();
666 let v = (1.0 - r) * (PI * r).cos() + (PI * r).sin() / PI;
667 data.push(v);
668 }
669 Array::from_vec(Ix1::new([m]), data)
670}
671
672pub fn flattop(m: usize) -> FerrayResult<Array<f64, Ix1>> {
681 const COEFFS: [f64; 5] = [
683 0.215_578_95,
684 0.416_631_58,
685 0.277_263_158,
686 0.083_578_95,
687 0.006_947_368,
688 ];
689 if m == 0 {
690 return Array::from_vec(Ix1::new([0]), vec![]);
691 }
692 let denom = (m - 1).max(1) as f64;
693 gen_window(m, |n| {
694 let phase = 2.0 * PI * n as f64 / denom;
695 let mut acc = COEFFS[0];
696 for (k, &c) in COEFFS.iter().enumerate().skip(1) {
697 let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
698 acc += sign * c * (k as f64 * phase).cos();
699 }
700 acc
701 })
702}
703
704pub fn lanczos(m: usize) -> FerrayResult<Array<f64, Ix1>> {
712 if m == 0 {
713 return Array::from_vec(Ix1::new([0]), vec![]);
714 }
715 if m == 1 {
716 return Array::from_vec(Ix1::new([1]), vec![1.0]);
717 }
718 let denom = (m - 1) as f64;
719 gen_window(m, |n| {
720 let x = 2.0 * n as f64 / denom - 1.0;
721 if x.abs() < f64::EPSILON {
722 1.0
723 } else {
724 let pi_x = PI * x;
725 pi_x.sin() / pi_x
726 }
727 })
728}
729
730pub fn taylor(m: usize, nbar: usize, sll: f64, norm: bool) -> FerrayResult<Array<f64, Ix1>> {
745 if m == 0 {
746 return Array::from_vec(Ix1::new([0]), vec![]);
747 }
748 if m == 1 {
749 return Array::from_vec(Ix1::new([1]), vec![1.0]);
750 }
751 if nbar == 0 {
752 return Err(FerrayError::invalid_value("taylor: nbar must be >= 1"));
753 }
754 if !sll.is_finite() {
755 return Err(FerrayError::invalid_value("taylor: sll must be finite"));
756 }
757 let r = 10.0_f64.powf(sll / 20.0);
759 let b = r.acosh() / PI;
760 let nbar_f = nbar as f64;
761 let sigma2 = (nbar_f * nbar_f) / (b * b + (nbar_f - 0.5) * (nbar_f - 0.5));
764
765 let mut f_coeffs = Vec::with_capacity(nbar.saturating_sub(1));
767 for mm in 1..nbar {
768 let mmf = mm as f64;
769 let mut num = 1.0_f64;
772 for n in 1..nbar {
773 let nf = n as f64;
774 num *= 1.0 - mmf * mmf / (sigma2 * (b * b + (nf - 0.5) * (nf - 0.5)));
775 }
776 let sign = if mm % 2 == 0 { -1.0 } else { 1.0 };
777 let mut den = 1.0_f64;
778 for n in 1..nbar {
779 if n == mm {
780 continue;
781 }
782 let nf = n as f64;
783 den *= 1.0 - mmf * mmf / (nf * nf);
784 }
785 f_coeffs.push(0.5 * sign * num / den);
789 }
790
791 let denom = (m - 1) as f64;
792 let arr = gen_window(m, |n| {
793 let xn = (n as f64) - denom / 2.0;
794 let mut w = 1.0_f64;
795 for (idx, &fk) in f_coeffs.iter().enumerate() {
796 let kk = (idx + 1) as f64;
797 w += 2.0 * fk * (2.0 * PI * kk * xn / m as f64).cos();
798 }
799 w
800 })?;
801
802 if !norm {
803 return Ok(arr);
804 }
805 let centre_val: f64 = 1.0 + 2.0 * f_coeffs.iter().sum::<f64>();
812 if centre_val == 0.0 {
813 return Ok(arr); }
815 let normed: Vec<f64> = arr
816 .as_slice()
817 .unwrap()
818 .iter()
819 .map(|&v| v / centre_val)
820 .collect();
821 Array::from_vec(Ix1::new([m]), normed)
822}
823
824pub fn tukey(m: usize, alpha: f64) -> FerrayResult<Array<f64, Ix1>> {
832 if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
833 return Err(FerrayError::invalid_value(format!(
834 "tukey: alpha must be in [0, 1], got {alpha}"
835 )));
836 }
837 if m == 0 {
838 return Array::from_vec(Ix1::new([0]), vec![]);
839 }
840 if m == 1 {
841 return Array::from_vec(Ix1::new([1]), vec![1.0]);
842 }
843 if alpha == 0.0 {
844 return Array::from_vec(Ix1::new([m]), vec![1.0; m]);
845 }
846 let denom = (m - 1) as f64;
847 let width = alpha * denom / 2.0;
848 gen_window(m, |n| {
849 let nf = n as f64;
850 if nf < width {
851 0.5 * (1.0 + (PI * (nf / width - 1.0)).cos())
852 } else if nf <= denom - width {
853 1.0
854 } else {
855 0.5 * (1.0 + (PI * ((denom - nf) / width - 1.0)).cos())
856 }
857 })
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
867 fn dpss_max_one_and_symmetric() {
868 let w = dpss(64, 3.0).unwrap();
869 let s = w.as_slice().unwrap();
870 let max = s.iter().copied().fold(f64::NEG_INFINITY, f64::max);
872 assert!((max - 1.0).abs() < 1e-12, "max = {max}");
873 for i in 0..s.len() / 2 {
875 let mirror = s.len() - 1 - i;
876 assert!(
877 (s[i] - s[mirror]).abs() < 1e-7,
878 "asymmetric at i={i}: {} vs {}",
879 s[i],
880 s[mirror]
881 );
882 }
883 }
884
885 #[test]
886 fn dpss_centre_is_positive_and_peak() {
887 let w = dpss(48, 2.5).unwrap();
891 let s = w.as_slice().unwrap();
892 let mid = s.len() / 2;
893 assert!(s[mid] > 0.0);
894 assert!((s[mid] - 1.0).abs() < 1e-12);
895 }
896
897 #[test]
898 fn dpss_endpoints_smaller_than_centre() {
899 let w = dpss(64, 3.0).unwrap();
902 let s = w.as_slice().unwrap();
903 let mid = s.len() / 2;
904 assert!(s[mid] > s[0]);
905 assert!(s[mid] > s[s.len() - 1]);
906 assert!(s[0].abs() < 0.1 * s[mid]);
907 }
908
909 #[test]
910 fn dpss_rejects_invalid_nw() {
911 assert!(dpss(32, -1.0).is_err());
912 assert!(dpss(32, 0.0).is_err());
913 assert!(dpss(32, f64::NAN).is_err());
914 assert!(dpss(8, 4.0).is_err());
916 assert!(dpss(8, 5.0).is_err());
917 }
918
919 #[test]
920 fn dpss_matches_scipy_within_relative_tolerance() {
921 let want_first_half: [f64; 8] = [
928 0.00710856, 0.03620714, 0.10884379, 0.24276927, 0.43733689, 0.6634221, 0.86701952,
929 0.98841699,
930 ];
931 let got = dpss(16, 3.0).unwrap();
932 let s = got.as_slice().unwrap();
933 for (i, (&g, &w)) in s[..8].iter().zip(want_first_half.iter()).enumerate() {
934 let tol = 0.05 * w.abs().max(1e-3);
939 assert!((g - w).abs() <= tol, "i={i}: got={g}, want={w}, tol={tol}");
940 }
941 }
942
943 #[test]
944 fn dpss_m0_m1_edge_cases() {
945 let z = dpss(0, 2.0).unwrap();
946 assert_eq!(z.shape(), &[0]);
947 let one = dpss(1, 0.5).unwrap();
948 assert_eq!(one.as_slice().unwrap(), &[1.0]);
949 }
950
951 #[test]
954 fn chebwin_centre_is_one_and_symmetric() {
955 let w = chebwin(11, 50.0).unwrap();
956 let s = w.as_slice().unwrap();
957 let mid = s.len() / 2;
958 assert!((s[mid] - 1.0).abs() < 1e-6, "centre = {}", s[mid]);
959 for i in 0..s.len() / 2 {
960 assert!(
961 (s[i] - s[s.len() - 1 - i]).abs() < 1e-9,
962 "asymmetric at i={i}: {} vs {}",
963 s[i],
964 s[s.len() - 1 - i]
965 );
966 }
967 }
968
969 #[test]
970 fn chebwin_even_length_symmetric() {
971 let w = chebwin(8, 40.0).unwrap();
972 let s = w.as_slice().unwrap();
973 for i in 0..s.len() / 2 {
974 assert!(
975 (s[i] - s[s.len() - 1 - i]).abs() < 1e-9,
976 "asymmetric at i={i}"
977 );
978 }
979 let max = s.iter().copied().fold(f64::NEG_INFINITY, f64::max);
981 assert!((max - 1.0).abs() < 1e-6);
982 }
983
984 #[test]
985 fn chebwin_endpoints_smaller_than_centre() {
986 let w = chebwin(31, 80.0).unwrap();
988 let s = w.as_slice().unwrap();
989 let mid = s.len() / 2;
990 assert!(s[mid] > s[0]);
991 assert!(s[mid] > s[s.len() - 1]);
992 }
993
994 #[test]
995 fn chebwin_rejects_invalid_attenuation() {
996 assert!(chebwin(16, -10.0).is_err());
997 assert!(chebwin(16, 0.0).is_err());
998 assert!(chebwin(16, f64::NAN).is_err());
999 }
1000
1001 #[test]
1002 fn chebwin_matches_scipy_known_values() {
1003 let want = [
1005 0.06228583, 0.20113857, 0.42847525, 0.69573494, 0.91497506, 1.0, 0.91497506,
1006 0.69573494, 0.42847525, 0.20113857, 0.06228583,
1007 ];
1008 let got = chebwin(11, 50.0).unwrap();
1009 let s = got.as_slice().unwrap();
1010 for (i, (&g, &w)) in s.iter().zip(want.iter()).enumerate() {
1011 assert!((g - w).abs() < 1e-6, "i={i}: got={g}, want={w}");
1012 }
1013 }
1014
1015 #[test]
1016 fn chebwin_matches_scipy_even_length() {
1017 let want = [
1019 0.14609713, 0.41790422, 0.75944595, 1.0, 1.0, 0.75944595, 0.41790422, 0.14609713,
1020 ];
1021 let got = chebwin(8, 40.0).unwrap();
1022 let s = got.as_slice().unwrap();
1023 for (i, (&g, &w)) in s.iter().zip(want.iter()).enumerate() {
1024 assert!((g - w).abs() < 1e-6, "i={i}: got={g}, want={w}");
1025 }
1026 }
1027
1028 #[test]
1029 fn chebwin_m0_m1_edge_cases() {
1030 let z = chebwin(0, 50.0).unwrap();
1031 assert_eq!(z.shape(), &[0]);
1032 let one = chebwin(1, 50.0).unwrap();
1033 assert_eq!(one.as_slice().unwrap(), &[1.0]);
1034 }
1035
1036 #[test]
1039 fn boxcar_all_ones() {
1040 let w = boxcar(8).unwrap();
1041 let s = w.as_slice().unwrap();
1042 for &v in s {
1043 assert!((v - 1.0).abs() < 1e-15);
1044 }
1045 }
1046
1047 #[test]
1048 fn triang_centre_is_max_and_endpoints_smaller() {
1049 let w = triang(7).unwrap();
1050 let s = w.as_slice().unwrap();
1051 for i in 0..s.len() / 2 {
1053 assert!((s[i] - s[s.len() - 1 - i]).abs() < 1e-12);
1054 }
1055 let mid = s.len() / 2;
1057 for i in 0..s.len() {
1058 if i != mid {
1059 assert!(s[i] <= s[mid] + 1e-12);
1060 }
1061 }
1062 }
1063
1064 #[test]
1065 fn bohman_zero_at_endpoints_and_one_at_centre() {
1066 let w = bohman(5).unwrap();
1067 let s = w.as_slice().unwrap();
1068 assert!(s[0].abs() < 1e-12);
1069 assert!(s[s.len() - 1].abs() < 1e-12);
1070 let mid = s.len() / 2;
1072 assert!((s[mid] - 1.0).abs() < 1e-6);
1073 }
1074
1075 #[test]
1076 fn flattop_centre_around_one_and_negative_lobes_present() {
1077 let w = flattop(11).unwrap();
1080 let s = w.as_slice().unwrap();
1081 let mid = s.len() / 2;
1082 assert!((s[mid] - 1.0).abs() < 0.01);
1083 let any_negative = s.iter().any(|&v| v < -0.001);
1086 assert!(any_negative, "flattop should have negative side lobes");
1087 }
1088
1089 #[test]
1090 fn lanczos_centre_is_one_and_endpoints_zero() {
1091 let w = lanczos(9).unwrap();
1092 let s = w.as_slice().unwrap();
1093 let mid = s.len() / 2;
1094 assert!((s[mid] - 1.0).abs() < 1e-12);
1095 assert!(s[0].abs() < 1e-12);
1096 assert!(s[s.len() - 1].abs() < 1e-12);
1097 }
1098
1099 #[test]
1100 fn small_m_edge_cases_handled() {
1101 for f in [triang as fn(usize) -> _, bohman, lanczos] {
1103 let z = f(0).unwrap();
1104 assert_eq!(z.shape(), &[0]);
1105 let one = f(1).unwrap();
1106 assert_eq!(one.shape(), &[1]);
1107 assert_eq!(one.as_slice().unwrap(), &[1.0]);
1108 }
1109 }
1110
1111 #[test]
1114 fn f32_windows_match_f64_within_f32_eps() {
1115 for m in [1usize, 5, 16, 64] {
1116 for (name, got, want) in [
1117 ("bartlett", bartlett_f32(m).unwrap(), bartlett(m).unwrap()),
1118 ("blackman", blackman_f32(m).unwrap(), blackman(m).unwrap()),
1119 ("hamming", hamming_f32(m).unwrap(), hamming(m).unwrap()),
1120 ("hanning", hanning_f32(m).unwrap(), hanning(m).unwrap()),
1121 ] {
1122 assert_eq!(got.shape(), want.shape(), "{name} m={m} shape");
1123 let g = got.as_slice().unwrap();
1124 let w = want.as_slice().unwrap();
1125 for (i, (&a, &b)) in g.iter().zip(w.iter()).enumerate() {
1126 let bf = b as f32;
1127 assert!(
1128 (a - bf).abs() < 1e-6,
1129 "{name} m={m} idx={i}: f32 {a} vs f64 {b}"
1130 );
1131 }
1132 }
1133 }
1134 }
1135
1136 #[test]
1137 fn kaiser_f32_matches_kaiser() {
1138 let m = 20;
1139 let beta = 8.6;
1140 let f32_arr = kaiser_f32(m, beta).unwrap();
1141 let f64_arr = kaiser(m, beta).unwrap();
1142 for (a, b) in f32_arr.iter().zip(f64_arr.iter()) {
1143 assert!((a - *b as f32).abs() < 1e-5);
1144 }
1145 }
1146
1147 fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
1149 assert_eq!(
1150 actual.len(),
1151 expected.len(),
1152 "length mismatch: {} vs {}",
1153 actual.len(),
1154 expected.len()
1155 );
1156 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
1157 assert!(
1158 (a - e).abs() <= tol,
1159 "element {i}: {a} vs {e} (diff = {})",
1160 (a - e).abs()
1161 );
1162 }
1163 }
1164
1165 #[test]
1170 fn bartlett_m0() {
1171 let w = bartlett(0).unwrap();
1172 assert_eq!(w.shape(), &[0]);
1173 }
1174
1175 #[test]
1176 fn bartlett_m1() {
1177 let w = bartlett(1).unwrap();
1178 assert_eq!(w.as_slice().unwrap(), &[1.0]);
1179 }
1180
1181 #[test]
1182 fn blackman_m0() {
1183 let w = blackman(0).unwrap();
1184 assert_eq!(w.shape(), &[0]);
1185 }
1186
1187 #[test]
1188 fn blackman_m1() {
1189 let w = blackman(1).unwrap();
1190 assert_eq!(w.as_slice().unwrap(), &[1.0]);
1191 }
1192
1193 #[test]
1194 fn hamming_m0() {
1195 let w = hamming(0).unwrap();
1196 assert_eq!(w.shape(), &[0]);
1197 }
1198
1199 #[test]
1200 fn hamming_m1() {
1201 let w = hamming(1).unwrap();
1202 assert_eq!(w.as_slice().unwrap(), &[1.0]);
1203 }
1204
1205 #[test]
1206 fn hanning_m0() {
1207 let w = hanning(0).unwrap();
1208 assert_eq!(w.shape(), &[0]);
1209 }
1210
1211 #[test]
1212 fn hanning_m1() {
1213 let w = hanning(1).unwrap();
1214 assert_eq!(w.as_slice().unwrap(), &[1.0]);
1215 }
1216
1217 #[test]
1218 fn kaiser_m0() {
1219 let w = kaiser(0, 14.0).unwrap();
1220 assert_eq!(w.shape(), &[0]);
1221 }
1222
1223 #[test]
1224 fn kaiser_m1() {
1225 let w = kaiser(1, 14.0).unwrap();
1226 assert_eq!(w.as_slice().unwrap(), &[1.0]);
1227 }
1228
1229 #[test]
1230 fn kaiser_negative_beta() {
1231 let pos = kaiser(5, 1.0).unwrap();
1233 let neg = kaiser(5, -1.0).unwrap();
1234 assert_eq!(pos.as_slice().unwrap(), neg.as_slice().unwrap());
1235 }
1236
1237 #[test]
1238 fn kaiser_nan_beta() {
1239 assert!(kaiser(5, f64::NAN).is_err());
1240 }
1241
1242 #[test]
1247 fn bartlett_5_ac1() {
1248 let w = bartlett(5).unwrap();
1249 let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
1250 assert_close(w.as_slice().unwrap(), &expected, 1e-14);
1251 }
1252
1253 #[test]
1258 fn kaiser_5_14_ac2() {
1259 let w = kaiser(5, 14.0).unwrap();
1260 let s = w.as_slice().unwrap();
1261 assert_eq!(s.len(), 5);
1262 let expected: [f64; 5] = [
1264 7.72686684e-06,
1265 1.64932188e-01,
1266 1.0,
1267 1.64932188e-01,
1268 7.72686684e-06,
1269 ];
1270 for (i, (&a, &e)) in s.iter().zip(expected.iter()).enumerate() {
1272 let tol = if e.abs() < 1e-4 { 1e-8 } else { 1e-6 };
1273 assert!(
1274 (a - e).abs() <= tol,
1275 "kaiser element {i}: {a} vs {e} (diff = {})",
1276 (a - e).abs()
1277 );
1278 }
1279 }
1280
1281 #[test]
1287 fn blackman_5() {
1288 let w = blackman(5).unwrap();
1289 assert_eq!(w.shape(), &[5]);
1290 let s = w.as_slice().unwrap();
1291 let expected = [
1292 -1.38777878e-17,
1293 3.40000000e-01,
1294 1.00000000e+00,
1295 3.40000000e-01,
1296 -1.38777878e-17,
1297 ];
1298 assert_close(s, &expected, 1e-10);
1299 }
1300
1301 #[test]
1303 fn hamming_5() {
1304 let w = hamming(5).unwrap();
1305 assert_eq!(w.shape(), &[5]);
1306 let s = w.as_slice().unwrap();
1307 let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
1308 assert_close(s, &expected, 1e-14);
1309 }
1310
1311 #[test]
1313 fn hanning_5() {
1314 let w = hanning(5).unwrap();
1315 assert_eq!(w.shape(), &[5]);
1316 let s = w.as_slice().unwrap();
1317 let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
1318 assert_close(s, &expected, 1e-14);
1319 }
1320
1321 #[test]
1323 fn bartlett_12() {
1324 let w = bartlett(12).unwrap();
1325 assert_eq!(w.shape(), &[12]);
1326 let s = w.as_slice().unwrap();
1327 assert!((s[0] - 0.0).abs() < 1e-14);
1329 assert!((s[11] - 0.0).abs() < 1e-14);
1330 for i in 0..6 {
1332 assert!((s[i] - s[11 - i]).abs() < 1e-14, "symmetry at {i}");
1333 }
1334 }
1335
1336 #[test]
1338 fn all_windows_symmetric() {
1339 let m = 7;
1340 let windows: Vec<Array<f64, Ix1>> = vec![
1341 bartlett(m).unwrap(),
1342 blackman(m).unwrap(),
1343 hamming(m).unwrap(),
1344 hanning(m).unwrap(),
1345 kaiser(m, 5.0).unwrap(),
1346 ];
1347 for (idx, w) in windows.iter().enumerate() {
1348 let s = w.as_slice().unwrap();
1349 for i in 0..m / 2 {
1350 assert!(
1351 (s[i] - s[m - 1 - i]).abs() < 1e-12,
1352 "window {idx} not symmetric at {i}"
1353 );
1354 }
1355 }
1356 }
1357
1358 #[test]
1360 fn all_windows_peak_at_center() {
1361 let m = 9;
1362 let windows: Vec<Array<f64, Ix1>> = vec![
1363 bartlett(m).unwrap(),
1364 blackman(m).unwrap(),
1365 hamming(m).unwrap(),
1366 hanning(m).unwrap(),
1367 kaiser(m, 5.0).unwrap(),
1368 ];
1369 for (idx, w) in windows.iter().enumerate() {
1370 let s = w.as_slice().unwrap();
1371 let center = s[m / 2];
1372 assert!(
1373 (center - 1.0).abs() < 1e-10,
1374 "window {idx} center = {center}, expected 1.0"
1375 );
1376 }
1377 }
1378
1379 #[test]
1381 fn kaiser_beta_zero() {
1382 let w = kaiser(5, 0.0).unwrap();
1383 let s = w.as_slice().unwrap();
1384 for &v in s {
1385 assert!((v - 1.0).abs() < 1e-10, "expected 1.0, got {v}");
1386 }
1387 }
1388
1389 #[test]
1390 fn bessel_i0_scalar_zero() {
1391 assert!((bessel_i0_scalar::<f64>(0.0) - 1.0).abs() < 1e-6);
1392 }
1393
1394 #[test]
1395 fn bessel_i0_scalar_known() {
1396 assert!((bessel_i0_scalar::<f64>(1.0) - 1.266_065_877_752_008_2).abs() < 1e-7);
1401 assert!((bessel_i0_scalar::<f64>(5.0) - 27.239_871_823_604_443).abs() < 1e-5);
1403 let expected_i0_10 = 2_815.716_628_466_254;
1405 assert!((bessel_i0_scalar::<f64>(10.0) - expected_i0_10).abs() / expected_i0_10 < 1e-5);
1406 }
1407
1408 #[test]
1411 fn bartlett_m2_is_zeros() {
1412 let w = bartlett(2).unwrap();
1416 assert_eq!(w.as_slice().unwrap(), &[0.0, 0.0]);
1417 }
1418
1419 #[test]
1420 fn hanning_m2_is_zeros() {
1421 let w = hanning(2).unwrap();
1425 let s = w.as_slice().unwrap();
1426 assert!(s[0].abs() < 1e-14);
1427 assert!(s[1].abs() < 1e-14);
1428 }
1429
1430 #[test]
1431 fn bartlett_even_length_is_symmetric() {
1432 for &m in &[4usize, 6, 8, 10] {
1435 let w = bartlett(m).unwrap();
1436 let s = w.as_slice().unwrap();
1437 for i in 0..m / 2 {
1438 assert!(
1439 (s[i] - s[m - 1 - i]).abs() < 1e-14,
1440 "bartlett({m}) not symmetric at {i}"
1441 );
1442 }
1443 }
1444 }
1445
1446 #[test]
1447 fn blackman_even_length_is_symmetric() {
1448 for &m in &[4usize, 6, 8, 10] {
1449 let w = blackman(m).unwrap();
1450 let s = w.as_slice().unwrap();
1451 for i in 0..m / 2 {
1452 assert!(
1453 (s[i] - s[m - 1 - i]).abs() < 1e-14,
1454 "blackman({m}) not symmetric at {i}"
1455 );
1456 }
1457 }
1458 }
1459
1460 #[test]
1461 fn kaiser_large_beta_errors() {
1462 assert!(kaiser(8, 800.0).is_err());
1465 assert!(kaiser(8, 1000.0).is_err());
1466 assert!(kaiser(8, 700.0).is_ok());
1468 }
1469
1470 fn close(a: f64, b: f64, tol: f64) -> bool {
1476 (a - b).abs() < tol
1477 }
1478
1479 #[test]
1482 fn cosine_length_and_endpoints() {
1483 let w = cosine(8).unwrap();
1486 let s = w.as_slice().unwrap();
1487 assert_eq!(s.len(), 8);
1488 for i in 0..4 {
1490 assert!(close(s[i], s[7 - i], 1e-14));
1491 }
1492 }
1493
1494 #[test]
1495 fn cosine_m1_and_m0() {
1496 assert_eq!(cosine(0).unwrap().shape(), &[0]);
1497 assert_eq!(cosine(1).unwrap().as_slice().unwrap(), &[1.0]);
1498 }
1499
1500 #[test]
1503 fn exponential_centred_default() {
1504 let w = exponential(8, None, 1.0).unwrap();
1507 let s = w.as_slice().unwrap();
1508 for i in 0..4 {
1510 assert!(close(s[i], s[7 - i], 1e-14));
1511 }
1512 let centre_max = s[3].max(s[4]);
1514 for &v in s {
1515 assert!(v <= centre_max + 1e-14);
1516 }
1517 }
1518
1519 #[test]
1520 fn exponential_rejects_nonpositive_tau() {
1521 assert!(exponential(8, None, 0.0).is_err());
1522 assert!(exponential(8, None, -1.0).is_err());
1523 assert!(exponential(8, None, f64::NAN).is_err());
1524 }
1525
1526 #[test]
1529 fn gaussian_centre_is_one() {
1530 let w = gaussian(11, 2.0).unwrap();
1532 let s = w.as_slice().unwrap();
1533 assert!(close(s[5], 1.0, 1e-14));
1534 }
1535
1536 #[test]
1537 fn gaussian_known_value() {
1538 let w = gaussian(7, 1.0).unwrap();
1540 let s = w.as_slice().unwrap();
1541 assert!(close(s[4], (-0.5_f64).exp(), 1e-14));
1542 }
1543
1544 #[test]
1545 fn gaussian_rejects_nonpositive_std() {
1546 assert!(gaussian(8, 0.0).is_err());
1547 assert!(gaussian(8, -1.0).is_err());
1548 }
1549
1550 #[test]
1553 fn general_cosine_with_hann_coeffs_matches_hann() {
1554 let m = 9;
1556 let gc = general_cosine(m, &[0.5, 0.5]).unwrap();
1557 let hn = hanning(m).unwrap();
1558 for i in 0..m {
1559 assert!(close(
1560 gc.as_slice().unwrap()[i],
1561 hn.as_slice().unwrap()[i],
1562 1e-14,
1563 ));
1564 }
1565 }
1566
1567 #[test]
1568 fn general_cosine_with_blackman_coeffs_matches_blackman() {
1569 let m = 9;
1571 let gc = general_cosine(m, &[0.42, 0.5, 0.08]).unwrap();
1572 let bk = blackman(m).unwrap();
1573 for i in 0..m {
1574 assert!(close(
1575 gc.as_slice().unwrap()[i],
1576 bk.as_slice().unwrap()[i],
1577 1e-12,
1578 ));
1579 }
1580 }
1581
1582 #[test]
1583 fn general_cosine_rejects_empty_coeffs() {
1584 assert!(general_cosine(8, &[]).is_err());
1585 }
1586
1587 #[test]
1590 fn general_hamming_alpha_half_matches_hann() {
1591 let m = 9;
1592 let gh = general_hamming(m, 0.5).unwrap();
1593 let hn = hanning(m).unwrap();
1594 for i in 0..m {
1595 assert!(close(
1596 gh.as_slice().unwrap()[i],
1597 hn.as_slice().unwrap()[i],
1598 1e-14,
1599 ));
1600 }
1601 }
1602
1603 #[test]
1604 fn general_hamming_alpha_054_matches_hamming() {
1605 let m = 9;
1606 let gh = general_hamming(m, 0.54).unwrap();
1607 let hm = hamming(m).unwrap();
1608 for i in 0..m {
1609 assert!(close(
1610 gh.as_slice().unwrap()[i],
1611 hm.as_slice().unwrap()[i],
1612 1e-14,
1613 ));
1614 }
1615 }
1616
1617 #[test]
1620 fn nuttall_endpoints_are_small() {
1621 let w = nuttall(64).unwrap();
1624 let s = w.as_slice().unwrap();
1625 assert!(s[0].abs() < 1e-2);
1626 assert!(s[s.len() - 1].abs() < 1e-2);
1627 }
1628
1629 #[test]
1630 fn nuttall_is_symmetric() {
1631 let m = 33;
1632 let w = nuttall(m).unwrap();
1633 let s = w.as_slice().unwrap();
1634 for i in 0..m / 2 {
1635 assert!(close(s[i], s[m - 1 - i], 1e-14));
1636 }
1637 }
1638
1639 #[test]
1642 fn parzen_endpoints_are_zero() {
1643 let w = parzen(16).unwrap();
1644 let s = w.as_slice().unwrap();
1645 assert!(s[0].abs() < 1e-2);
1647 assert!(s[s.len() - 1].abs() < 1e-2);
1648 }
1649
1650 #[test]
1651 fn parzen_centre_is_one() {
1652 let w = parzen(13).unwrap();
1654 let s = w.as_slice().unwrap();
1655 assert!(close(s[6], 1.0, 1e-14));
1656 }
1657
1658 #[test]
1659 fn parzen_is_symmetric() {
1660 let m = 21;
1661 let w = parzen(m).unwrap();
1662 let s = w.as_slice().unwrap();
1663 for i in 0..m / 2 {
1664 assert!(close(s[i], s[m - 1 - i], 1e-14));
1665 }
1666 }
1667
1668 #[test]
1671 fn taylor_default_normalised_centre_is_one() {
1672 let w = taylor(33, 4, 30.0, true).unwrap();
1674 let s = w.as_slice().unwrap();
1675 assert!(close(s[16], 1.0, 1e-12));
1676 }
1677
1678 #[test]
1679 fn taylor_is_symmetric() {
1680 let m = 33;
1681 let w = taylor(m, 4, 30.0, true).unwrap();
1682 let s = w.as_slice().unwrap();
1683 for i in 0..m / 2 {
1684 assert!(close(s[i], s[m - 1 - i], 1e-12));
1685 }
1686 }
1687
1688 #[test]
1689 fn taylor_rejects_nbar_zero() {
1690 assert!(taylor(8, 0, 30.0, true).is_err());
1691 assert!(taylor(8, 4, f64::NAN, true).is_err());
1692 }
1693
1694 #[test]
1695 fn taylor_even_m_matches_scipy_analytic_peak() {
1696 let w = taylor(16, 4, 30.0, true).unwrap();
1705 let s = w.as_slice().unwrap();
1706 let expected_first = 0.252_321_041_674_507_f64;
1707 assert!(
1708 (s[0] - expected_first).abs() < 1e-13,
1709 "taylor(16,4,30,true)[0] = {} expected {}",
1710 s[0],
1711 expected_first,
1712 );
1713 assert!(s[7] < 1.0 && s[8] < 1.0);
1718 assert!(s[7] > 0.99 && s[8] > 0.99);
1719 }
1720
1721 #[test]
1724 fn tukey_alpha_zero_is_rectangular() {
1725 let w = tukey(8, 0.0).unwrap();
1726 for &v in w.as_slice().unwrap() {
1727 assert!(close(v, 1.0, 1e-14));
1728 }
1729 }
1730
1731 #[test]
1732 fn tukey_alpha_one_matches_hann() {
1733 let m = 9;
1735 let tk = tukey(m, 1.0).unwrap();
1736 let hn = hanning(m).unwrap();
1737 for i in 0..m {
1738 assert!(close(
1739 tk.as_slice().unwrap()[i],
1740 hn.as_slice().unwrap()[i],
1741 1e-12,
1742 ));
1743 }
1744 }
1745
1746 #[test]
1747 fn tukey_centre_is_one() {
1748 let m = 21;
1749 let w = tukey(m, 0.5).unwrap();
1750 assert!(close(w.as_slice().unwrap()[m / 2], 1.0, 1e-14));
1752 }
1753
1754 #[test]
1755 fn tukey_rejects_invalid_alpha() {
1756 assert!(tukey(8, -0.1).is_err());
1757 assert!(tukey(8, 1.1).is_err());
1758 assert!(tukey(8, f64::NAN).is_err());
1759 }
1760}
1761
1762#[cfg(test)]
1763mod debug_test_removed {
1764 }