#![allow(clippy::unreadable_literal)]
use ferray_core::Array;
use ferray_core::dimension::Ix1;
use ferray_core::error::{FerrayError, FerrayResult};
use std::f64::consts::PI;
use ferray_ufunc::ops::special::bessel_i0_scalar;
#[inline]
fn gen_window<F: FnMut(usize) -> f64>(m: usize, mut f: F) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let mut data = Vec::with_capacity(m);
for n in 0..m {
data.push(f(n));
}
Array::from_vec(Ix1::new([m]), data)
}
fn cast_to_f32(arr: &Array<f64, Ix1>) -> FerrayResult<Array<f32, Ix1>> {
let data: Vec<f32> = arr.iter().map(|&x| x as f32).collect();
Array::<f32, Ix1>::from_vec(Ix1::new([data.len()]), data)
}
pub fn bartlett_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
cast_to_f32(&bartlett(m)?)
}
pub fn blackman_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
cast_to_f32(&blackman(m)?)
}
pub fn hamming_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
cast_to_f32(&hamming(m)?)
}
pub fn hanning_f32(m: usize) -> FerrayResult<Array<f32, Ix1>> {
cast_to_f32(&hanning(m)?)
}
pub fn kaiser_f32(m: usize, beta: f64) -> FerrayResult<Array<f32, Ix1>> {
cast_to_f32(&kaiser(m, beta)?)
}
pub fn bartlett(m: usize) -> FerrayResult<Array<f64, Ix1>> {
let half = (m.saturating_sub(1)) as f64 / 2.0;
gen_window(m, |n| 1.0 - ((n as f64 - half) / half).abs())
}
pub fn blackman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
let denom = (m.saturating_sub(1)) as f64;
gen_window(m, |n| {
let x = n as f64;
0.08f64.mul_add(
(4.0 * PI * x / denom).cos(),
0.5f64.mul_add(-(2.0 * PI * x / denom).cos(), 0.42),
)
})
}
pub fn hamming(m: usize) -> FerrayResult<Array<f64, Ix1>> {
let denom = (m.saturating_sub(1)) as f64;
gen_window(m, |n| {
0.46f64.mul_add(-(2.0 * PI * n as f64 / denom).cos(), 0.54)
})
}
pub fn hanning(m: usize) -> FerrayResult<Array<f64, Ix1>> {
let denom = (m.saturating_sub(1)) as f64;
gen_window(m, |n| 0.5 * (1.0 - (2.0 * PI * n as f64 / denom).cos()))
}
pub fn kaiser(m: usize, beta: f64) -> FerrayResult<Array<f64, Ix1>> {
if beta.is_nan() {
return Err(FerrayError::invalid_value("kaiser: beta must not be NaN"));
}
let beta = beta.abs();
const BETA_OVERFLOW_THRESHOLD: f64 = 708.0;
if beta > BETA_OVERFLOW_THRESHOLD {
return Err(FerrayError::invalid_value(format!(
"kaiser: |beta| = {beta} exceeds the safe range ({BETA_OVERFLOW_THRESHOLD}) \
for f64 I_0; the window would be NaN everywhere"
)));
}
let i0_beta = bessel_i0_scalar::<f64>(beta);
let alpha = (m.saturating_sub(1)) as f64 / 2.0;
gen_window(m, |n| {
let t = (n as f64 - alpha) / alpha;
let arg = beta * (1.0 - t * t).max(0.0).sqrt();
bessel_i0_scalar::<f64>(arg) / i0_beta
})
}
pub fn cosine(m: usize) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let mf = m as f64;
gen_window(m, |n| (PI * (n as f64 + 0.5) / mf).sin())
}
pub fn exponential(m: usize, center: Option<f64>, tau: f64) -> FerrayResult<Array<f64, Ix1>> {
if !tau.is_finite() || tau <= 0.0 {
return Err(FerrayError::invalid_value(format!(
"exponential: tau must be positive and finite, got {tau}"
)));
}
let centre = center.unwrap_or((m.saturating_sub(1)) as f64 / 2.0);
gen_window(m, |n| (-((n as f64) - centre).abs() / tau).exp())
}
pub fn gaussian(m: usize, std: f64) -> FerrayResult<Array<f64, Ix1>> {
if !std.is_finite() || std <= 0.0 {
return Err(FerrayError::invalid_value(format!(
"gaussian: std must be positive and finite, got {std}"
)));
}
let centre = (m.saturating_sub(1)) as f64 / 2.0;
gen_window(m, |n| {
let z = ((n as f64) - centre) / std;
(-0.5 * z * z).exp()
})
}
pub fn general_cosine(m: usize, coeffs: &[f64]) -> FerrayResult<Array<f64, Ix1>> {
if coeffs.is_empty() {
return Err(FerrayError::invalid_value(
"general_cosine: coeffs must not be empty",
));
}
let denom = (m.saturating_sub(1)) as f64;
let coeffs = coeffs.to_vec();
gen_window(m, |n| {
let nf = n as f64;
let mut sum = 0.0_f64;
for (k, &a) in coeffs.iter().enumerate() {
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
sum += sign * a * (2.0 * PI * (k as f64) * nf / denom).cos();
}
sum
})
}
pub fn general_hamming(m: usize, alpha: f64) -> FerrayResult<Array<f64, Ix1>> {
if !alpha.is_finite() {
return Err(FerrayError::invalid_value(format!(
"general_hamming: alpha must be finite, got {alpha}"
)));
}
let denom = (m.saturating_sub(1)) as f64;
gen_window(m, |n| {
alpha - (1.0 - alpha) * (2.0 * PI * (n as f64) / denom).cos()
})
}
pub fn nuttall(m: usize) -> FerrayResult<Array<f64, Ix1>> {
general_cosine(m, &[0.3635819, 0.4891775, 0.1365995, 0.0106411])
}
pub fn parzen(m: usize) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let half = m as f64 / 2.0;
gen_window(m, |n| {
let x = (n as f64) - (m as f64 - 1.0) / 2.0;
let r = x.abs() / half;
if r <= 0.5 {
1.0 - 6.0 * r * r + 6.0 * r * r * r
} else if r <= 1.0 {
let one_minus_r = 1.0 - r;
2.0 * one_minus_r * one_minus_r * one_minus_r
} else {
0.0
}
})
}
pub fn chebwin(m: usize, at: f64) -> FerrayResult<Array<f64, Ix1>> {
if !at.is_finite() || at <= 0.0 {
return Err(FerrayError::invalid_value(format!(
"chebwin: at must be a positive finite dB attenuation, got {at}"
)));
}
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let order = (m - 1) as f64;
let r = 10.0_f64.powf(at / 20.0);
let beta = (r.acosh() / order).cosh();
let mut spectrum = Vec::with_capacity(m);
for k in 0..m {
let x = beta * (PI * k as f64 / m as f64).cos();
spectrum.push(chebyshev_t_extended(x, order));
}
let mf = m as f64;
let half_dft = |idx: usize| -> f64 {
if m % 2 == 1 {
spectrum
.iter()
.enumerate()
.map(|(k, &pk)| pk * (-2.0 * PI * k as f64 * idx as f64 / mf).cos())
.sum()
} else {
spectrum
.iter()
.enumerate()
.map(|(k, &pk)| {
let phase = PI * k as f64 * (1.0 - 2.0 * idx as f64) / mf;
pk * phase.cos()
})
.sum()
}
};
let half_len = m / 2 + 1;
let mut half: Vec<f64> = (0..half_len).map(half_dft).collect();
let denom = if m % 2 == 1 { half[0] } else { half[1] };
if denom != 0.0 {
for v in &mut half {
*v /= denom;
}
}
let mut w = Vec::with_capacity(m);
if m % 2 == 1 {
for &v in half.iter().skip(1).rev() {
w.push(v);
}
w.extend_from_slice(&half);
} else {
let n = m / 2 + 1;
for &v in half[1..n].iter().rev() {
w.push(v);
}
w.extend_from_slice(&half[1..n]);
}
Array::from_vec(Ix1::new([m]), w)
}
pub fn dpss(m: usize, nw: f64) -> FerrayResult<Array<f64, Ix1>> {
if !nw.is_finite() || nw <= 0.0 {
return Err(FerrayError::invalid_value(format!(
"dpss: nw must be a positive finite half bandwidth, got {nw}"
)));
}
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
if nw >= (m as f64) / 2.0 {
return Err(FerrayError::invalid_value(format!(
"dpss: nw = {nw} must be < M/2 = {} for a non-degenerate band",
(m as f64) / 2.0
)));
}
let mf = m as f64;
let cos_w = (2.0 * PI * nw / mf).cos();
let diag: Vec<f64> = (0..m)
.map(|i| {
let centre = (mf - 1.0) / 2.0 - i as f64;
centre * centre * cos_w
})
.collect();
let off: Vec<f64> = (0..m.saturating_sub(1))
.map(|i| (i as f64 + 1.0) * (mf - i as f64 - 1.0) / 2.0)
.collect();
let mut v: Vec<f64> = (0..m)
.map(|i| {
let centred = (i as f64 - (mf - 1.0) / 2.0) / mf;
(-2.0 * centred * centred).exp()
})
.collect();
let n0 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
for x in &mut v {
*x /= n0.max(f64::MIN_POSITIVE);
}
let mut prev_eig = 0.0_f64;
for _ in 0..500 {
let mut next = vec![0.0_f64; m];
for i in 0..m {
next[i] = diag[i] * v[i];
if i > 0 {
next[i] += off[i - 1] * v[i - 1];
}
if i + 1 < m {
next[i] += off[i] * v[i + 1];
}
}
let eig: f64 = v.iter().zip(next.iter()).map(|(a, b)| a * b).sum();
let norm = next.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm < f64::MIN_POSITIVE {
return Err(FerrayError::invalid_value(
"dpss: power iteration produced a zero vector",
));
}
for x in &mut next {
*x /= norm;
}
v = next;
if (eig - prev_eig).abs() < 1e-12 * eig.abs().max(1.0) {
break;
}
prev_eig = eig;
}
let centre_val = if m % 2 == 1 {
v[m / 2]
} else {
v[m / 2 - 1] + v[m / 2]
};
if centre_val < 0.0 {
for x in &mut v {
*x = -*x;
}
}
let peak = v.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if peak > 0.0 {
for x in &mut v {
*x /= peak;
}
}
Array::from_vec(Ix1::new([m]), v)
}
fn chebyshev_t_extended(x: f64, n: f64) -> f64 {
if x.abs() <= 1.0 {
(n * x.acos()).cos()
} else {
let mag = (n * x.abs().acosh()).cosh();
if x < 0.0 && (n as i64) % 2 != 0 {
-mag
} else {
mag
}
}
}
pub fn boxcar(m: usize) -> FerrayResult<Array<f64, Ix1>> {
gen_window(m, |_| 1.0)
}
pub fn triang(m: usize) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let mf = m as f64;
let centre = (mf - 1.0) / 2.0;
if m % 2 == 0 {
gen_window(m, |n| 1.0 - (2.0 * n as f64 - centre - centre).abs() / mf)
} else {
gen_window(m, |n| {
1.0 - (2.0 * n as f64 - centre - centre).abs() / (mf + 1.0)
})
}
}
pub fn bohman(m: usize) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let mf = (m - 1) as f64;
let mut data = Vec::with_capacity(m);
for n in 0..m {
if n == 0 || n == m - 1 {
data.push(0.0);
continue;
}
let r = (2.0 * n as f64 / mf - 1.0).abs();
let v = (1.0 - r) * (PI * r).cos() + (PI * r).sin() / PI;
data.push(v);
}
Array::from_vec(Ix1::new([m]), data)
}
pub fn flattop(m: usize) -> FerrayResult<Array<f64, Ix1>> {
const COEFFS: [f64; 5] = [
0.215_578_95,
0.416_631_58,
0.277_263_158,
0.083_578_95,
0.006_947_368,
];
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
let denom = (m - 1).max(1) as f64;
gen_window(m, |n| {
let phase = 2.0 * PI * n as f64 / denom;
let mut acc = COEFFS[0];
for (k, &c) in COEFFS.iter().enumerate().skip(1) {
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
acc += sign * c * (k as f64 * phase).cos();
}
acc
})
}
pub fn lanczos(m: usize) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
let denom = (m - 1) as f64;
gen_window(m, |n| {
let x = 2.0 * n as f64 / denom - 1.0;
if x.abs() < f64::EPSILON {
1.0
} else {
let pi_x = PI * x;
pi_x.sin() / pi_x
}
})
}
pub fn taylor(m: usize, nbar: usize, sll: f64, norm: bool) -> FerrayResult<Array<f64, Ix1>> {
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
if nbar == 0 {
return Err(FerrayError::invalid_value("taylor: nbar must be >= 1"));
}
if !sll.is_finite() {
return Err(FerrayError::invalid_value("taylor: sll must be finite"));
}
let r = 10.0_f64.powf(sll / 20.0);
let b = r.acosh() / PI;
let nbar_f = nbar as f64;
let sigma2 = (nbar_f * nbar_f) / (b * b + (nbar_f - 0.5) * (nbar_f - 0.5));
let mut f_coeffs = Vec::with_capacity(nbar.saturating_sub(1));
for mm in 1..nbar {
let mmf = mm as f64;
let mut num = 1.0_f64;
for n in 1..nbar {
let nf = n as f64;
num *= 1.0 - mmf * mmf / (sigma2 * (b * b + (nf - 0.5) * (nf - 0.5)));
}
let sign = if mm % 2 == 0 { -1.0 } else { 1.0 };
let mut den = 1.0_f64;
for n in 1..nbar {
if n == mm {
continue;
}
let nf = n as f64;
den *= 1.0 - mmf * mmf / (nf * nf);
}
f_coeffs.push(0.5 * sign * num / den);
}
let denom = (m - 1) as f64;
let arr = gen_window(m, |n| {
let xn = (n as f64) - denom / 2.0;
let mut w = 1.0_f64;
for (idx, &fk) in f_coeffs.iter().enumerate() {
let kk = (idx + 1) as f64;
w += 2.0 * fk * (2.0 * PI * kk * xn / m as f64).cos();
}
w
})?;
if !norm {
return Ok(arr);
}
let s = arr.as_slice().unwrap().to_vec();
let centre_val = if m % 2 == 1 {
s[m / 2]
} else {
0.5 * (s[m / 2 - 1] + s[m / 2])
};
if centre_val == 0.0 {
return Ok(arr); }
let normed: Vec<f64> = s.into_iter().map(|v| v / centre_val).collect();
Array::from_vec(Ix1::new([m]), normed)
}
pub fn tukey(m: usize, alpha: f64) -> FerrayResult<Array<f64, Ix1>> {
if !alpha.is_finite() || !(0.0..=1.0).contains(&alpha) {
return Err(FerrayError::invalid_value(format!(
"tukey: alpha must be in [0, 1], got {alpha}"
)));
}
if m == 0 {
return Array::from_vec(Ix1::new([0]), vec![]);
}
if m == 1 {
return Array::from_vec(Ix1::new([1]), vec![1.0]);
}
if alpha == 0.0 {
return Array::from_vec(Ix1::new([m]), vec![1.0; m]);
}
let denom = (m - 1) as f64;
let width = alpha * denom / 2.0;
gen_window(m, |n| {
let nf = n as f64;
if nf < width {
0.5 * (1.0 + (PI * (nf / width - 1.0)).cos())
} else if nf <= denom - width {
1.0
} else {
0.5 * (1.0 + (PI * ((denom - nf) / width - 1.0)).cos())
}
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dpss_max_one_and_symmetric() {
let w = dpss(64, 3.0).unwrap();
let s = w.as_slice().unwrap();
let max = s.iter().copied().fold(f64::NEG_INFINITY, f64::max);
assert!((max - 1.0).abs() < 1e-12, "max = {max}");
for i in 0..s.len() / 2 {
let mirror = s.len() - 1 - i;
assert!(
(s[i] - s[mirror]).abs() < 1e-7,
"asymmetric at i={i}: {} vs {}",
s[i],
s[mirror]
);
}
}
#[test]
fn dpss_centre_is_positive_and_peak() {
let w = dpss(48, 2.5).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!(s[mid] > 0.0);
assert!((s[mid] - 1.0).abs() < 1e-12);
}
#[test]
fn dpss_endpoints_smaller_than_centre() {
let w = dpss(64, 3.0).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!(s[mid] > s[0]);
assert!(s[mid] > s[s.len() - 1]);
assert!(s[0].abs() < 0.1 * s[mid]);
}
#[test]
fn dpss_rejects_invalid_nw() {
assert!(dpss(32, -1.0).is_err());
assert!(dpss(32, 0.0).is_err());
assert!(dpss(32, f64::NAN).is_err());
assert!(dpss(8, 4.0).is_err());
assert!(dpss(8, 5.0).is_err());
}
#[test]
fn dpss_matches_scipy_within_relative_tolerance() {
let want_first_half: [f64; 8] = [
0.00710856, 0.03620714, 0.10884379, 0.24276927, 0.43733689, 0.6634221, 0.86701952,
0.98841699,
];
let got = dpss(16, 3.0).unwrap();
let s = got.as_slice().unwrap();
for (i, (&g, &w)) in s[..8].iter().zip(want_first_half.iter()).enumerate() {
let tol = 0.05 * w.abs().max(1e-3);
assert!((g - w).abs() <= tol, "i={i}: got={g}, want={w}, tol={tol}");
}
}
#[test]
fn dpss_m0_m1_edge_cases() {
let z = dpss(0, 2.0).unwrap();
assert_eq!(z.shape(), &[0]);
let one = dpss(1, 0.5).unwrap();
assert_eq!(one.as_slice().unwrap(), &[1.0]);
}
#[test]
fn chebwin_centre_is_one_and_symmetric() {
let w = chebwin(11, 50.0).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!((s[mid] - 1.0).abs() < 1e-6, "centre = {}", s[mid]);
for i in 0..s.len() / 2 {
assert!(
(s[i] - s[s.len() - 1 - i]).abs() < 1e-9,
"asymmetric at i={i}: {} vs {}",
s[i],
s[s.len() - 1 - i]
);
}
}
#[test]
fn chebwin_even_length_symmetric() {
let w = chebwin(8, 40.0).unwrap();
let s = w.as_slice().unwrap();
for i in 0..s.len() / 2 {
assert!(
(s[i] - s[s.len() - 1 - i]).abs() < 1e-9,
"asymmetric at i={i}"
);
}
let max = s.iter().copied().fold(f64::NEG_INFINITY, f64::max);
assert!((max - 1.0).abs() < 1e-6);
}
#[test]
fn chebwin_endpoints_smaller_than_centre() {
let w = chebwin(31, 80.0).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!(s[mid] > s[0]);
assert!(s[mid] > s[s.len() - 1]);
}
#[test]
fn chebwin_rejects_invalid_attenuation() {
assert!(chebwin(16, -10.0).is_err());
assert!(chebwin(16, 0.0).is_err());
assert!(chebwin(16, f64::NAN).is_err());
}
#[test]
fn chebwin_matches_scipy_known_values() {
let want = [
0.06228583, 0.20113857, 0.42847525, 0.69573494, 0.91497506, 1.0, 0.91497506,
0.69573494, 0.42847525, 0.20113857, 0.06228583,
];
let got = chebwin(11, 50.0).unwrap();
let s = got.as_slice().unwrap();
for (i, (&g, &w)) in s.iter().zip(want.iter()).enumerate() {
assert!((g - w).abs() < 1e-6, "i={i}: got={g}, want={w}");
}
}
#[test]
fn chebwin_matches_scipy_even_length() {
let want = [
0.14609713, 0.41790422, 0.75944595, 1.0, 1.0, 0.75944595, 0.41790422, 0.14609713,
];
let got = chebwin(8, 40.0).unwrap();
let s = got.as_slice().unwrap();
for (i, (&g, &w)) in s.iter().zip(want.iter()).enumerate() {
assert!((g - w).abs() < 1e-6, "i={i}: got={g}, want={w}");
}
}
#[test]
fn chebwin_m0_m1_edge_cases() {
let z = chebwin(0, 50.0).unwrap();
assert_eq!(z.shape(), &[0]);
let one = chebwin(1, 50.0).unwrap();
assert_eq!(one.as_slice().unwrap(), &[1.0]);
}
#[test]
fn boxcar_all_ones() {
let w = boxcar(8).unwrap();
let s = w.as_slice().unwrap();
for &v in s {
assert!((v - 1.0).abs() < 1e-15);
}
}
#[test]
fn triang_centre_is_max_and_endpoints_smaller() {
let w = triang(7).unwrap();
let s = w.as_slice().unwrap();
for i in 0..s.len() / 2 {
assert!((s[i] - s[s.len() - 1 - i]).abs() < 1e-12);
}
let mid = s.len() / 2;
for i in 0..s.len() {
if i != mid {
assert!(s[i] <= s[mid] + 1e-12);
}
}
}
#[test]
fn bohman_zero_at_endpoints_and_one_at_centre() {
let w = bohman(5).unwrap();
let s = w.as_slice().unwrap();
assert!(s[0].abs() < 1e-12);
assert!(s[s.len() - 1].abs() < 1e-12);
let mid = s.len() / 2;
assert!((s[mid] - 1.0).abs() < 1e-6);
}
#[test]
fn flattop_centre_around_one_and_negative_lobes_present() {
let w = flattop(11).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!((s[mid] - 1.0).abs() < 0.01);
let any_negative = s.iter().any(|&v| v < -0.001);
assert!(any_negative, "flattop should have negative side lobes");
}
#[test]
fn lanczos_centre_is_one_and_endpoints_zero() {
let w = lanczos(9).unwrap();
let s = w.as_slice().unwrap();
let mid = s.len() / 2;
assert!((s[mid] - 1.0).abs() < 1e-12);
assert!(s[0].abs() < 1e-12);
assert!(s[s.len() - 1].abs() < 1e-12);
}
#[test]
fn small_m_edge_cases_handled() {
for f in [triang as fn(usize) -> _, bohman, lanczos] {
let z = f(0).unwrap();
assert_eq!(z.shape(), &[0]);
let one = f(1).unwrap();
assert_eq!(one.shape(), &[1]);
assert_eq!(one.as_slice().unwrap(), &[1.0]);
}
}
#[test]
fn f32_windows_match_f64_within_f32_eps() {
for m in [1usize, 5, 16, 64] {
for (name, got, want) in [
("bartlett", bartlett_f32(m).unwrap(), bartlett(m).unwrap()),
("blackman", blackman_f32(m).unwrap(), blackman(m).unwrap()),
("hamming", hamming_f32(m).unwrap(), hamming(m).unwrap()),
("hanning", hanning_f32(m).unwrap(), hanning(m).unwrap()),
] {
assert_eq!(got.shape(), want.shape(), "{name} m={m} shape");
let g = got.as_slice().unwrap();
let w = want.as_slice().unwrap();
for (i, (&a, &b)) in g.iter().zip(w.iter()).enumerate() {
let bf = b as f32;
assert!(
(a - bf).abs() < 1e-6,
"{name} m={m} idx={i}: f32 {a} vs f64 {b}"
);
}
}
}
}
#[test]
fn kaiser_f32_matches_kaiser() {
let m = 20;
let beta = 8.6;
let f32_arr = kaiser_f32(m, beta).unwrap();
let f64_arr = kaiser(m, beta).unwrap();
for (a, b) in f32_arr.iter().zip(f64_arr.iter()) {
assert!((a - *b as f32).abs() < 1e-5);
}
}
fn assert_close(actual: &[f64], expected: &[f64], tol: f64) {
assert_eq!(
actual.len(),
expected.len(),
"length mismatch: {} vs {}",
actual.len(),
expected.len()
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert!(
(a - e).abs() <= tol,
"element {i}: {a} vs {e} (diff = {})",
(a - e).abs()
);
}
}
#[test]
fn bartlett_m0() {
let w = bartlett(0).unwrap();
assert_eq!(w.shape(), &[0]);
}
#[test]
fn bartlett_m1() {
let w = bartlett(1).unwrap();
assert_eq!(w.as_slice().unwrap(), &[1.0]);
}
#[test]
fn blackman_m0() {
let w = blackman(0).unwrap();
assert_eq!(w.shape(), &[0]);
}
#[test]
fn blackman_m1() {
let w = blackman(1).unwrap();
assert_eq!(w.as_slice().unwrap(), &[1.0]);
}
#[test]
fn hamming_m0() {
let w = hamming(0).unwrap();
assert_eq!(w.shape(), &[0]);
}
#[test]
fn hamming_m1() {
let w = hamming(1).unwrap();
assert_eq!(w.as_slice().unwrap(), &[1.0]);
}
#[test]
fn hanning_m0() {
let w = hanning(0).unwrap();
assert_eq!(w.shape(), &[0]);
}
#[test]
fn hanning_m1() {
let w = hanning(1).unwrap();
assert_eq!(w.as_slice().unwrap(), &[1.0]);
}
#[test]
fn kaiser_m0() {
let w = kaiser(0, 14.0).unwrap();
assert_eq!(w.shape(), &[0]);
}
#[test]
fn kaiser_m1() {
let w = kaiser(1, 14.0).unwrap();
assert_eq!(w.as_slice().unwrap(), &[1.0]);
}
#[test]
fn kaiser_negative_beta() {
let pos = kaiser(5, 1.0).unwrap();
let neg = kaiser(5, -1.0).unwrap();
assert_eq!(pos.as_slice().unwrap(), neg.as_slice().unwrap());
}
#[test]
fn kaiser_nan_beta() {
assert!(kaiser(5, f64::NAN).is_err());
}
#[test]
fn bartlett_5_ac1() {
let w = bartlett(5).unwrap();
let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
assert_close(w.as_slice().unwrap(), &expected, 1e-14);
}
#[test]
fn kaiser_5_14_ac2() {
let w = kaiser(5, 14.0).unwrap();
let s = w.as_slice().unwrap();
assert_eq!(s.len(), 5);
let expected: [f64; 5] = [
7.72686684e-06,
1.64932188e-01,
1.0,
1.64932188e-01,
7.72686684e-06,
];
for (i, (&a, &e)) in s.iter().zip(expected.iter()).enumerate() {
let tol = if e.abs() < 1e-4 { 1e-8 } else { 1e-6 };
assert!(
(a - e).abs() <= tol,
"kaiser element {i}: {a} vs {e} (diff = {})",
(a - e).abs()
);
}
}
#[test]
fn blackman_5() {
let w = blackman(5).unwrap();
assert_eq!(w.shape(), &[5]);
let s = w.as_slice().unwrap();
let expected = [
-1.38777878e-17,
3.40000000e-01,
1.00000000e+00,
3.40000000e-01,
-1.38777878e-17,
];
assert_close(s, &expected, 1e-10);
}
#[test]
fn hamming_5() {
let w = hamming(5).unwrap();
assert_eq!(w.shape(), &[5]);
let s = w.as_slice().unwrap();
let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
assert_close(s, &expected, 1e-14);
}
#[test]
fn hanning_5() {
let w = hanning(5).unwrap();
assert_eq!(w.shape(), &[5]);
let s = w.as_slice().unwrap();
let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
assert_close(s, &expected, 1e-14);
}
#[test]
fn bartlett_12() {
let w = bartlett(12).unwrap();
assert_eq!(w.shape(), &[12]);
let s = w.as_slice().unwrap();
assert!((s[0] - 0.0).abs() < 1e-14);
assert!((s[11] - 0.0).abs() < 1e-14);
for i in 0..6 {
assert!((s[i] - s[11 - i]).abs() < 1e-14, "symmetry at {i}");
}
}
#[test]
fn all_windows_symmetric() {
let m = 7;
let windows: Vec<Array<f64, Ix1>> = vec![
bartlett(m).unwrap(),
blackman(m).unwrap(),
hamming(m).unwrap(),
hanning(m).unwrap(),
kaiser(m, 5.0).unwrap(),
];
for (idx, w) in windows.iter().enumerate() {
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(
(s[i] - s[m - 1 - i]).abs() < 1e-12,
"window {idx} not symmetric at {i}"
);
}
}
}
#[test]
fn all_windows_peak_at_center() {
let m = 9;
let windows: Vec<Array<f64, Ix1>> = vec![
bartlett(m).unwrap(),
blackman(m).unwrap(),
hamming(m).unwrap(),
hanning(m).unwrap(),
kaiser(m, 5.0).unwrap(),
];
for (idx, w) in windows.iter().enumerate() {
let s = w.as_slice().unwrap();
let center = s[m / 2];
assert!(
(center - 1.0).abs() < 1e-10,
"window {idx} center = {center}, expected 1.0"
);
}
}
#[test]
fn kaiser_beta_zero() {
let w = kaiser(5, 0.0).unwrap();
let s = w.as_slice().unwrap();
for &v in s {
assert!((v - 1.0).abs() < 1e-10, "expected 1.0, got {v}");
}
}
#[test]
fn bessel_i0_scalar_zero() {
assert!((bessel_i0_scalar::<f64>(0.0) - 1.0).abs() < 1e-6);
}
#[test]
fn bessel_i0_scalar_known() {
assert!((bessel_i0_scalar::<f64>(1.0) - 1.266_065_877_752_008_2).abs() < 1e-7);
assert!((bessel_i0_scalar::<f64>(5.0) - 27.239_871_823_604_443).abs() < 1e-5);
let expected_i0_10 = 2_815.716_628_466_254;
assert!((bessel_i0_scalar::<f64>(10.0) - expected_i0_10).abs() / expected_i0_10 < 1e-5);
}
#[test]
fn bartlett_m2_is_zeros() {
let w = bartlett(2).unwrap();
assert_eq!(w.as_slice().unwrap(), &[0.0, 0.0]);
}
#[test]
fn hanning_m2_is_zeros() {
let w = hanning(2).unwrap();
let s = w.as_slice().unwrap();
assert!(s[0].abs() < 1e-14);
assert!(s[1].abs() < 1e-14);
}
#[test]
fn bartlett_even_length_is_symmetric() {
for &m in &[4usize, 6, 8, 10] {
let w = bartlett(m).unwrap();
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(
(s[i] - s[m - 1 - i]).abs() < 1e-14,
"bartlett({m}) not symmetric at {i}"
);
}
}
}
#[test]
fn blackman_even_length_is_symmetric() {
for &m in &[4usize, 6, 8, 10] {
let w = blackman(m).unwrap();
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(
(s[i] - s[m - 1 - i]).abs() < 1e-14,
"blackman({m}) not symmetric at {i}"
);
}
}
}
#[test]
fn kaiser_large_beta_errors() {
assert!(kaiser(8, 800.0).is_err());
assert!(kaiser(8, 1000.0).is_err());
assert!(kaiser(8, 700.0).is_ok());
}
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn cosine_length_and_endpoints() {
let w = cosine(8).unwrap();
let s = w.as_slice().unwrap();
assert_eq!(s.len(), 8);
for i in 0..4 {
assert!(close(s[i], s[7 - i], 1e-14));
}
}
#[test]
fn cosine_m1_and_m0() {
assert_eq!(cosine(0).unwrap().shape(), &[0]);
assert_eq!(cosine(1).unwrap().as_slice().unwrap(), &[1.0]);
}
#[test]
fn exponential_centred_default() {
let w = exponential(8, None, 1.0).unwrap();
let s = w.as_slice().unwrap();
for i in 0..4 {
assert!(close(s[i], s[7 - i], 1e-14));
}
let centre_max = s[3].max(s[4]);
for &v in s {
assert!(v <= centre_max + 1e-14);
}
}
#[test]
fn exponential_rejects_nonpositive_tau() {
assert!(exponential(8, None, 0.0).is_err());
assert!(exponential(8, None, -1.0).is_err());
assert!(exponential(8, None, f64::NAN).is_err());
}
#[test]
fn gaussian_centre_is_one() {
let w = gaussian(11, 2.0).unwrap();
let s = w.as_slice().unwrap();
assert!(close(s[5], 1.0, 1e-14));
}
#[test]
fn gaussian_known_value() {
let w = gaussian(7, 1.0).unwrap();
let s = w.as_slice().unwrap();
assert!(close(s[4], (-0.5_f64).exp(), 1e-14));
}
#[test]
fn gaussian_rejects_nonpositive_std() {
assert!(gaussian(8, 0.0).is_err());
assert!(gaussian(8, -1.0).is_err());
}
#[test]
fn general_cosine_with_hann_coeffs_matches_hann() {
let m = 9;
let gc = general_cosine(m, &[0.5, 0.5]).unwrap();
let hn = hanning(m).unwrap();
for i in 0..m {
assert!(close(
gc.as_slice().unwrap()[i],
hn.as_slice().unwrap()[i],
1e-14,
));
}
}
#[test]
fn general_cosine_with_blackman_coeffs_matches_blackman() {
let m = 9;
let gc = general_cosine(m, &[0.42, 0.5, 0.08]).unwrap();
let bk = blackman(m).unwrap();
for i in 0..m {
assert!(close(
gc.as_slice().unwrap()[i],
bk.as_slice().unwrap()[i],
1e-12,
));
}
}
#[test]
fn general_cosine_rejects_empty_coeffs() {
assert!(general_cosine(8, &[]).is_err());
}
#[test]
fn general_hamming_alpha_half_matches_hann() {
let m = 9;
let gh = general_hamming(m, 0.5).unwrap();
let hn = hanning(m).unwrap();
for i in 0..m {
assert!(close(
gh.as_slice().unwrap()[i],
hn.as_slice().unwrap()[i],
1e-14,
));
}
}
#[test]
fn general_hamming_alpha_054_matches_hamming() {
let m = 9;
let gh = general_hamming(m, 0.54).unwrap();
let hm = hamming(m).unwrap();
for i in 0..m {
assert!(close(
gh.as_slice().unwrap()[i],
hm.as_slice().unwrap()[i],
1e-14,
));
}
}
#[test]
fn nuttall_endpoints_are_small() {
let w = nuttall(64).unwrap();
let s = w.as_slice().unwrap();
assert!(s[0].abs() < 1e-2);
assert!(s[s.len() - 1].abs() < 1e-2);
}
#[test]
fn nuttall_is_symmetric() {
let m = 33;
let w = nuttall(m).unwrap();
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(close(s[i], s[m - 1 - i], 1e-14));
}
}
#[test]
fn parzen_endpoints_are_zero() {
let w = parzen(16).unwrap();
let s = w.as_slice().unwrap();
assert!(s[0].abs() < 1e-2);
assert!(s[s.len() - 1].abs() < 1e-2);
}
#[test]
fn parzen_centre_is_one() {
let w = parzen(13).unwrap();
let s = w.as_slice().unwrap();
assert!(close(s[6], 1.0, 1e-14));
}
#[test]
fn parzen_is_symmetric() {
let m = 21;
let w = parzen(m).unwrap();
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(close(s[i], s[m - 1 - i], 1e-14));
}
}
#[test]
fn taylor_default_normalised_centre_is_one() {
let w = taylor(33, 4, 30.0, true).unwrap();
let s = w.as_slice().unwrap();
assert!(close(s[16], 1.0, 1e-12));
}
#[test]
fn taylor_is_symmetric() {
let m = 33;
let w = taylor(m, 4, 30.0, true).unwrap();
let s = w.as_slice().unwrap();
for i in 0..m / 2 {
assert!(close(s[i], s[m - 1 - i], 1e-12));
}
}
#[test]
fn taylor_rejects_nbar_zero() {
assert!(taylor(8, 0, 30.0, true).is_err());
assert!(taylor(8, 4, f64::NAN, true).is_err());
}
#[test]
fn tukey_alpha_zero_is_rectangular() {
let w = tukey(8, 0.0).unwrap();
for &v in w.as_slice().unwrap() {
assert!(close(v, 1.0, 1e-14));
}
}
#[test]
fn tukey_alpha_one_matches_hann() {
let m = 9;
let tk = tukey(m, 1.0).unwrap();
let hn = hanning(m).unwrap();
for i in 0..m {
assert!(close(
tk.as_slice().unwrap()[i],
hn.as_slice().unwrap()[i],
1e-12,
));
}
}
#[test]
fn tukey_centre_is_one() {
let m = 21;
let w = tukey(m, 0.5).unwrap();
assert!(close(w.as_slice().unwrap()[m / 2], 1.0, 1e-14));
}
#[test]
fn tukey_rejects_invalid_alpha() {
assert!(tukey(8, -0.1).is_err());
assert!(tukey(8, 1.1).is_err());
assert!(tukey(8, f64::NAN).is_err());
}
}
#[cfg(test)]
mod debug_test_removed {
}