use serde::{Deserialize, Serialize};
use super::errors::HawkesError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HawkesEstimationConfig {
pub max_iter: usize,
pub tol: f64,
pub learning_rate: f64,
pub initial_params: Option<(f64, f64, f64)>,
}
impl Default for HawkesEstimationConfig {
fn default() -> Self {
Self {
max_iter: 1_000,
tol: 1e-3,
learning_rate: 1e-3,
initial_params: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HawkesEstimationResult {
pub mu: f64,
pub alpha: f64,
pub beta: f64,
pub log_likelihood: f64,
pub iterations: usize,
pub converged: bool,
pub branching_ratio: f64,
pub aic: f64,
pub bic: f64,
}
const PARAM_EPS: f64 = 1e-12;
const MAX_BRANCHING: f64 = 0.9999;
pub fn log_likelihood(mu: f64, alpha: f64, beta: f64, events: &[f64]) -> f64 {
let n = events.len();
if n == 0 {
return 0.0;
}
let t_max = events[n - 1];
let mut ll = -(mu * t_max);
let mut a = 0.0_f64;
for i in 0..n {
if i > 0 {
let dt = events[i] - events[i - 1];
a = (-beta * dt).exp() * (1.0 + a);
}
let intensity = mu + alpha * a;
if intensity <= 0.0 {
return f64::NEG_INFINITY;
}
ll += intensity.ln();
let remaining = t_max - events[i];
ll -= (alpha / beta) * (1.0 - (-beta * remaining).exp());
}
ll
}
pub fn gradient(mu: f64, alpha: f64, beta: f64, events: &[f64]) -> (f64, f64, f64) {
let n = events.len();
if n == 0 {
return (0.0, 0.0, 0.0);
}
let t_max = events[n - 1];
let mut dl_dmu = -t_max;
let mut dl_dalpha = 0.0_f64;
let mut dl_dbeta = 0.0_f64;
let mut a = 0.0_f64; let mut b = 0.0_f64;
for i in 0..n {
if i > 0 {
let dt = events[i] - events[i - 1];
let exp_neg = (-beta * dt).exp();
let one_plus_a = 1.0 + a;
b = -dt * exp_neg * one_plus_a + exp_neg * b;
a = exp_neg * one_plus_a;
}
let intensity = mu + alpha * a;
if intensity <= PARAM_EPS {
continue; }
let inv_lambda = 1.0 / intensity;
dl_dmu += inv_lambda;
dl_dalpha += a * inv_lambda;
dl_dbeta += alpha * b * inv_lambda;
let remaining = t_max - events[i];
let exp_rem = (-beta * remaining).exp();
dl_dalpha -= (1.0 / beta) * (1.0 - exp_rem);
dl_dbeta += (alpha / (beta * beta)) * (1.0 - exp_rem);
dl_dbeta -= (alpha / beta) * remaining * exp_rem;
}
(dl_dmu, dl_dalpha, dl_dbeta)
}
#[inline]
pub fn project(mu: &mut f64, alpha: &mut f64, beta: &mut f64) {
*mu = mu.max(PARAM_EPS);
*alpha = alpha.max(PARAM_EPS);
*beta = beta.max(PARAM_EPS);
if *alpha / *beta >= MAX_BRANCHING {
*alpha = MAX_BRANCHING * *beta;
}
}
#[inline]
pub fn grad_norm(g: &(f64, f64, f64)) -> f64 {
g.0.abs().max(g.1.abs()).max(g.2.abs())
}
pub fn estimate_hawkes_mle(
events: &[f64],
config: &HawkesEstimationConfig,
) -> Result<HawkesEstimationResult, HawkesError> {
validate_events(events)?;
let n = events.len();
let t_max = events[n - 1];
let t_min = events[0];
let duration = t_max - t_min;
if duration <= 0.0 {
return Err(HawkesError::InvalidEventTimes(
"All events have the same timestamp".into(),
));
}
let (mut mu, mut alpha, mut beta) = match config.initial_params {
Some((m, a, b)) => (m, a, b),
None => {
let mean_ia = duration / (n as f64 - 1.0);
let lambda_hat = n as f64 / duration;
let mu0 = 0.8 * lambda_hat;
let beta0 = 1.0 / mean_ia;
let alpha0 = 0.2 * beta0;
(mu0, alpha0, beta0)
}
};
project(&mut mu, &mut alpha, &mut beta);
let mut ll = log_likelihood(mu, alpha, beta, events);
let mut lr = config.learning_rate;
let mut converged = false;
let mut iterations = 0;
for iter in 0..config.max_iter {
iterations = iter + 1;
let g = gradient(mu, alpha, beta, events);
let gn = grad_norm(&g);
if gn < config.tol {
converged = true;
break;
}
let inv_gn = 1.0 / gn;
let d = (g.0 * inv_gn, g.1 * inv_gn, g.2 * inv_gn);
let c1 = 1e-4; let mut step = lr;
let mut found_step = false;
for _ in 0..30 {
let mut mu_new = mu + step * d.0;
let mut alpha_new = alpha + step * d.1;
let mut beta_new = beta + step * d.2;
project(&mut mu_new, &mut alpha_new, &mut beta_new);
let ll_new = log_likelihood(mu_new, alpha_new, beta_new, events);
let directional =
g.0 * (mu_new - mu) + g.1 * (alpha_new - alpha) + g.2 * (beta_new - beta);
if ll_new.is_finite() && ll_new >= ll + c1 * directional {
mu = mu_new;
alpha = alpha_new;
beta = beta_new;
ll = ll_new;
found_step = true;
lr = (step * 1.2).min(1.0);
break;
}
step *= 0.5;
}
if !found_step {
lr *= 0.1;
if lr < 1e-15 {
break;
}
}
if !ll.is_finite() {
return Err(HawkesError::EstimationFailed(
"Log-likelihood became NaN/Inf during optimization".into(),
));
}
}
let branching_ratio = alpha / beta;
let k = 3.0_f64; let n_f = n as f64;
let aic = 2.0 * k - 2.0 * ll;
let bic = k * n_f.ln() - 2.0 * ll;
if !converged {
let final_grad = gradient(mu, alpha, beta, events);
let gn = grad_norm(&final_grad);
if gn < config.tol {
converged = true;
} else {
return Err(HawkesError::NotConverged {
iterations,
gradient_norm: gn,
});
}
}
Ok(HawkesEstimationResult {
mu,
alpha,
beta,
log_likelihood: ll,
iterations,
converged,
branching_ratio,
aic,
bic,
})
}
pub fn compute_recursion_arrays(beta: f64, events: &[f64]) -> (Vec<f64>, Vec<f64>) {
let n = events.len();
let mut a_vals = Vec::with_capacity(n);
let mut b_vals = Vec::with_capacity(n);
let mut a = 0.0_f64;
let mut b = 0.0_f64;
for i in 0..n {
if i > 0 {
let dt = events[i] - events[i - 1];
let exp_neg = (-beta * dt).exp();
let one_plus_a = 1.0 + a;
b = -dt * exp_neg * one_plus_a + exp_neg * b;
a = exp_neg * one_plus_a;
}
a_vals.push(a);
b_vals.push(b);
}
(a_vals, b_vals)
}
pub fn validate_events(events: &[f64]) -> Result<(), HawkesError> {
if events.len() < 2 {
return Err(HawkesError::InvalidEventTimes(
"Need at least 2 events for estimation".into(),
));
}
for (i, &t) in events.iter().enumerate() {
if !t.is_finite() {
return Err(HawkesError::InvalidEventTimes(format!(
"Event at index {} is not finite: {}",
i, t
)));
}
}
for i in 1..events.len() {
if events[i] <= events[i - 1] {
return Err(HawkesError::InvalidEventTimes(format!(
"Events not strictly increasing at index {}: {} <= {}",
i,
events[i],
events[i - 1]
)));
}
}
Ok(())
}
pub fn compensator(mu: f64, alpha: f64, beta: f64, events: &[f64], t: f64) -> f64 {
let mut comp = mu * t;
for &ti in events {
if ti >= t {
break;
}
comp += (alpha / beta) * (1.0 - (-beta * (t - ti)).exp());
}
comp
}
pub fn time_rescaling_residuals(
mu: f64,
alpha: f64,
beta: f64,
events: &[f64],
) -> Vec<f64> {
let n = events.len();
if n < 2 {
return vec![];
}
let mut residuals = Vec::with_capacity(n - 1);
let mut comp_prev = compensator(mu, alpha, beta, events, events[0]);
for i in 1..n {
let comp_curr = compensator(mu, alpha, beta, events, events[i]);
residuals.push(comp_curr - comp_prev);
comp_prev = comp_curr;
}
residuals
}