#[derive(Clone, Copy, Debug)]
pub struct MomentAccumulator {
count: u64,
min: f32,
max: f32,
mean: f64,
m2: f64,
}
impl Default for MomentAccumulator {
fn default() -> Self {
Self {
count: 0,
min: f32::INFINITY,
max: f32::NEG_INFINITY,
mean: 0.0,
m2: 0.0,
}
}
}
impl MomentAccumulator {
pub fn push(&mut self, v: f32) -> bool {
if !v.is_finite() {
return false;
}
self.count += 1;
self.min = self.min.min(v);
self.max = self.max.max(v);
let x = v as f64;
let delta = x - self.mean;
self.mean += delta / self.count as f64;
self.m2 += delta * (x - self.mean);
true
}
pub fn count(&self) -> u64 {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn min(&self) -> f32 {
self.min
}
pub fn max(&self) -> f32 {
self.max
}
pub fn mean(&self) -> f64 {
self.mean
}
pub fn variance(&self) -> f64 {
if self.count == 0 {
0.0
} else {
self.m2.max(0.0) / self.count as f64
}
}
pub fn std(&self) -> f64 {
self.variance().sqrt()
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::MomentAccumulator;
#[test]
fn empty_accumulator_reports_zeroed_moments() {
let acc = MomentAccumulator::default();
assert!(acc.is_empty());
assert_eq!(acc.count(), 0);
assert_eq!(acc.mean(), 0.0);
assert_eq!(acc.variance(), 0.0);
assert_eq!(acc.std(), 0.0);
assert_eq!(acc.min(), f32::INFINITY);
assert_eq!(acc.max(), f32::NEG_INFINITY);
}
#[test]
fn non_finite_is_rejected_without_folding() {
let mut acc = MomentAccumulator::default();
assert!(acc.push(1.0));
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert!(!acc.push(bad), "{bad} should be rejected");
}
assert_eq!(acc.count(), 1);
assert_eq!(acc.mean(), 1.0);
}
#[test]
fn known_values_match_population_moments() {
let mut acc = MomentAccumulator::default();
for v in [2.0f32, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
assert!(acc.push(v));
}
assert_eq!(acc.count(), 8);
assert_eq!(acc.min(), 2.0);
assert_eq!(acc.max(), 9.0);
assert!((acc.mean() - 5.0).abs() < 1e-12);
assert!((acc.variance() - 4.0).abs() < 1e-12);
assert!((acc.std() - 2.0).abs() < 1e-12);
}
proptest! {
#[test]
fn moment_invariants(values in prop::collection::vec(-1.0e6f32..1.0e6f32, 1..64)) {
let mut acc = MomentAccumulator::default();
for &v in &values {
prop_assert!(acc.push(v));
}
prop_assert_eq!(acc.count() as usize, values.len());
prop_assert!(acc.min() <= acc.max());
prop_assert!(acc.mean() >= acc.min() as f64 - 1e-6);
prop_assert!(acc.mean() <= acc.max() as f64 + 1e-6);
prop_assert!(acc.variance() >= 0.0);
prop_assert!(acc.std() >= 0.0);
}
}
}