use ndarray::Array1;
pub struct CosineFilter {
combined_weights: Vec<f64>,
n_steps: usize,
}
impl CosineFilter {
pub fn new(omega: f64, dt: f64, n_steps: usize) -> Self {
let mut combined_weights = Vec::with_capacity(n_steps + 1);
let scale = 2.0 / n_steps as f64;
for n in 0..=n_steps {
let trap_weight = if n == 0 || n == n_steps { 0.5 } else { 1.0 };
let cos_val = (omega * n as f64 * dt).cos();
combined_weights.push(scale * cos_val * trap_weight);
}
Self {
combined_weights,
n_steps,
}
}
pub fn new_with_dispersion_correction(
omega: f64,
dt: f64,
n_steps: usize,
eigenvalue_range: (f64, f64),
) -> Self {
let mut combined_weights = Vec::with_capacity(n_steps + 1);
let omega_sq = omega * omega;
let dt_sq = dt * dt;
let lambda_mid = 0.5 * (eigenvalue_range.0 + eigenvalue_range.1);
let lambda_eff = lambda_mid.max(omega_sq * 0.1);
let numerator = 2.0 - dt_sq * lambda_eff;
let denominator = 2.0 + dt_sq * lambda_eff;
let cos_omega_d_dt = (numerator / denominator).clamp(-1.0, 1.0);
let omega_d_dt = cos_omega_d_dt.acos();
let scale = 2.0 / n_steps as f64;
for n in 0..=n_steps {
let trap_weight = if n == 0 || n == n_steps { 0.5 } else { 1.0 };
let cos_val = (omega_d_dt * n as f64).cos();
combined_weights.push(scale * cos_val * trap_weight);
}
Self {
combined_weights,
n_steps,
}
}
#[inline]
pub fn weight(&self, n: usize) -> f64 {
self.combined_weights[n]
}
pub fn n_steps(&self) -> usize {
self.n_steps
}
pub fn accumulate(&self, n: usize, w_n: &Array1<f64>, accumulator: &mut Array1<f64>) {
let w = self.combined_weights[n];
if w.abs() > 1e-20 {
accumulator.scaled_add(w, w_n);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_weights_sum() {
let omega = 2.0 * std::f64::consts::PI; let n_steps = 20;
let dt = 1.0 / n_steps as f64;
let filter = CosineFilter::new(omega, dt, n_steps);
let mut sum = 0.0;
for n in 0..=n_steps {
let t = n as f64 * dt;
sum += filter.weight(n) * (omega * t).cos();
}
assert!(
(sum - 1.0).abs() < 0.02,
"Filter self-correlation should be ~1, got {}",
sum
);
}
#[test]
fn test_filter_extracts_single_frequency() {
let omega = 2.0 * std::f64::consts::PI;
let n_steps = 40;
let dt = 1.0 / n_steps as f64;
let filter = CosineFilter::new(omega, dt, n_steps);
let mut result_target = 0.0;
let mut result_harmonic = 0.0;
for n in 0..=n_steps {
let t = n as f64 * dt;
result_target += filter.weight(n) * (omega * t).cos();
result_harmonic += filter.weight(n) * (3.0 * omega * t).cos();
}
assert!(
(result_target - 1.0).abs() < 0.02,
"Should extract target frequency: got {}",
result_target
);
assert!(
result_harmonic.abs() < 0.05,
"Should reject harmonic: got {}",
result_harmonic
);
}
#[test]
fn test_accumulate() {
let omega = 2.0 * std::f64::consts::PI;
let n_steps = 10;
let dt = 1.0 / n_steps as f64;
let filter = CosineFilter::new(omega, dt, n_steps);
let ndofs = 3;
let mut acc = Array1::zeros(ndofs);
let w = Array1::from_vec(vec![1.0, 2.0, 3.0]);
filter.accumulate(0, &w, &mut acc);
assert!((acc[0] - 0.1).abs() < 1e-10);
assert!((acc[1] - 0.2).abs() < 1e-10);
}
#[test]
fn test_dispersion_corrected_filter() {
let omega = 2.0 * std::f64::consts::PI;
let n_steps = 20;
let dt = 1.0 / n_steps as f64;
let standard = CosineFilter::new(omega, dt, n_steps);
let corrected =
CosineFilter::new_with_dispersion_correction(omega, dt, n_steps, (1.0, 100.0));
let mut diff_sum = 0.0;
for n in 0..=n_steps {
diff_sum += (standard.weight(n) - corrected.weight(n)).abs();
}
assert!(
diff_sum > 1e-6,
"Dispersion correction should modify weights"
);
}
}