#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Peak<T> {
pub value: T,
pub is_maximum: bool,
}
macro_rules! impl_peak_detector_float {
($name:ident, $ty:ty, $zero:expr) => {
#[derive(Debug, Clone)]
pub struct $name {
prominence: $ty,
extreme: $ty,
rising: bool,
count: u64,
}
impl $name {
#[inline]
pub fn new(prominence: $ty) -> Result<Self, crate::ConfigError> {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
if !(prominence >= $zero) {
return Err(crate::ConfigError::Invalid(
"prominence must be non-negative",
));
}
Ok(Self {
prominence,
extreme: $zero,
rising: true,
count: 0,
})
}
#[inline]
pub fn update(&mut self, sample: $ty) -> Result<Option<Peak<$ty>>, crate::DataError> {
check_finite!(sample);
self.count += 1;
if self.count == 1 {
self.extreme = sample;
return Ok(Option::None);
}
if self.rising {
if sample > self.extreme {
self.extreme = sample;
Ok(Option::None)
} else if self.extreme - sample >= self.prominence {
let peak = Peak {
value: self.extreme,
is_maximum: true,
};
self.extreme = sample;
self.rising = false;
Ok(Option::Some(peak))
} else {
Ok(Option::None)
}
} else if sample < self.extreme {
self.extreme = sample;
Ok(Option::None)
} else if sample - self.extreme >= self.prominence {
let peak = Peak {
value: self.extreme,
is_maximum: false,
};
self.extreme = sample;
self.rising = true;
Ok(Option::Some(peak))
} else {
Ok(Option::None)
}
}
#[inline]
pub fn reset(&mut self) {
self.extreme = $zero;
self.rising = true;
self.count = 0;
}
}
};
}
macro_rules! impl_peak_detector_int {
($name:ident, $ty:ty, $zero:expr) => {
#[derive(Debug, Clone)]
pub struct $name {
prominence: $ty,
extreme: $ty,
rising: bool,
count: u64,
}
impl $name {
#[inline]
pub fn new(prominence: $ty) -> Result<Self, crate::ConfigError> {
if !(prominence >= $zero) {
return Err(crate::ConfigError::Invalid(
"prominence must be non-negative",
));
}
Ok(Self {
prominence,
extreme: $zero,
rising: true,
count: 0,
})
}
#[inline]
#[must_use]
pub fn update(&mut self, sample: $ty) -> Option<Peak<$ty>> {
self.count += 1;
if self.count == 1 {
self.extreme = sample;
return Option::None;
}
if self.rising {
if sample > self.extreme {
self.extreme = sample;
Option::None
} else if self.extreme - sample >= self.prominence {
let peak = Peak {
value: self.extreme,
is_maximum: true,
};
self.extreme = sample;
self.rising = false;
Option::Some(peak)
} else {
Option::None
}
} else if sample < self.extreme {
self.extreme = sample;
Option::None
} else if sample - self.extreme >= self.prominence {
let peak = Peak {
value: self.extreme,
is_maximum: false,
};
self.extreme = sample;
self.rising = true;
Option::Some(peak)
} else {
Option::None
}
}
#[inline]
pub fn reset(&mut self) {
self.extreme = $zero;
self.rising = true;
self.count = 0;
}
}
};
}
impl_peak_detector_float!(PeakDetectorF64, f64, 0.0);
impl_peak_detector_float!(PeakDetectorF32, f32, 0.0);
impl_peak_detector_int!(PeakDetectorI64, i64, 0);
impl_peak_detector_int!(PeakDetectorI32, i32, 0);
impl_peak_detector_int!(PeakDetectorI128, i128, 0);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_maximum() {
let mut pd = PeakDetectorF64::new(5.0).unwrap();
let _ = pd.update(10.0).unwrap();
let _ = pd.update(20.0).unwrap();
let _ = pd.update(30.0).unwrap(); let peak = pd.update(20.0).unwrap(); assert_eq!(
peak,
Some(Peak {
value: 30.0,
is_maximum: true
})
);
}
#[test]
fn detects_minimum() {
let mut pd = PeakDetectorF64::new(5.0).unwrap();
let _ = pd.update(30.0).unwrap();
let _ = pd.update(20.0).unwrap();
let _ = pd.update(10.0).unwrap(); let _ = pd.update(20.0).unwrap();
let mut pd2 = PeakDetectorF64::new(5.0).unwrap();
let _ = pd2.update(10.0).unwrap();
let _ = pd2.update(20.0).unwrap(); let _ = pd2.update(10.0).unwrap(); let _ = pd2.update(5.0).unwrap(); let peak = pd2.update(15.0).unwrap(); assert_eq!(
peak,
Some(Peak {
value: 5.0,
is_maximum: false
})
);
}
#[test]
fn small_oscillation_filtered() {
let mut pd = PeakDetectorF64::new(10.0).unwrap();
let _ = pd.update(100.0).unwrap();
let _ = pd.update(105.0).unwrap();
assert!(pd.update(102.0).unwrap().is_none()); }
#[test]
fn i64_basic() {
let mut pd = PeakDetectorI64::new(10).unwrap();
let _ = pd.update(0);
let _ = pd.update(50);
let peak = pd.update(30); assert_eq!(
peak,
Some(Peak {
value: 50,
is_maximum: true
})
);
}
#[test]
fn reset() {
let mut pd = PeakDetectorF64::new(5.0).unwrap();
let _ = pd.update(100.0).unwrap();
pd.reset();
assert!(pd.update(50.0).unwrap().is_none()); }
#[test]
fn rejects_negative_prominence() {
assert!(matches!(
PeakDetectorF64::new(-1.0),
Err(crate::ConfigError::Invalid(_))
));
}
#[test]
fn i128_basic() {
let mut pd = PeakDetectorI128::new(10).unwrap();
let _ = pd.update(0);
let _ = pd.update(50);
let peak = pd.update(30); assert_eq!(
peak,
Some(Peak {
value: 50,
is_maximum: true
})
);
}
#[test]
fn rejects_nan_and_inf() {
let mut pd = PeakDetectorF64::new(5.0).unwrap();
assert_eq!(pd.update(f64::NAN), Err(crate::DataError::NotANumber));
assert_eq!(pd.update(f64::INFINITY), Err(crate::DataError::Infinite));
assert_eq!(
pd.update(f64::NEG_INFINITY),
Err(crate::DataError::Infinite)
);
}
}