File size: 3,380 Bytes
e3e7558 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | //! Exponential Moving Average (EMA) for smooth tracking
//!
//! EMA smooths noisy measurements while maintaining responsiveness.
//! Used to track period and amplitude patterns in Marine algorithm.
#![cfg_attr(not(feature = "std"), no_std)]
/// Exponential Moving Average tracker
///
/// EMA formula: value = alpha * new + (1 - alpha) * old
/// - Higher alpha = faster response, more noise
/// - Lower alpha = slower response, smoother
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub struct Ema {
/// Smoothing factor (0..1)
alpha: f32,
/// Current smoothed value
value: f32,
/// Whether we've received at least one sample
initialized: bool,
}
impl Ema {
/// Create new EMA with given smoothing factor
///
/// # Arguments
/// * `alpha` - Smoothing factor (0..1). Higher = faster adaptation.
///
/// # Example
/// ```
/// use marine_salience::ema::Ema;
/// let mut ema = Ema::new(0.1); // 10% new, 90% old
/// ema.update(100.0);
/// assert_eq!(ema.get(), 100.0); // First value becomes baseline
/// ema.update(200.0);
/// assert!((ema.get() - 110.0).abs() < 0.01); // 0.1*200 + 0.9*100
/// ```
pub const fn new(alpha: f32) -> Self {
Self {
alpha,
value: 0.0,
initialized: false,
}
}
/// Update EMA with new measurement
pub fn update(&mut self, x: f32) {
if !self.initialized {
// First value becomes the baseline
self.value = x;
self.initialized = true;
} else {
// EMA update: new = alpha * x + (1 - alpha) * old
self.value = self.alpha * x + (1.0 - self.alpha) * self.value;
}
}
/// Get current smoothed value
pub fn get(&self) -> f32 {
self.value
}
/// Check if EMA has been initialized (received at least one sample)
pub fn is_ready(&self) -> bool {
self.initialized
}
/// Reset EMA to uninitialized state
pub fn reset(&mut self) {
self.value = 0.0;
self.initialized = false;
}
/// Get the smoothing factor
pub fn alpha(&self) -> f32 {
self.alpha
}
/// Set a new smoothing factor
pub fn set_alpha(&mut self, alpha: f32) {
self.alpha = alpha.clamp(0.0, 1.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_first_value_becomes_baseline() {
let mut ema = Ema::new(0.1);
assert!(!ema.is_ready());
ema.update(42.0);
assert!(ema.is_ready());
assert_eq!(ema.get(), 42.0);
}
#[test]
fn test_ema_smoothing() {
let mut ema = Ema::new(0.1);
ema.update(100.0);
ema.update(200.0);
// 0.1 * 200 + 0.9 * 100 = 20 + 90 = 110
assert!((ema.get() - 110.0).abs() < 0.001);
}
#[test]
fn test_high_alpha_fast_response() {
let mut ema = Ema::new(0.9);
ema.update(100.0);
ema.update(200.0);
// 0.9 * 200 + 0.1 * 100 = 180 + 10 = 190
assert!((ema.get() - 190.0).abs() < 0.001);
}
#[test]
fn test_reset() {
let mut ema = Ema::new(0.1);
ema.update(100.0);
assert!(ema.is_ready());
ema.reset();
assert!(!ema.is_ready());
assert_eq!(ema.get(), 0.0);
}
}
|