macro_rules! impl_peak_hold_float {
($name:ident, $builder:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
peak: $ty,
hold_samples: u64,
decay_rate: $ty,
hold_remaining: u64,
count: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
hold_samples: u64,
decay_rate: Option<$ty>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
hold_samples: 0,
decay_rate: Option::None,
}
}
#[inline]
pub fn update(&mut self, sample: $ty) -> Result<$ty, crate::DataError> {
check_finite!(sample);
self.count += 1;
if sample >= self.peak {
self.peak = sample;
self.hold_remaining = self.hold_samples;
return Ok(self.peak);
}
if self.hold_remaining > 0 {
self.hold_remaining -= 1;
return Ok(self.peak);
}
self.peak *= self.decay_rate;
if sample > self.peak {
self.peak = sample;
self.hold_remaining = self.hold_samples;
}
Ok(self.peak)
}
#[inline]
#[must_use]
pub fn peak(&self) -> $ty {
self.peak
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
pub fn reset(&mut self) {
self.peak = 0.0 as $ty;
self.hold_remaining = 0;
self.count = 0;
}
}
impl $builder {
#[inline]
#[must_use]
pub fn hold_samples(mut self, n: u64) -> Self {
self.hold_samples = n;
self
}
#[inline]
#[must_use]
pub fn decay_rate(mut self, rate: $ty) -> Self {
self.decay_rate = Option::Some(rate);
self
}
#[inline]
pub fn build(self) -> Result<$name, crate::ConfigError> {
let rate = self
.decay_rate
.ok_or(crate::ConfigError::Missing("decay_rate"))?;
if !(rate > 0.0 as $ty && rate <= 1.0 as $ty) {
return Err(crate::ConfigError::Invalid("decay_rate must be in (0, 1]"));
}
Ok($name {
peak: 0.0 as $ty,
hold_samples: self.hold_samples,
decay_rate: rate,
hold_remaining: 0,
count: 0,
})
}
}
};
}
macro_rules! impl_peak_hold_int {
($name:ident, $builder:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
peak: $ty,
hold_samples: u64,
hold_remaining: u64,
count: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
hold_samples: u64,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder { hold_samples: 0 }
}
#[inline]
#[must_use]
pub fn update(&mut self, sample: $ty) -> $ty {
self.count += 1;
if sample >= self.peak || self.count == 1 {
self.peak = sample;
self.hold_remaining = self.hold_samples;
return self.peak;
}
if self.hold_remaining > 0 {
self.hold_remaining -= 1;
return self.peak;
}
self.peak = sample;
self.hold_remaining = self.hold_samples;
self.peak
}
#[inline]
#[must_use]
pub fn peak(&self) -> $ty {
self.peak
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
pub fn reset(&mut self) {
self.peak = 0;
self.hold_remaining = 0;
self.count = 0;
}
}
impl $builder {
#[inline]
#[must_use]
pub fn hold_samples(mut self, n: u64) -> Self {
self.hold_samples = n;
self
}
#[inline]
pub fn build(self) -> Result<$name, crate::ConfigError> {
Ok($name {
peak: 0,
hold_samples: self.hold_samples,
hold_remaining: 0,
count: 0,
})
}
}
};
}
impl_peak_hold_float!(PeakHoldF64, PeakHoldF64Builder, f64);
impl_peak_hold_float!(PeakHoldF32, PeakHoldF32Builder, f32);
impl_peak_hold_int!(PeakHoldI64, PeakHoldI64Builder, i64);
impl_peak_hold_int!(PeakHoldI32, PeakHoldI32Builder, i32);
impl_peak_hold_int!(PeakHoldI128, PeakHoldI128Builder, i128);
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn instant_attack() {
let mut ph = PeakHoldF64::builder()
.decay_rate(0.95)
.hold_samples(5)
.build()
.unwrap();
assert_eq!(ph.update(50.0).unwrap(), 50.0);
assert_eq!(ph.update(100.0).unwrap(), 100.0); }
#[test]
#[allow(clippy::float_cmp)]
fn hold_period() {
let mut ph = PeakHoldF64::builder()
.decay_rate(0.95)
.hold_samples(3)
.build()
.unwrap();
let _ = ph.update(100.0).unwrap();
assert_eq!(ph.update(50.0).unwrap(), 100.0); assert_eq!(ph.update(50.0).unwrap(), 100.0); assert_eq!(ph.update(50.0).unwrap(), 100.0); }
#[test]
fn decay_after_hold() {
let mut ph = PeakHoldF64::builder()
.decay_rate(0.9)
.hold_samples(0)
.build()
.unwrap();
let _ = ph.update(100.0).unwrap();
let v = ph.update(0.0).unwrap(); assert!(v < 100.0, "should have decayed, got {v}");
}
#[test]
#[allow(clippy::float_cmp)]
fn new_peak_during_hold() {
let mut ph = PeakHoldF64::builder()
.decay_rate(0.95)
.hold_samples(10)
.build()
.unwrap();
let _ = ph.update(100.0).unwrap();
let _ = ph.update(50.0).unwrap(); assert_eq!(ph.update(200.0).unwrap(), 200.0); }
#[test]
fn i64_hold() {
let mut ph = PeakHoldI64::builder().hold_samples(3).build().unwrap();
let _ = ph.update(100);
assert_eq!(ph.update(50), 100); assert_eq!(ph.update(50), 100); assert_eq!(ph.update(50), 100); assert_eq!(ph.update(50), 50); }
#[test]
fn reset() {
let mut ph = PeakHoldF64::builder().decay_rate(0.95).build().unwrap();
let _ = ph.update(100.0).unwrap();
ph.reset();
assert_eq!(ph.count(), 0);
}
#[test]
fn errors_without_decay_rate() {
let result = PeakHoldF64::builder().build();
assert!(matches!(
result,
Err(crate::ConfigError::Missing("decay_rate"))
));
}
#[test]
fn i128_basic() {
let mut ph = PeakHoldI128::builder().hold_samples(3).build().unwrap();
let _ = ph.update(100);
assert_eq!(ph.update(50), 100); }
#[test]
fn rejects_nan_and_inf() {
let mut ph = PeakHoldF64::builder().decay_rate(0.95).build().unwrap();
assert!(matches!(
ph.update(f64::NAN),
Err(crate::DataError::NotANumber)
));
assert!(matches!(
ph.update(f64::INFINITY),
Err(crate::DataError::Infinite)
));
assert!(matches!(
ph.update(f64::NEG_INFINITY),
Err(crate::DataError::Infinite)
));
let mut ph32 = PeakHoldF32::builder().decay_rate(0.95).build().unwrap();
assert!(matches!(
ph32.update(f32::NAN),
Err(crate::DataError::NotANumber)
));
}
}