use crate::Condition;
use crate::math::MulAdd;
macro_rules! impl_saturation {
($name:ident, $builder:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
alpha: $ty,
one_minus_alpha: $ty,
value: $ty,
threshold: $ty,
count: u64,
min_samples: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
alpha: Option<$ty>,
threshold: Option<$ty>,
min_samples: u64,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
alpha: Option::None,
threshold: Option::None,
min_samples: 1,
}
}
#[inline]
pub fn update(
&mut self,
utilization: $ty,
) -> Result<Option<Condition>, crate::DataError> {
check_finite!(utilization);
self.count += 1;
if self.count == 1 {
self.value = utilization;
} else {
self.value = self
.alpha
.fma(utilization, self.one_minus_alpha * self.value);
}
if self.count < self.min_samples {
return Ok(Option::None);
}
Ok(if self.value > self.threshold {
Option::Some(Condition::Degraded)
} else {
Option::Some(Condition::Normal)
})
}
#[inline]
#[must_use]
pub fn utilization(&self) -> Option<$ty> {
if self.count >= self.min_samples {
Option::Some(self.value)
} else {
Option::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 as $ty;
self.count = 0;
}
#[inline]
pub fn reconfigure_threshold(&mut self, threshold: $ty) {
self.threshold = threshold;
}
}
impl $builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: $ty) -> Self {
self.alpha = Option::Some(alpha);
self
}
#[inline]
#[must_use]
#[cfg(any(feature = "std", feature = "libm"))]
pub fn halflife(mut self, halflife: $ty) -> Self {
let ln2 = core::f64::consts::LN_2 as $ty;
self.alpha =
Option::Some(1.0 as $ty - crate::math::exp((-ln2 / halflife) as f64) as $ty);
self
}
#[inline]
#[must_use]
pub fn span(mut self, n: u64) -> Self {
self.alpha = Option::Some(2.0 as $ty / (n as $ty + 1.0 as $ty));
self
}
#[inline]
#[must_use]
pub fn threshold(mut self, threshold: $ty) -> Self {
self.threshold = Option::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<$name, 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 as $ty && alpha < 1.0 as $ty) {
return Err(crate::ConfigError::Invalid("alpha must be in (0, 1)"));
}
Ok($name {
alpha,
one_minus_alpha: 1.0 as $ty - alpha,
value: 0.0 as $ty,
threshold,
count: 0,
min_samples: self.min_samples,
})
}
}
};
}
impl_saturation!(SaturationF64, SaturationF64Builder, f64);
impl_saturation!(SaturationF32, SaturationF32Builder, f32);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn below_threshold_is_normal() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
for _ in 0..50 {
assert_eq!(s.update(0.5).unwrap(), Some(Condition::Normal));
}
}
#[test]
fn above_threshold_is_saturated() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
for _ in 0..50 {
let _ = s.update(0.95);
}
assert_eq!(s.update(0.95).unwrap(), Some(Condition::Degraded));
}
#[test]
fn crosses_back() {
let mut s = SaturationF64::builder()
.alpha(0.5)
.threshold(0.8)
.build()
.unwrap();
for _ in 0..50 {
let _ = s.update(0.95);
}
assert_eq!(s.update(0.95).unwrap(), Some(Condition::Degraded));
for _ in 0..50 {
let _ = s.update(0.3);
}
assert_eq!(s.update(0.3).unwrap(), Some(Condition::Normal));
}
#[test]
fn priming() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.min_samples(5)
.build()
.unwrap();
for _ in 0..4 {
assert!(s.update(0.95).unwrap().is_none());
}
assert!(s.update(0.95).unwrap().is_some());
}
#[test]
fn reset() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
for _ in 0..10 {
let _ = s.update(0.95);
}
s.reset();
assert_eq!(s.count(), 0);
assert!(s.utilization().is_none());
}
#[test]
fn f32_basic() {
let mut s = SaturationF32::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
assert_eq!(s.update(0.5).unwrap(), Some(Condition::Normal));
}
#[test]
#[allow(clippy::float_cmp)]
fn reconfigure_threshold_changes_behavior() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
for _ in 0..50 {
let _ = s.update(0.75);
}
assert_eq!(s.update(0.75).unwrap(), Some(Condition::Normal));
s.reconfigure_threshold(0.7);
assert_eq!(s.update(0.75).unwrap(), Some(Condition::Degraded));
}
#[test]
fn errors_without_threshold() {
let result = SaturationF64::builder().alpha(0.3).build();
assert!(matches!(
result,
Err(crate::ConfigError::Missing("threshold"))
));
}
#[test]
fn rejects_nan_and_inf() {
let mut s = SaturationF64::builder()
.alpha(0.3)
.threshold(0.8)
.build()
.unwrap();
assert_eq!(
s.update(f64::NAN).unwrap_err(),
crate::DataError::NotANumber
);
assert_eq!(
s.update(f64::INFINITY).unwrap_err(),
crate::DataError::Infinite
);
assert_eq!(
s.update(f64::NEG_INFINITY).unwrap_err(),
crate::DataError::Infinite
);
assert_eq!(s.count(), 0);
}
}