#[derive(Debug, Clone)]
pub struct HawkesIntensity {
mu: f64,
alpha: f64,
beta: f64,
intensity: f64,
last_time_ns: i64,
event_count: u64,
ema_delta_ns: f64,
ema_delta_sq_ns: f64,
}
const MIN_EVENTS_FOR_ESTIMATION: u64 = 10;
const EMA_ALPHA: f64 = 0.05;
impl Default for HawkesIntensity {
fn default() -> Self {
Self::new()
}
}
impl HawkesIntensity {
pub const fn new() -> Self {
Self {
mu: 1e-9, alpha: 0.5e-9, beta: 1e-9, intensity: 1e-9,
last_time_ns: 0,
event_count: 0,
ema_delta_ns: 0.0,
ema_delta_sq_ns: 0.0,
}
}
pub fn update(&mut self, timestamp_ns: i64) {
if self.event_count == 0 {
self.last_time_ns = timestamp_ns;
self.intensity = self.mu + self.alpha;
self.event_count = 1;
return;
}
let dt = (timestamp_ns - self.last_time_ns).max(0) as f64;
if self.event_count == 1 {
self.ema_delta_ns = dt;
self.ema_delta_sq_ns = dt * dt;
} else {
self.ema_delta_ns = EMA_ALPHA.mul_add(dt, (1.0 - EMA_ALPHA) * self.ema_delta_ns);
self.ema_delta_sq_ns =
(EMA_ALPHA * dt).mul_add(dt, (1.0 - EMA_ALPHA) * self.ema_delta_sq_ns);
}
let decay = (-self.beta * dt).exp();
self.intensity = (self.intensity - self.mu + self.alpha).mul_add(decay, self.mu);
self.last_time_ns = timestamp_ns;
self.event_count += 1;
if self.event_count >= MIN_EVENTS_FOR_ESTIMATION {
self.estimate_parameters();
}
}
pub fn current_intensity(&self, now_ns: i64) -> f64 {
if self.event_count == 0 {
return self.mu;
}
let dt = (now_ns - self.last_time_ns).max(0) as f64;
let decay = (-self.beta * dt).exp();
(self.intensity - self.mu).mul_add(decay, self.mu)
}
pub fn boost_factor(&self, now_ns: i64) -> f64 {
if self.mu <= 0.0 {
return 1.0;
}
let current = self.current_intensity(now_ns);
(current / self.mu).clamp(1.0, 5.0)
}
fn estimate_parameters(&mut self) {
let mean_delta = self.ema_delta_ns;
if mean_delta <= 0.0 {
return;
}
self.mu = (1.0 / mean_delta).max(1e-15);
let variance = mean_delta.mul_add(-mean_delta, self.ema_delta_sq_ns);
if variance > 0.0 {
let stddev = variance.sqrt();
self.beta = (1.0 / stddev).max(1e-15);
}
self.alpha = self.mu * 0.5;
self.intensity = self.intensity.max(self.mu);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_boost_is_one() {
let hawkes = HawkesIntensity::new();
let boost = hawkes.boost_factor(0);
assert!(
(boost - 1.0).abs() < 1e-6,
"Initial boost should be 1.0, got {boost}"
);
}
#[test]
fn test_burst_increases_boost() {
let mut hawkes = HawkesIntensity::new();
for i in 0..20 {
hawkes.update(i * 1_000_000);
}
let boost = hawkes.boost_factor(20 * 1_000_000);
assert!(
boost > 1.0,
"Burst of events should increase boost above 1.0, got {boost}"
);
}
#[test]
fn test_decay_reduces_boost() {
let mut hawkes = HawkesIntensity::new();
for i in 0..20 {
hawkes.update(i * 1_000_000);
}
let boost_at_burst = hawkes.boost_factor(20 * 1_000_000);
let boost_after_decay = hawkes.boost_factor(20 * 1_000_000 + 10_000_000_000);
assert!(
boost_after_decay < boost_at_burst,
"Boost should decay over time: at_burst={boost_at_burst}, after_decay={boost_after_decay}"
);
}
#[test]
fn test_boost_clamped_at_five() {
let mut hawkes = HawkesIntensity::new();
for i in 0..1000 {
hawkes.update(i * 1_000);
}
let boost = hawkes.boost_factor(1000 * 1_000);
assert!(boost <= 5.0, "Boost should be clamped at 5.0, got {boost}");
}
#[test]
fn test_single_event_no_panic() {
let mut hawkes = HawkesIntensity::new();
hawkes.update(1_000_000_000);
let boost = hawkes.boost_factor(1_000_000_000);
assert!(boost >= 1.0, "Single event boost should be >= 1.0");
}
#[test]
fn test_parameter_estimation() {
let mut hawkes = HawkesIntensity::new();
for i in 0..100 {
hawkes.update(i * 1_000_000_000);
}
assert!(hawkes.mu > 0.0, "mu should be positive after estimation");
assert!(
hawkes.beta > 0.0,
"beta should be positive after estimation"
);
assert!(
hawkes.alpha > 0.0,
"alpha should be positive after estimation"
);
}
#[test]
fn test_zero_delta_handled() {
let mut hawkes = HawkesIntensity::new();
hawkes.update(1_000_000_000);
hawkes.update(1_000_000_000);
hawkes.update(1_000_000_000);
let boost = hawkes.boost_factor(1_000_000_000);
assert!(boost.is_finite(), "Boost should be finite, got {boost}");
assert!(boost >= 1.0, "Boost should be >= 1.0, got {boost}");
}
}