use crate::Condition;
use crate::math::MulAdd;
#[derive(Debug, Clone)]
pub struct ErrorRateF64 {
alpha: f64,
one_minus_alpha: f64,
value: f64,
threshold: f64,
count: u64,
min_samples: u64,
}
#[derive(Debug, Clone)]
pub struct ErrorRateF64Builder {
alpha: Option<f64>,
threshold: Option<f64>,
min_samples: u64,
}
impl ErrorRateF64 {
#[inline]
#[must_use]
pub fn builder() -> ErrorRateF64Builder {
ErrorRateF64Builder {
alpha: None,
threshold: None,
min_samples: 1,
}
}
#[inline]
#[must_use]
pub fn update(&mut self, success: bool) -> Option<Condition> {
self.update_weighted(success, 1.0).unwrap()
}
#[inline]
pub fn update_weighted(
&mut self,
success: bool,
weight: f64,
) -> Result<Option<Condition>, crate::DataError> {
check_finite!(weight);
self.count += 1;
let sample = if success { 0.0 } else { weight };
if self.count == 1 {
self.value = sample;
} else {
self.value = self.alpha.fma(sample, self.one_minus_alpha * self.value);
}
if self.count < self.min_samples {
return Ok(None);
}
Ok(if self.value > self.threshold {
Some(Condition::Degraded)
} else {
Some(Condition::Normal)
})
}
#[inline]
#[must_use]
pub fn error_rate(&self) -> Option<f64> {
if self.count >= self.min_samples {
Some(self.value)
} else {
None
}
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.count >= self.min_samples
}
#[inline]
pub fn reset(&mut self) {
self.value = 0.0;
self.count = 0;
}
#[inline]
pub fn reconfigure_threshold(&mut self, threshold: f64) -> Result<(), crate::ConfigError> {
if threshold < 0.0 {
return Err(crate::ConfigError::Invalid(
"threshold must be non-negative",
));
}
self.threshold = threshold;
Ok(())
}
}
impl ErrorRateF64Builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
self
}
#[inline]
#[must_use]
#[cfg(any(feature = "std", feature = "libm"))]
pub fn halflife(mut self, halflife: f64) -> Self {
let ln2 = core::f64::consts::LN_2;
self.alpha = Some(1.0 - crate::math::exp(-ln2 / halflife));
self
}
#[inline]
#[must_use]
pub fn span(mut self, n: u64) -> Self {
self.alpha = Some(2.0 / (n as f64 + 1.0));
self
}
#[inline]
#[must_use]
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = Some(threshold);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
pub fn build(self) -> Result<ErrorRateF64, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
let threshold = self
.threshold
.ok_or(crate::ConfigError::Missing("threshold"))?;
if !(alpha > 0.0 && alpha < 1.0) {
return Err(crate::ConfigError::Invalid("alpha must be in (0, 1)"));
}
if threshold < 0.0 {
return Err(crate::ConfigError::Invalid(
"threshold must be non-negative",
));
}
Ok(ErrorRateF64 {
alpha,
one_minus_alpha: 1.0 - alpha,
value: 0.0,
threshold,
count: 0,
min_samples: self.min_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_success_is_healthy() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.05)
.build()
.unwrap();
for _ in 0..100 {
assert_eq!(er.update(true), Some(Condition::Normal));
}
}
#[test]
fn all_failure_is_degraded() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.05)
.build()
.unwrap();
for _ in 0..50 {
let _ = er.update(false);
}
assert_eq!(er.update(false), Some(Condition::Degraded));
}
#[test]
fn threshold_crossing() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.5)
.build()
.unwrap();
for _ in 0..20 {
let _ = er.update(true);
}
assert_eq!(er.update(true), Some(Condition::Normal));
for _ in 0..50 {
let _ = er.update(false);
}
assert_eq!(er.update(false), Some(Condition::Degraded));
}
#[test]
fn weighted_failure_triggers_faster() {
let mut light = ErrorRateF64::builder()
.alpha(0.5)
.threshold(0.5)
.build()
.unwrap();
let mut heavy = ErrorRateF64::builder()
.alpha(0.5)
.threshold(0.5)
.build()
.unwrap();
for _ in 0..10 {
let _ = light.update(true);
let _ = heavy.update(true);
}
let _ = light.update_weighted(false, 1.0).unwrap();
let _ = heavy.update_weighted(false, 5.0).unwrap();
let light_rate = light.error_rate().unwrap();
let heavy_rate = heavy.error_rate().unwrap();
assert!(
heavy_rate > light_rate,
"heavy ({heavy_rate}) should exceed light ({light_rate})"
);
}
#[test]
fn priming() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.05)
.min_samples(5)
.build()
.unwrap();
for _ in 0..4 {
assert!(er.update(false).is_none());
}
assert!(er.update(false).is_some());
}
#[test]
fn reset() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.05)
.build()
.unwrap();
for _ in 0..10 {
let _ = er.update(false);
}
er.reset();
assert_eq!(er.count(), 0);
assert!(er.error_rate().is_none());
}
#[test]
fn reconfigure_threshold_changes_behavior() {
let mut er = ErrorRateF64::builder()
.alpha(0.1)
.threshold(0.5)
.build()
.unwrap();
for _ in 0..50 {
let _ = er.update(true);
}
let rate = er.error_rate().unwrap();
assert!(
rate < 0.5,
"rate should be low after all successes, got {rate}"
);
assert_eq!(er.update(true), Some(Condition::Normal));
er.reconfigure_threshold(0.0).unwrap();
assert_eq!(er.update(false), Some(Condition::Degraded));
}
#[test]
fn errors_without_threshold() {
let result = ErrorRateF64::builder().alpha(0.3).build();
assert!(matches!(
result,
Err(crate::ConfigError::Missing("threshold"))
));
}
#[test]
fn allows_zero_threshold() {
let er = ErrorRateF64::builder().alpha(0.3).threshold(0.0).build();
assert!(er.is_ok());
}
#[test]
fn rejects_negative_threshold() {
let result = ErrorRateF64::builder().alpha(0.3).threshold(-0.1).build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
}
#[test]
fn reconfigure_rejects_negative_threshold() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.5)
.build()
.unwrap();
assert!(er.reconfigure_threshold(-0.1).is_err());
assert!(er.reconfigure_threshold(0.0).is_ok());
}
#[test]
fn rejects_nan_and_inf() {
let mut er = ErrorRateF64::builder()
.alpha(0.3)
.threshold(0.5)
.build()
.unwrap();
assert_eq!(
er.update_weighted(false, f64::NAN).unwrap_err(),
crate::DataError::NotANumber
);
assert_eq!(
er.update_weighted(false, f64::INFINITY).unwrap_err(),
crate::DataError::Infinite
);
assert_eq!(
er.update_weighted(false, f64::NEG_INFINITY).unwrap_err(),
crate::DataError::Infinite
);
assert_eq!(er.count(), 0);
}
}