#[derive(Debug, Clone)]
pub struct HalfLifeF64 {
n: u64,
mean_x: f64, mean_prev: f64, m2_x: f64, co_moment: f64, prev: f64,
count: u64, }
impl Default for HalfLifeF64 {
fn default() -> Self {
Self::new()
}
}
impl HalfLifeF64 {
#[inline]
#[must_use]
pub const fn new() -> Self {
Self {
n: 0,
mean_x: 0.0,
mean_prev: 0.0,
m2_x: 0.0,
co_moment: 0.0,
prev: 0.0,
count: 0,
}
}
#[inline]
#[must_use]
pub fn builder() -> HalfLifeF64Builder {
HalfLifeF64Builder { _private: () }
}
#[inline]
pub fn update(&mut self, value: f64) -> Result<(), crate::DataError> {
check_finite!(value);
self.count += 1;
if self.count >= 2 {
self.n += 1;
let n = self.n as f64;
let dx = value - self.mean_x;
let dy = self.prev - self.mean_prev;
self.mean_x += dx / n;
self.mean_prev += dy / n;
let dx2 = value - self.mean_x;
self.co_moment += dx * (self.prev - self.mean_prev);
self.m2_x += dx * dx2;
}
self.prev = value;
Ok(())
}
#[inline]
#[must_use]
pub fn autocorrelation(&self) -> Option<f64> {
if self.n < 2 {
return None;
}
if self.m2_x <= 0.0 {
return None;
}
Some(self.co_moment / self.m2_x)
}
#[inline]
#[must_use]
pub fn half_life(&self) -> Option<f64> {
let ac1 = self.autocorrelation()?;
if ac1 <= 0.0 || ac1 >= 1.0 {
return None;
}
Some(-core::f64::consts::LN_2 / crate::math::ln(ac1))
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.n >= 2
}
pub fn reset(&mut self) {
self.n = 0;
self.mean_x = 0.0;
self.mean_prev = 0.0;
self.m2_x = 0.0;
self.co_moment = 0.0;
self.prev = 0.0;
self.count = 0;
}
}
#[derive(Debug, Clone)]
pub struct HalfLifeF64Builder {
_private: (),
}
impl HalfLifeF64Builder {
pub fn build(self) -> HalfLifeF64 {
HalfLifeF64::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn xorshift64(state: &mut u64) -> f64 {
let mut s = *state;
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
*state = s;
(s as f64 / u64::MAX as f64).mul_add(2.0, -1.0)
}
#[test]
fn ar1_process_half_life() {
let mut hl = HalfLifeF64::new();
let phi = 0.9;
let mut x = 0.0;
let mut rng = 12345u64;
for _ in 0..20_000 {
let eps = xorshift64(&mut rng) * 0.1;
x = phi * x + eps;
hl.update(x).unwrap();
}
let ac = hl.autocorrelation().unwrap();
assert!(ac > 0.8 && ac < 0.95, "AC(1) should be ~0.9, got {ac}");
let h = hl.half_life().unwrap();
assert!(h > 4.0 && h < 12.0, "half-life should be ~6.58, got {h}");
}
#[test]
fn not_primed_before_3() {
let mut hl = HalfLifeF64::new();
hl.update(1.0).unwrap();
hl.update(2.0).unwrap();
assert!(!hl.is_primed());
assert!(hl.half_life().is_none());
}
#[test]
fn primed_at_3() {
let mut hl = HalfLifeF64::new();
hl.update(1.0).unwrap();
hl.update(2.0).unwrap();
hl.update(3.0).unwrap();
assert!(hl.is_primed());
}
#[test]
fn reset_clears_state() {
let mut hl = HalfLifeF64::new();
for i in 0..100 {
hl.update(i as f64).unwrap();
}
hl.reset();
assert_eq!(hl.count(), 0);
assert!(!hl.is_primed());
}
#[test]
fn nan_rejected() {
let mut hl = HalfLifeF64::new();
assert!(hl.update(f64::NAN).is_err());
}
#[test]
fn inf_rejected() {
let mut hl = HalfLifeF64::new();
assert!(hl.update(f64::INFINITY).is_err());
}
#[test]
fn constant_series_no_half_life() {
let mut hl = HalfLifeF64::new();
for _ in 0..100 {
hl.update(5.0).unwrap();
}
assert!(hl.half_life().is_none());
}
#[test]
fn alternating_series_no_half_life() {
let mut hl = HalfLifeF64::new();
for i in 0..1000 {
let val = if i % 2 == 0 { 10.0 } else { -10.0 };
hl.update(val).unwrap();
}
let ac = hl.autocorrelation().unwrap();
assert!(ac < 0.0, "alternating should have negative AC(1), got {ac}");
assert!(hl.half_life().is_none());
}
}