use super::count_to_f64;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RunningMoments {
count: u64,
mean: f64,
m2: f64,
}
impl RunningMoments {
#[must_use]
pub const fn new() -> Self {
Self {
count: 0,
mean: 0.0,
m2: 0.0,
}
}
pub fn update(&mut self, x: f64) {
self.count += 1;
let n = count_to_f64(self.count);
let delta = x - self.mean;
self.mean += delta / n;
let delta2 = x - self.mean;
self.m2 = delta.mul_add(delta2, self.m2);
}
#[must_use]
pub const fn mean(&self) -> f64 {
self.mean
}
#[must_use]
pub fn variance(&self) -> f64 {
if self.count < 2 {
return 0.0;
}
self.m2 / count_to_f64(self.count - 1)
}
#[must_use]
pub fn std_dev(&self) -> f64 {
self.variance().sqrt()
}
#[must_use]
pub const fn count(&self) -> u64 {
self.count
}
}
impl Default for RunningMoments {
fn default() -> Self {
Self::new()
}
}
#[cfg(kani)]
mod verification {
use super::RunningMoments;
const MAX_ABS: f64 = 1e150;
fn any_bounded() -> f64 {
let x: f64 = kani::any();
kani::assume(x.is_finite());
kani::assume(x.abs() <= MAX_ABS);
x
}
#[kani::proof]
fn moments_variance_non_negative() {
let mut m = RunningMoments::new();
m.update(any_bounded());
m.update(any_bounded());
m.update(any_bounded());
let v = m.variance();
assert!(v >= 0.0, "variance was negative or NaN: {v}");
assert_eq!(m.count(), 3, "count diverged from the number of updates");
}
#[kani::proof]
fn moments_single_update_variance_zero() {
let mut m = RunningMoments::new();
m.update(any_bounded());
let v = m.variance();
assert!(
v == 0.0,
"single-sample variance must be exactly 0.0, was {v}"
);
}
}