#[derive(Debug, Clone)]
pub struct HawkesIntensityF64 {
mu: f64,
alpha: f64,
beta: f64,
excitation: f64,
last_time: u64,
count: u64,
min_samples: u64,
}
#[derive(Debug, Clone)]
pub struct HawkesIntensityF64Builder {
mu: Option<f64>,
alpha: Option<f64>,
beta: Option<f64>,
min_samples: u64,
}
impl HawkesIntensityF64 {
#[inline]
#[must_use]
pub fn builder() -> HawkesIntensityF64Builder {
HawkesIntensityF64Builder {
mu: None,
alpha: None,
beta: None,
min_samples: 2,
}
}
#[inline]
pub fn update(&mut self, time: u64) {
self.count += 1;
if self.count == 1 {
self.excitation = self.alpha;
self.last_time = time;
return;
}
let dt = time.saturating_sub(self.last_time);
let decay = crate::math::exp(-(self.beta * dt as f64));
self.excitation = crate::math::MulAdd::fma(decay, self.excitation, self.alpha);
self.last_time = time;
}
#[inline]
#[must_use]
pub fn intensity(&self) -> f64 {
if self.count == 0 {
self.mu
} else {
self.mu + self.excitation
}
}
#[inline]
#[must_use]
pub fn intensity_at(&self, time: u64) -> f64 {
if self.count == 0 {
return self.mu;
}
let dt = time.saturating_sub(self.last_time);
let decay = crate::math::exp(-(self.beta * dt as f64));
crate::math::MulAdd::fma(decay, self.excitation, self.mu)
}
#[inline]
#[must_use]
pub fn baseline(&self) -> f64 {
self.mu
}
#[inline]
#[must_use]
pub fn branching_ratio(&self) -> f64 {
self.alpha / self.beta
}
#[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.excitation = 0.0;
self.last_time = 0;
self.count = 0;
}
}
impl HawkesIntensityF64Builder {
#[inline]
#[must_use]
pub fn mu(mut self, mu: f64) -> Self {
self.mu = Some(mu);
self
}
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
self
}
#[inline]
#[must_use]
pub fn beta(mut self, beta: f64) -> Self {
self.beta = Some(beta);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
pub fn build(self) -> Result<HawkesIntensityF64, crate::ConfigError> {
let mu = self.mu.ok_or(crate::ConfigError::Missing("mu"))?;
if mu <= 0.0 || !mu.is_finite() {
return Err(crate::ConfigError::Invalid(
"Hawkes mu must be positive and finite",
));
}
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
if alpha < 0.0 || !alpha.is_finite() {
return Err(crate::ConfigError::Invalid(
"Hawkes alpha must be non-negative and finite",
));
}
let beta = self.beta.ok_or(crate::ConfigError::Missing("beta"))?;
if beta <= 0.0 || !beta.is_finite() {
return Err(crate::ConfigError::Invalid(
"Hawkes beta must be positive and finite",
));
}
if alpha >= beta {
return Err(crate::ConfigError::Invalid(
"Hawkes alpha must be < beta (branching ratio < 1)",
));
}
Ok(HawkesIntensityF64 {
mu,
alpha,
beta,
excitation: 0.0,
last_time: 0,
count: 0,
min_samples: self.min_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn baseline_without_events() {
let h = HawkesIntensityF64::builder()
.mu(5.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
assert!((h.intensity() - 5.0).abs() < 1e-10);
assert!((h.intensity_at(1_000_000) - 5.0).abs() < 1e-10);
}
#[test]
fn excitation_spike() {
let mut h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
h.update(0);
let after_event = h.intensity();
assert!(
after_event > 1.0,
"intensity should spike above baseline after event, got {after_event}"
);
}
#[test]
fn decay_over_time() {
let mut h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
h.update(0);
let far_future = h.intensity_at(1_000_000);
assert!(
(far_future - 1.0).abs() < 0.01,
"intensity should decay to baseline, got {far_future}"
);
}
#[test]
fn burst_intensifies() {
let mut h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
h.update(0);
let after_one = h.intensity();
h.update(1);
let after_two = h.intensity();
h.update(2);
let after_three = h.intensity();
assert!(
after_three > after_two && after_two > after_one,
"rapid events should increase intensity: {after_one} < {after_two} < {after_three}"
);
}
#[test]
fn branching_ratio_value() {
let h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.3)
.beta(1.0)
.build()
.unwrap();
assert!((h.branching_ratio() - 0.3).abs() < 1e-10);
}
#[test]
fn stability_validation() {
let result = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(1.0)
.beta(1.0)
.build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
let result = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(2.0)
.beta(1.0)
.build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
}
#[test]
fn reset_clears() {
let mut h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
h.update(0);
h.update(10);
h.reset();
assert_eq!(h.count(), 0);
assert!((h.intensity() - 1.0).abs() < 1e-10);
}
#[test]
fn same_timestamp_events() {
let mut h = HawkesIntensityF64::builder()
.mu(1.0)
.alpha(0.5)
.beta(1.0)
.build()
.unwrap();
h.update(100);
let after_one = h.intensity();
h.update(100);
let after_two = h.intensity();
assert!(
after_two > after_one,
"same-time events should stack: {after_one} < {after_two}"
);
}
}