use std::f64::consts::PI;
pub fn general_cosine(n: usize, coeffs: &[f64], periodic: bool) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![1.0];
}
let m = if periodic { n + 1 } else { n };
let mut w: Vec<f64> = (0..m)
.map(|k| {
let frac = (k as f64) / ((m - 1) as f64);
coeffs
.iter()
.enumerate()
.map(|(i, &c)| {
let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
sign * c * (2.0 * PI * (i as f64) * frac).cos()
})
.sum()
})
.collect();
w.truncate(n);
w
}
pub fn hann(n: usize, periodic: bool) -> Vec<f64> {
general_cosine(n, &[0.5, 0.5], periodic)
}
pub fn hamming(n: usize, periodic: bool) -> Vec<f64> {
general_cosine(n, &[0.54, 0.46], periodic)
}
pub fn blackman(n: usize, periodic: bool) -> Vec<f64> {
general_cosine(n, &[0.42, 0.50, 0.08], periodic)
}
pub fn blackmanharris(n: usize, periodic: bool) -> Vec<f64> {
general_cosine(n, &[0.35875, 0.48829, 0.14128, 0.01168], periodic)
}
pub fn bartlett(n: usize, periodic: bool) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![1.0];
}
let m = if periodic { n + 1 } else { n };
let half = (m - 1) as f64 / 2.0;
let mut w: Vec<f64> = (0..m)
.map(|k| 1.0 - ((k as f64 - half) / half).abs())
.collect();
w.truncate(n);
w
}
pub fn triangular(n: usize, periodic: bool) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![1.0];
}
let m = if periodic { n + 1 } else { n };
let mut w: Vec<f64> = if m % 2 == 1 {
let half = (m + 1) as f64 / 2.0;
(0..m)
.map(|k| 1.0 - ((k as f64 + 1.0 - half) / half).abs())
.collect()
} else {
let half = m as f64 / 2.0;
(0..m)
.map(|k| 1.0 - ((k as f64 + 0.5 - half) / half).abs())
.collect()
};
w.truncate(n);
w
}
pub fn boxcar(n: usize) -> Vec<f64> {
vec![1.0; n]
}
fn i0(x: f64) -> f64 {
let half_x = x.abs() * 0.5;
let mut sum = 1.0;
let mut term = 1.0;
for k in 1..=30 {
term *= half_x / (k as f64);
let t2 = term * term;
sum += t2;
if t2 < sum * f64::EPSILON {
break;
}
}
sum
}
pub fn kaiser(n: usize, beta: f64, periodic: bool) -> Vec<f64> {
if n == 0 {
return Vec::new();
}
if n == 1 {
return vec![1.0];
}
let m = if periodic { n + 1 } else { n };
let alpha = ((m - 1) as f64) / 2.0;
let denom = i0(beta);
let mut w: Vec<f64> = (0..m)
.map(|k| {
let ratio = (k as f64 - alpha) / alpha;
let arg = beta * (1.0 - ratio * ratio).max(0.0).sqrt();
i0(arg) / denom
})
.collect();
w.truncate(n);
w
}
pub fn get_window(name: &str, n: usize, periodic: bool) -> Result<Vec<f64>, String> {
let name_lower = name.to_lowercase();
if let Some(beta_str) = name_lower.strip_prefix("kaiser:") {
let beta: f64 = beta_str
.parse()
.map_err(|_| format!("Invalid kaiser beta: {}", beta_str))?;
return Ok(kaiser(n, beta, periodic));
}
match name_lower.as_str() {
"hann" | "hanning" => Ok(hann(n, periodic)),
"hamming" => Ok(hamming(n, periodic)),
"blackman" => Ok(blackman(n, periodic)),
"blackmanharris" => Ok(blackmanharris(n, periodic)),
"bartlett" => Ok(bartlett(n, periodic)),
"triangular" | "triang" => Ok(triangular(n, periodic)),
"boxcar" | "rectangular" | "ones" => Ok(boxcar(n)),
_ => Err(format!("Unknown window: {}", name)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[derive(Deserialize)]
struct WindowTestCase {
name: String,
function: String,
n: usize,
periodic: bool,
#[serde(default)]
beta: Option<f64>,
expected: Vec<f64>,
}
fn load_window_references() -> Vec<WindowTestCase> {
let data = include_str!("../tests/references/windows.json");
serde_json::from_str(data).expect("Failed to parse windows.json")
}
fn assert_approx_eq(actual: &[f64], expected: &[f64], tol: f64, name: &str) {
assert_eq!(
actual.len(),
expected.len(),
"{name}: length mismatch: got {} expected {}",
actual.len(),
expected.len()
);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
let diff = (a - e).abs();
assert!(
diff < tol,
"{name}[{i}]: got {a}, expected {e}, diff {diff} > tol {tol}"
);
}
}
#[test]
fn test_hann_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "hann") {
let result = hann(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_hamming_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "hamming") {
let result = hamming(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_blackman_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "blackman") {
let result = blackman(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_blackmanharris_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "blackmanharris") {
let result = blackmanharris(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_bartlett_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "bartlett") {
let result = bartlett(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_triangular_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "triangular") {
let result = triangular(case.n, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_boxcar_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "boxcar") {
let result = boxcar(case.n);
assert_approx_eq(&result, &case.expected, 1e-12, &case.name);
}
}
#[test]
fn test_kaiser_matches_scipy() {
let refs = load_window_references();
for case in refs.iter().filter(|c| c.function == "kaiser") {
let beta = case.beta.unwrap_or(5.0);
let result = kaiser(case.n, beta, case.periodic);
assert_approx_eq(&result, &case.expected, 1e-10, &case.name);
}
}
#[test]
fn test_n1_returns_one() {
assert_eq!(hann(1, true), vec![1.0]);
assert_eq!(hamming(1, true), vec![1.0]);
assert_eq!(blackman(1, true), vec![1.0]);
assert_eq!(kaiser(1, 5.0, true), vec![1.0]);
assert_eq!(bartlett(1, true), vec![1.0]);
assert_eq!(triangular(1, true), vec![1.0]);
}
#[test]
fn test_get_window_dispatches_correctly() {
assert_eq!(get_window("hann", 64, true).unwrap(), hann(64, true));
assert_eq!(get_window("Hanning", 64, true).unwrap(), hann(64, true));
assert_eq!(get_window("HAMMING", 64, true).unwrap(), hamming(64, true));
assert_eq!(
get_window("blackman", 64, false).unwrap(),
blackman(64, false)
);
assert_eq!(
get_window("blackmanharris", 64, true).unwrap(),
blackmanharris(64, true)
);
assert_eq!(
get_window("bartlett", 64, true).unwrap(),
bartlett(64, true)
);
assert_eq!(
get_window("triang", 64, true).unwrap(),
triangular(64, true)
);
assert_eq!(
get_window("triangular", 64, true).unwrap(),
triangular(64, true)
);
assert_eq!(get_window("boxcar", 64, true).unwrap(), boxcar(64));
assert_eq!(get_window("rectangular", 64, true).unwrap(), boxcar(64));
assert_eq!(get_window("ones", 64, true).unwrap(), boxcar(64));
assert_eq!(
get_window("kaiser:5.0", 64, true).unwrap(),
kaiser(64, 5.0, true)
);
}
#[test]
fn test_get_window_unknown() {
assert!(get_window("unknown", 64, true).is_err());
}
#[test]
fn test_get_window_kaiser_invalid_beta() {
assert!(get_window("kaiser:abc", 64, true).is_err());
}
#[test]
fn test_n0_returns_empty() {
assert!(hann(0, true).is_empty());
assert!(hamming(0, true).is_empty());
assert!(kaiser(0, 5.0, true).is_empty());
assert!(bartlett(0, true).is_empty());
assert!(boxcar(0).is_empty());
}
}