use crate::compute::simd_sum;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Smoother {
Exponential {
alpha: f64,
},
MovingAverage {
window: usize,
},
WeightedMovingAverage {
weights: Vec<f64>,
},
}
impl Smoother {
pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
match self {
Smoother::Exponential { alpha } => {
let safe_alpha = alpha.clamp(f64::MIN_POSITIVE, 1.0);
ExponentialSmoother::new(safe_alpha).smooth(values)
}
Smoother::MovingAverage { window } => {
let safe_window = (*window).max(1);
MovingAverageSmoother::new(safe_window).smooth(values)
}
Smoother::WeightedMovingAverage { weights } => {
if weights.is_empty() {
return values.to_vec();
}
WeightedMovingAverage::new(weights.clone()).smooth(values)
}
}
}
}
pub struct ExponentialSmoother {
alpha: f64,
}
impl ExponentialSmoother {
pub fn new(alpha: f64) -> Self {
assert!(
alpha > 0.0 && alpha <= 1.0,
"alpha must be in (0, 1], got {alpha}"
);
Self { alpha }
}
pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
if values.is_empty() {
return Vec::new();
}
let mut result = Vec::with_capacity(values.len());
result.push(values[0]);
let one_minus = 1.0 - self.alpha;
for i in 1..values.len() {
result.push(self.alpha * values[i] + one_minus * result[i - 1]);
}
result
}
}
pub struct MovingAverageSmoother {
window: usize,
}
impl MovingAverageSmoother {
pub fn new(window: usize) -> Self {
assert!(window > 0, "window must be > 0");
Self { window }
}
pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
if values.is_empty() {
return Vec::new();
}
let n = values.len();
let w = self.window.min(n);
let half_left = if w.is_multiple_of(2) {
w / 2 - 1
} else {
w / 2
};
let half_right = w / 2;
let mut result = Vec::with_capacity(n);
let mut prefix = Vec::with_capacity(n + 1);
prefix.push(0.0);
let mut acc = 0.0;
for &v in values {
acc += v;
prefix.push(acc);
}
for i in 0..n {
let lo = i.saturating_sub(half_left);
let hi = (i + half_right + 1).min(n);
let count = (hi - lo) as f64;
let sum = prefix[hi] - prefix[lo];
result.push(sum / count);
}
result
}
}
pub struct WeightedMovingAverage {
weights: Vec<f64>,
weight_sum: f64,
}
impl WeightedMovingAverage {
pub fn new(weights: Vec<f64>) -> Self {
assert!(!weights.is_empty(), "weights must not be empty");
let weight_sum = simd_sum(&weights);
Self {
weights,
weight_sum,
}
}
pub fn smooth(&self, values: &[f64]) -> Vec<f64> {
if values.is_empty() {
return Vec::new();
}
let n = values.len();
let w = self.weights.len();
let mut result = Vec::with_capacity(n);
for i in 0..n {
if i + 1 < w {
let available = i + 1;
let offset = w - available;
let mut sum = 0.0;
let mut wsum = 0.0;
for (j, &val) in values[..available].iter().enumerate() {
sum += val * self.weights[offset + j];
wsum += self.weights[offset + j];
}
result.push(if wsum.abs() > 1e-15 {
sum / wsum
} else {
values[i]
});
} else {
let mut sum = 0.0;
let start = i + 1 - w;
for j in 0..w {
sum += values[start + j] * self.weights[j];
}
result.push(if self.weight_sum.abs() > 1e-15 {
sum / self.weight_sum
} else {
values[i]
});
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compute::simd_mean;
#[test]
fn exponential_smoother_alpha_1() {
let s = ExponentialSmoother::new(1.0);
let vals = vec![1.0, 5.0, 3.0, 7.0];
let result = s.smooth(&vals);
assert_eq!(result, vals); }
#[test]
fn exponential_smoother_smooth() {
let s = ExponentialSmoother::new(0.5);
let vals = vec![10.0, 20.0, 30.0, 40.0];
let result = s.smooth(&vals);
assert!((result[0] - 10.0).abs() < 1e-10);
assert!((result[1] - 15.0).abs() < 1e-10); assert!((result[2] - 22.5).abs() < 1e-10); assert!((result[3] - 31.25).abs() < 1e-10); }
#[test]
fn moving_average_basic() {
let s = MovingAverageSmoother::new(3);
let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = s.smooth(&vals);
assert_eq!(result.len(), 5);
assert!((result[1] - 2.0).abs() < 1e-10);
assert!((result[2] - 3.0).abs() < 1e-10);
}
#[test]
fn moving_average_window_larger_than_data() {
let s = MovingAverageSmoother::new(100);
let vals = vec![1.0, 2.0, 3.0];
let result = s.smooth(&vals);
assert_eq!(result.len(), 3);
let mean = simd_mean(&vals);
assert!((result[1] - mean).abs() < 1e-10);
}
#[test]
fn weighted_moving_average() {
let s = WeightedMovingAverage::new(vec![1.0, 2.0, 3.0]);
let vals = vec![10.0, 20.0, 30.0, 40.0];
let result = s.smooth(&vals);
assert_eq!(result.len(), 4);
assert!((result[2] - 140.0 / 6.0).abs() < 1e-10);
}
#[test]
fn empty_data() {
assert!(ExponentialSmoother::new(0.5).smooth(&[]).is_empty());
assert!(MovingAverageSmoother::new(3).smooth(&[]).is_empty());
assert!(WeightedMovingAverage::new(vec![1.0]).smooth(&[]).is_empty());
}
#[test]
fn smoother_enum_dispatch() {
let vals = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let s = Smoother::Exponential { alpha: 0.5 };
let r = s.smooth(&vals);
assert_eq!(r.len(), 5);
}
}