use crate::Window;
#[derive(Clone, Copy, Debug)]
pub struct Anomaly<const N: usize> {
window: Window<N>,
sigmas: f32,
}
impl<const N: usize> Anomaly<N> {
pub fn new(sigmas: f32) -> Self {
Self {
window: Window::new(),
sigmas: if sigmas < 0.0 { -sigmas } else { sigmas },
}
}
pub fn check(&mut self, reading: f32) -> bool {
let anomalous = match self.window.mean() {
Some(mean) if self.window.len() >= 2 => {
let variance = self.window.variance().unwrap_or(0.0);
let deviation = reading - mean;
deviation * deviation > self.sigmas * self.sigmas * variance
}
_ => false,
};
self.window.push(reading);
anomalous
}
pub fn len(&self) -> usize {
self.window.len()
}
pub fn is_empty(&self) -> bool {
self.window.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_is_flagged_until_there_is_a_baseline() {
let mut watch = Anomaly::<5>::new(3.0);
assert!(!watch.check(100.0)); assert!(!watch.check(0.0)); assert_eq!(watch.len(), 2);
}
#[test]
fn flags_beyond_three_sigma_but_not_within() {
let baseline = [1.0, -1.0, 1.0, -1.0];
let mut inside = Anomaly::<4>::new(3.0);
for reading in baseline {
inside.check(reading);
}
assert!(!inside.check(2.9));
let mut outside = Anomaly::<4>::new(3.0);
for reading in baseline {
outside.check(reading);
}
assert!(outside.check(3.1)); }
#[test]
fn a_clear_outlier_is_flagged_after_a_noisy_baseline() {
let mut watch = Anomaly::<6>::new(3.0);
for reading in [50.0, 51.0, 49.0, 50.5, 49.5, 50.0] {
watch.check(reading);
}
assert!(!watch.check(50.5)); assert!(watch.check(80.0)); }
#[test]
fn any_change_from_a_flat_baseline_is_flagged() {
let mut watch = Anomaly::<4>::new(3.0);
watch.check(5.0);
watch.check(5.0); assert!(watch.check(6.0)); }
}