use core::sync::atomic::Ordering;
use portable_atomic::AtomicU64;
#[derive(Debug)]
pub struct SignalStats {
count: AtomicU64,
last_bits: AtomicU64,
min_bits: AtomicU64,
max_bits: AtomicU64,
sum_bits: AtomicU64,
}
impl Default for SignalStats {
fn default() -> Self {
Self {
count: AtomicU64::new(0),
last_bits: AtomicU64::new(0),
min_bits: AtomicU64::new(f64::INFINITY.to_bits()),
max_bits: AtomicU64::new(f64::NEG_INFINITY.to_bits()),
sum_bits: AtomicU64::new(0),
}
}
}
impl SignalStats {
pub fn new() -> Self {
Self::default()
}
pub fn update(&self, value: f64) {
self.last_bits.store(value.to_bits(), Ordering::Relaxed);
self.count.fetch_add(1, Ordering::Relaxed);
Self::accumulate(&self.sum_bits, |cur| cur + value);
Self::keep_if(&self.min_bits, value, |value, cur| value < cur);
Self::keep_if(&self.max_bits, value, |value, cur| value > cur);
}
fn accumulate(cell: &AtomicU64, f: impl Fn(f64) -> f64) {
let mut cur = cell.load(Ordering::Relaxed);
loop {
let next = f(f64::from_bits(cur)).to_bits();
match cell.compare_exchange_weak(cur, next, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
fn keep_if(cell: &AtomicU64, value: f64, keep: impl Fn(f64, f64) -> bool) {
let mut cur = cell.load(Ordering::Relaxed);
while keep(value, f64::from_bits(cur)) {
match cell.compare_exchange_weak(
cur,
value.to_bits(),
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => cur = actual,
}
}
}
pub fn count(&self) -> u64 {
self.count.load(Ordering::Relaxed)
}
pub fn last(&self) -> f64 {
f64::from_bits(self.last_bits.load(Ordering::Relaxed))
}
pub fn min(&self) -> f64 {
if self.count() == 0 {
0.0
} else {
f64::from_bits(self.min_bits.load(Ordering::Relaxed))
}
}
pub fn max(&self) -> f64 {
if self.count() == 0 {
0.0
} else {
f64::from_bits(self.max_bits.load(Ordering::Relaxed))
}
}
pub fn mean(&self) -> f64 {
let count = self.count();
if count == 0 {
0.0
} else {
f64::from_bits(self.sum_bits.load(Ordering::Relaxed)) / count as f64
}
}
pub fn reset(&self) {
self.count.store(0, Ordering::Relaxed);
self.last_bits.store(0, Ordering::Relaxed);
self.min_bits
.store(f64::INFINITY.to_bits(), Ordering::Relaxed);
self.max_bits
.store(f64::NEG_INFINITY.to_bits(), Ordering::Relaxed);
self.sum_bits.store(0, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_is_zero() {
let s = SignalStats::new();
assert_eq!(s.count(), 0);
assert_eq!(s.last(), 0.0);
assert_eq!(s.min(), 0.0);
assert_eq!(s.max(), 0.0);
assert_eq!(s.mean(), 0.0);
}
#[test]
fn tracks_last_min_max_mean() {
let s = SignalStats::new();
for v in [10.0, 30.0, 20.0] {
s.update(v);
}
assert_eq!(s.count(), 3);
assert_eq!(s.last(), 20.0);
assert_eq!(s.min(), 10.0);
assert_eq!(s.max(), 30.0);
assert_eq!(s.mean(), 20.0);
}
#[test]
fn handles_negative_values() {
let s = SignalStats::new();
for v in [-5.0, -20.0, -1.0] {
s.update(v);
}
assert_eq!(s.min(), -20.0);
assert_eq!(s.max(), -1.0);
assert_eq!(s.last(), -1.0);
}
#[test]
fn reset_clears_then_reusable() {
let s = SignalStats::new();
s.update(5.0);
s.update(7.0);
s.reset();
assert_eq!(s.count(), 0);
assert_eq!(s.min(), 0.0);
s.update(3.0);
assert_eq!(s.count(), 1);
assert_eq!(s.min(), 3.0);
assert_eq!(s.max(), 3.0);
}
}