helena 0.1.0

Core types and component interfaces for helena, a latent data-to-waveform generation platform.
Documentation
//! Streaming summary statistics shared across the boundary.
//!
//! [`MomentAccumulator`] is the single-pass Welford core that the downstream
//! data-profiling (`helena-data`) and run-inspection (`helena-train`) reports both
//! fold their scalar samples through. Each crate keeps its own serde report struct
//! (they differ: a profiling summary carries a count, inspection moments carry an
//! L2 norm); this owns only the numeric core they share, so the two cannot drift
//! in how a mean, variance, or extent is computed.

/// Single-pass Welford accumulator over finite `f32` samples.
///
/// Folds count, extent (min/max), running mean, and the sum of squared
/// deviations (`m2`) in one pass, so population variance and standard deviation
/// are available without a second pass or retaining the samples. Memory is
/// constant in the sample count.
///
/// Non-finite inputs are rejected: [`push`](Self::push) returns `false` without
/// folding, leaving the caller to account for them (a profiler counts them as
/// out-of-summary; an inspector tallies them alongside the finite moments).
#[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 {
    /// Fold one sample. Returns `false` without folding if `v` is non-finite.
    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
    }

    /// Finite samples folded.
    pub fn count(&self) -> u64 {
        self.count
    }

    /// Whether no sample has been folded.
    pub fn is_empty(&self) -> bool {
        self.count == 0
    }

    /// Smallest folded sample, or [`f32::INFINITY`] if none.
    pub fn min(&self) -> f32 {
        self.min
    }

    /// Largest folded sample, or [`f32::NEG_INFINITY`] if none.
    pub fn max(&self) -> f32 {
        self.max
    }

    /// Running mean, `0.0` if no samples.
    pub fn mean(&self) -> f64 {
        self.mean
    }

    /// Population variance, `0.0` if no samples.
    pub fn variance(&self) -> f64 {
        if self.count == 0 {
            0.0
        } else {
            self.m2.max(0.0) / self.count as f64
        }
    }

    /// Population standard deviation, `0.0` if no samples.
    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);
        // Population variance of the canonical sample is exactly 4.
        assert!((acc.variance() - 4.0).abs() < 1e-12);
        assert!((acc.std() - 2.0).abs() < 1e-12);
    }

    proptest! {
        /// Moments stay within their defining bounds for any finite sample set.
        #[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);
        }
    }
}