use crate::math::MulAdd;
macro_rules! impl_event_rate_float {
($name:ident, $builder:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
alpha: $ty,
one_minus_alpha: $ty,
interval: $ty,
last_timestamp: $ty,
count: u64,
min_samples: u64,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
alpha: Option<$ty>,
min_samples: u64,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
alpha: Option::None,
min_samples: 2,
}
}
#[inline]
pub fn update(&mut self, timestamp: $ty) -> Result<(), crate::DataError> {
check_finite!(timestamp);
self.count += 1;
if self.count == 1 {
self.last_timestamp = timestamp;
return Ok(());
}
let dt = timestamp - self.last_timestamp;
self.last_timestamp = timestamp;
if self.count == 2 {
self.interval = dt;
} else {
self.interval = self.alpha.fma(dt, self.one_minus_alpha * self.interval);
}
Ok(())
}
#[inline]
#[must_use]
pub fn rate(&self) -> Option<$ty> {
if self.count < self.min_samples || self.interval <= (0.0 as $ty) {
Option::None
} else {
Option::Some(1.0 as $ty / self.interval)
}
}
#[inline]
#[must_use]
pub fn interval(&self) -> Option<$ty> {
if self.count >= 2 {
Option::Some(self.interval)
} 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.interval = 0.0 as $ty;
self.last_timestamp = 0.0 as $ty;
self.count = 0;
}
}
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;
let alpha = 1.0 as $ty - crate::math::exp((-ln2 / halflife) as f64) as $ty;
self.alpha = Option::Some(alpha);
self
}
#[inline]
#[must_use]
pub fn span(mut self, n: u64) -> Self {
let alpha = 2.0 as $ty / (n as $ty + 1.0 as $ty);
self.alpha = Option::Some(alpha);
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"))?;
if !(alpha > 0.0 as $ty && alpha < 1.0 as $ty) {
return Err(crate::ConfigError::Invalid(
"EventRate alpha must be in (0, 1)",
));
}
Ok($name {
alpha,
one_minus_alpha: 1.0 as $ty - alpha,
interval: 0.0 as $ty,
last_timestamp: 0.0 as $ty,
count: 0,
min_samples: self.min_samples,
})
}
}
};
}
impl_event_rate_float!(EventRateF64, EventRateF64Builder, f64);
impl_event_rate_float!(EventRateF32, EventRateF32Builder, f32);
macro_rules! impl_event_rate_int {
($name:ident, $builder:ident, $ty:ty, $acc_ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
acc: $acc_ty,
shift: u32,
span: u64,
last_timestamp: $ty,
count: u64,
min_samples: u64,
initialized: bool,
}
#[doc = stringify!($name)]
#[derive(Debug, Clone)]
pub struct $builder {
span: Option<u64>,
min_samples: u64,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
span: Option::None,
min_samples: 2,
}
}
#[inline]
pub fn update(&mut self, timestamp: $ty) {
self.count += 1;
if self.count == 1 {
self.last_timestamp = timestamp;
return;
}
let dt = timestamp - self.last_timestamp;
self.last_timestamp = timestamp;
if !self.initialized {
self.acc = (dt as $acc_ty) << self.shift;
self.initialized = true;
} else {
let dt_shifted = (dt as $acc_ty) << self.shift;
self.acc += (dt_shifted - self.acc) >> self.shift;
}
}
#[inline]
#[must_use]
pub fn interval(&self) -> Option<$ty> {
if self.count >= 2 && self.initialized {
Option::Some((self.acc >> self.shift) as $ty)
} else {
Option::None
}
}
#[inline]
#[must_use]
pub fn effective_span(&self) -> u64 {
self.span
}
#[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.acc = 0;
self.last_timestamp = 0;
self.count = 0;
self.initialized = false;
}
}
impl $builder {
#[inline]
#[must_use]
pub fn span(mut self, n: u64) -> Self {
self.span = Option::Some(n);
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 requested = self.span.ok_or(crate::ConfigError::Missing("span"))?;
if requested < 1 {
return Err(crate::ConfigError::Invalid("EventRate span must be >= 1"));
}
let effective = crate::smoothing::ema::next_power_of_two_minus_one(requested);
let shift = crate::smoothing::ema::log2_of_span_plus_one(effective);
Ok($name {
acc: 0,
shift,
span: effective,
last_timestamp: 0,
count: 0,
min_samples: self.min_samples,
initialized: false,
})
}
}
};
}
impl_event_rate_int!(EventRateI64, EventRateI64Builder, i64, i128);
impl_event_rate_int!(EventRateI32, EventRateI32Builder, i32, i64);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_rate() {
let mut er = EventRateF64::builder().alpha(0.3).build().unwrap();
for i in 0..100 {
er.update(i as f64 * 10.0).unwrap();
}
let rate = er.rate().unwrap();
assert!((rate - 0.1).abs() < 0.01, "rate should be ~0.1, got {rate}");
}
#[test]
fn burst_increases_rate() {
let mut er = EventRateF64::builder().alpha(0.5).build().unwrap();
for i in 0..20 {
er.update(i as f64 * 10.0).unwrap();
}
let normal_rate = er.rate().unwrap();
for i in 0..20 {
er.update(200.0 + i as f64).unwrap();
}
let burst_rate = er.rate().unwrap();
assert!(
burst_rate > normal_rate,
"burst rate ({burst_rate}) should exceed normal ({normal_rate})"
);
}
#[test]
fn priming() {
let mut er = EventRateF64::builder()
.alpha(0.3)
.min_samples(5)
.build()
.unwrap();
for i in 0..4 {
er.update(i as f64 * 10.0).unwrap();
assert!(er.rate().is_none());
}
er.update(40.0).unwrap();
assert!(er.rate().is_some());
}
#[test]
fn reset() {
let mut er = EventRateF64::builder().alpha(0.3).build().unwrap();
for i in 0..10 {
er.update(i as f64 * 10.0).unwrap();
}
er.reset();
assert_eq!(er.count(), 0);
assert!(er.rate().is_none());
}
#[test]
fn f32_basic() {
let mut er = EventRateF32::builder().alpha(0.3).build().unwrap();
er.update(0.0).unwrap();
er.update(10.0).unwrap();
assert!(er.rate().is_some());
}
#[test]
fn i64_basic() {
let mut er = EventRateI64::builder().span(7).build().unwrap();
for i in 0..10 {
er.update(i * 100);
}
let interval = er.interval().unwrap();
assert!(
(interval - 100).abs() <= 1,
"interval should be ~100, got {interval}"
);
}
#[test]
fn i32_basic() {
let mut er = EventRateI32::builder().span(3).build().unwrap();
er.update(0);
er.update(50);
assert!(er.interval().is_some());
}
#[test]
fn zero_interval_returns_none() {
let mut er = EventRateF64::builder().alpha(0.3).build().unwrap();
er.update(100.0).unwrap();
er.update(100.0).unwrap(); assert!(
er.rate().is_none(),
"rate should be None with zero interval"
);
}
#[test]
fn errors_without_alpha() {
let result = EventRateF64::builder().build();
assert!(matches!(result, Err(crate::ConfigError::Missing("alpha"))));
}
#[test]
fn rejects_nan_and_inf() {
let mut er = EventRateF64::builder().alpha(0.3).build().unwrap();
assert!(matches!(
er.update(f64::NAN),
Err(crate::DataError::NotANumber)
));
assert!(matches!(
er.update(f64::INFINITY),
Err(crate::DataError::Infinite)
));
let mut er32 = EventRateF32::builder().alpha(0.3).build().unwrap();
assert!(matches!(
er32.update(f32::NAN),
Err(crate::DataError::NotANumber)
));
}
}
#[cfg(feature = "std")]
mod instant_event_rate {
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct EventRateInstant {
alpha: f64,
one_minus_alpha: f64,
interval: f64, last_timestamp: Option<Instant>,
count: u64,
min_samples: u64,
}
#[derive(Debug, Clone)]
pub struct EventRateInstantBuilder {
alpha: Option<f64>,
min_samples: u64,
}
impl EventRateInstant {
#[inline]
#[must_use]
pub fn builder() -> EventRateInstantBuilder {
EventRateInstantBuilder {
alpha: None,
min_samples: 2,
}
}
#[inline]
pub fn update(&mut self, now: Instant) {
self.count += 1;
if let Some(last) = self.last_timestamp {
let dt = now.saturating_duration_since(last).as_secs_f64();
if self.count == 2 {
self.interval = dt;
} else {
self.interval = self.alpha.mul_add(dt, self.one_minus_alpha * self.interval);
}
}
self.last_timestamp = Some(now);
}
#[inline]
#[must_use]
pub fn rate(&self) -> Option<f64> {
if self.count < self.min_samples || self.interval <= 0.0 {
None
} else {
Some(1.0 / self.interval)
}
}
#[inline]
#[must_use]
pub fn interval(&self) -> Option<Duration> {
if self.count >= 2 && self.interval > 0.0 {
Some(Duration::from_secs_f64(self.interval))
} 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, now: Instant) {
self.interval = 0.0;
self.last_timestamp = Some(now);
self.count = 0;
}
}
impl EventRateInstantBuilder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
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 min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
pub fn build(self) -> Result<EventRateInstant, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
if !(alpha > 0.0 && alpha < 1.0) {
return Err(crate::ConfigError::Invalid(
"EventRateInstant alpha must be in (0, 1)",
));
}
Ok(EventRateInstant {
alpha,
one_minus_alpha: 1.0 - alpha,
interval: 0.0,
last_timestamp: None,
count: 0,
min_samples: self.min_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_rate() {
let base = Instant::now();
let mut er = EventRateInstant::builder().span(10).build().unwrap();
er.update(base);
er.update(base + Duration::from_secs(1));
let rate = er.rate().unwrap();
assert!((rate - 1.0).abs() < 0.1, "expected ~1.0 eps, got {rate}");
}
#[test]
fn interval_as_duration() {
let base = Instant::now();
let mut er = EventRateInstant::builder().span(10).build().unwrap();
er.update(base);
er.update(base + Duration::from_millis(500));
let interval = er.interval().unwrap();
let ms = interval.as_millis();
assert!((450..=550).contains(&ms), "expected ~500ms, got {ms}ms");
}
#[test]
fn not_primed_before_min_samples() {
let base = Instant::now();
let mut er = EventRateInstant::builder()
.span(10)
.min_samples(5)
.build()
.unwrap();
er.update(base);
er.update(base + Duration::from_secs(1));
assert!(!er.is_primed());
assert!(er.rate().is_none());
}
#[test]
fn reset_clears() {
let base = Instant::now();
let mut er = EventRateInstant::builder().span(10).build().unwrap();
er.update(base);
er.update(base + Duration::from_secs(1));
er.reset(base + Duration::from_secs(2));
assert_eq!(er.count(), 0);
assert!(er.rate().is_none());
}
}
}
#[cfg(feature = "std")]
pub use instant_event_rate::{EventRateInstant, EventRateInstantBuilder};