#![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)
}
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 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::*;
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());
}
}