1use crate::process::StochasticProcess;
8use crate::simulatable::Simulatable;
9use rand::Rng;
10
11pub trait Kernel: Clone + Send + Sync {
13 fn call(&self, t: f64) -> f64;
14
15 fn supports_recursive_intensity(&self) -> bool {
18 false
19 }
20
21 fn decay_rate(&self) -> f64 {
24 0.0
25 }
26
27 fn excitation_size(&self) -> f64 {
30 0.0
31 }
32}
33
34#[derive(Clone)]
36pub struct ExponentialKernel {
37 pub alpha: f64,
38 pub beta: f64,
39}
40
41impl ExponentialKernel {
42 pub fn new(alpha: f64, beta: f64) -> Self {
43 Self { alpha, beta }
44 }
45}
46
47impl Kernel for ExponentialKernel {
48 fn call(&self, t: f64) -> f64 {
49 if t < 0.0 {
50 0.0
51 } else {
52 self.alpha * (-self.beta * t).exp()
53 }
54 }
55
56 fn supports_recursive_intensity(&self) -> bool {
57 true
58 }
59
60 fn decay_rate(&self) -> f64 {
61 self.beta
62 }
63
64 fn excitation_size(&self) -> f64 {
65 self.alpha
66 }
67}
68
69#[derive(Clone)]
71pub struct PowerLawKernel {
72 pub alpha: f64,
73 pub c: f64,
74 pub p: f64,
75}
76
77impl PowerLawKernel {
78 pub fn new(alpha: f64, c: f64, p: f64) -> Self {
79 Self { alpha, c, p }
80 }
81}
82
83impl Kernel for PowerLawKernel {
84 fn call(&self, t: f64) -> f64 {
85 if t < 0.0 {
86 0.0
87 } else {
88 self.alpha / (self.c + t).powf(self.p)
89 }
90 }
91}
92
93#[derive(Clone)]
101pub struct HawkesProcess<K: Kernel> {
102 pub mu: f64,
103 pub kernel: K,
104 jumps: Vec<f64>,
105 current_time: f64,
106 state: f64,
107 current_intensity: f64,
110 prune_threshold: f64,
112}
113
114impl<K: Kernel> HawkesProcess<K> {
115 pub fn new(mu: f64, kernel: K) -> Self {
116 Self {
117 mu,
118 kernel,
119 jumps: Vec::new(),
120 current_time: 0.0,
121 state: mu,
122 current_intensity: mu,
123 prune_threshold: f64::NEG_INFINITY,
124 }
125 }
126
127 pub fn value(&self) -> f64 {
128 self.state
129 }
130
131 fn compute_intensity(&self, t: f64) -> f64 {
132 if self.kernel.supports_recursive_intensity() {
133 let alpha = self.kernel.excitation_size();
139 let beta = self.kernel.decay_rate();
140 let mut intensity = self.mu;
141 for &tj in &self.jumps {
142 if tj < t {
143 intensity += alpha * (-beta * (t - tj)).exp();
144 } else {
145 break;
146 }
147 }
148 intensity
149 } else {
150 let mut sum = 0.0;
152 for &tj in &self.jumps {
153 if tj < t {
154 sum += self.kernel.call(t - tj);
155 } else {
156 break;
157 }
158 }
159 self.mu + sum
160 }
161 }
162
163 fn prune_jumps(&mut self) {
170 let cutoff = self.current_time - 10.0 / self.kernel.decay_rate();
171 if cutoff <= self.prune_threshold {
172 return;
173 }
174 self.prune_threshold = cutoff;
175 let first = self.jumps.partition_point(|tj| *tj < cutoff);
176 if first > 0 {
177 self.jumps.drain(..first);
178 }
179 }
180}
181
182impl<K: Kernel + 'static> Simulatable for HawkesProcess<K> {
183 type Output = f64;
184
185 fn dim(&self) -> usize {
186 0 }
188
189 fn step(&mut self, dt: f64, _dw: &[f64], rng: &mut impl Rng) -> f64 {
190 let end_time = self.current_time + dt;
191
192 if self.kernel.supports_recursive_intensity() {
193 let beta = self.kernel.decay_rate();
204 let alpha = self.kernel.excitation_size();
205 let mut lambda_t = self.current_intensity;
206 let mut lambda_p = self.current_intensity;
207 let mut t_now = self.current_time;
208
209 while t_now < end_time {
210 if lambda_p <= 1e-12 {
211 break;
212 }
213
214 let u: f64 = rng.random();
215 let tau = -u.ln() / lambda_p;
216 let candidate = t_now + tau;
217
218 if candidate > end_time {
219 let dtau = end_time - t_now;
221 let decay = (-beta * dtau).exp();
222 lambda_t = self.mu + (lambda_t - self.mu) * decay;
223 t_now = end_time;
224 break;
225 }
226
227 let dtau = candidate - t_now;
228 let decay = (-beta * dtau).exp();
229 let lambda_t_candidate = self.mu + (lambda_t - self.mu) * decay;
230 let lambda_p_candidate = self.mu + (lambda_p - self.mu) * decay;
231
232 let u_accept: f64 = rng.random();
233 if u_accept * lambda_p <= lambda_t_candidate {
236 self.jumps.push(candidate);
237 lambda_t = lambda_t_candidate + alpha;
238 lambda_p = lambda_t_candidate;
241 } else {
242 lambda_t = lambda_t_candidate;
243 lambda_p = lambda_p_candidate;
244 }
245 t_now = candidate;
246 }
247
248 self.current_time = t_now;
249 self.state = lambda_t;
250 self.current_intensity = lambda_t;
251 self.prune_jumps();
252 } else {
253 while self.current_time < end_time {
255 let lambda_star = self.compute_intensity(self.current_time);
256
257 if lambda_star <= 1e-12 {
258 self.current_time = end_time;
259 break;
260 }
261
262 let u: f64 = rng.random();
263 let tau = -u.ln() / lambda_star;
264 let candidate = self.current_time + tau;
265
266 if candidate > end_time {
267 self.current_time = end_time;
268 break;
269 }
270
271 self.current_time = candidate;
272 let lambda_s = self.compute_intensity(self.current_time);
273 let u_accept: f64 = rng.random();
274
275 if u_accept * lambda_star <= lambda_s {
276 self.jumps.push(self.current_time);
277 }
278 }
279
280 self.state = self.compute_intensity(self.current_time);
281 self.current_intensity = self.state;
282 }
283
284 self.state
285 }
286
287 fn current(&self) -> f64 {
288 self.state
289 }
290}
291
292impl<K: Kernel + 'static> HawkesProcess<K> {
294 pub fn simulate_intensity(&self, total_time: f64, n_steps: usize) -> Vec<f64> {
295 let mut sim = self.clone();
296 let dt = total_time / n_steps as f64;
297 let mut path = Vec::with_capacity(n_steps + 1);
298 path.push(sim.current());
299 for _ in 0..n_steps {
300 let v = Simulatable::step(&mut sim, dt, &[], &mut rand::rng());
301 path.push(v);
302 }
303 path
304 }
305}
306
307impl<K: Kernel + 'static> StochasticProcess for HawkesProcess<K> {
308 type State = f64;
309 type Diffusion = f64;
310 type BrownianIncrement = f64;
311
312 fn drift(&self, x: &f64, _t: f64) -> f64 {
313 self.mu - x
314 }
315
316 fn diffusion(&self, _x: &f64, _t: f64) -> f64 {
317 0.0
318 }
319
320 fn jump_intensity(&self, _x: &f64, _t: f64) -> f64 {
321 self.mu
322 }
323
324 fn jump_size<R: Rng + ?Sized>(&self, _x: &f64, t: f64, _rng: &mut R) -> f64 {
325 self.kernel.call(t)
326 }
327
328 fn expected_value(&self, x0: &f64, _t: f64) -> f64 {
329 *x0
330 }
331
332 fn variance(&self, _x0: &f64, _t: f64) -> f64 {
333 self.mu
334 }
335
336 fn step<R: Rng + ?Sized>(&self, x: &f64, _t: f64, dt: f64, _dw: &f64, rng: &mut R) -> f64 {
337 let lambda = self.mu;
338 if lambda <= 1e-12 {
339 return *x;
340 }
341 let u: f64 = rng.random();
342 let tau = -u.ln() / lambda;
343 if tau < dt { *x + 1.0 } else { *x }
344 }
345
346 fn generate_increment<R: Rng + ?Sized>(&self, _rng: &mut R, _dt: f64) -> f64 {
347 0.0
348 }
349
350 fn simulate<R: Rng + ?Sized>(
351 &self,
352 x0: f64,
353 t0: f64,
354 t_end: f64,
355 dt: f64,
356 rng: &mut R,
357 ) -> Vec<(f64, f64)> {
358 let n_steps = ((t_end - t0) / dt).ceil() as usize;
359 let mut path = Vec::with_capacity(n_steps + 1);
360 let mut t = t0;
361 let mut x = x0;
362 path.push((t, x));
363
364 for _ in 0..n_steps {
365 x = self.step(&x, t, dt, &0.0, rng);
366 t += dt;
367 path.push((t, x));
368 }
369 path
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_hawkes_no_brownian_dimension() {
379 let kernel = ExponentialKernel::new(0.5, 2.0);
380 let h = HawkesProcess::new(0.1, kernel);
381 assert_eq!(h.dim(), 0);
382 }
383
384 #[test]
385 fn test_hawkes_intensity_positive() {
386 let kernel = ExponentialKernel::new(0.5, 2.0);
387 let h = HawkesProcess::new(0.5, kernel);
388 let path = h.simulate_intensity(1.0, 100);
389 for v in path {
390 assert!(v >= 0.0);
391 }
392 }
393
394 #[test]
395 fn test_exponential_kernel() {
396 let k = ExponentialKernel::new(1.0, 2.0);
397 assert_eq!(k.call(0.0), 1.0);
398 assert!(k.call(1.0) < 1.0);
399 assert_eq!(k.call(-0.5), 0.0);
400 }
401
402 #[test]
403 fn test_power_law_kernel() {
404 let k = PowerLawKernel::new(1.0, 1.0, 2.0);
405 assert_eq!(k.call(0.0), 1.0);
406 assert!(k.call(1.0) < 0.5);
407 assert_eq!(k.call(-0.5), 0.0);
408 }
409
410 #[test]
411 fn test_recursive_intensity_same_seed_match() {
412 use rand::SeedableRng;
416 use rand::rngs::StdRng;
417
418 let kernel = ExponentialKernel::new(2.0, 3.0);
419 let mu = 1.0;
420 let dt = 0.01;
421 let n_steps = 5000;
422 let seed = 42u64;
423
424 #[derive(Clone)]
425 struct NonRecursiveKernel(ExponentialKernel);
426 impl Kernel for NonRecursiveKernel {
427 fn call(&self, t: f64) -> f64 {
428 self.0.call(t)
429 }
430 }
431
432 let mut ha = HawkesProcess::new(mu, kernel.clone());
433 let mut rng_a = StdRng::seed_from_u64(seed);
434 let mut hb = HawkesProcess::new(mu, NonRecursiveKernel(kernel.clone()));
435 let mut rng_b = StdRng::seed_from_u64(seed);
436
437 let mut first_diverge = None;
438 for i in 0..n_steps {
439 let a = Simulatable::step(&mut ha, dt, &[], &mut rng_a);
440 let b = Simulatable::step(&mut hb, dt, &[], &mut rng_b);
441 if first_diverge.is_none() && (a - b).abs() > 1e-12 {
442 first_diverge = Some((i, a, b));
443 }
444 }
445
446 if let Some((i, a, b)) = first_diverge {
447 panic!(
448 "recursive and exact diverged at step {}: rec={:.15e}, exact={:.15e}",
449 i, a, b
450 );
451 }
452 }
453
454 #[test]
455 fn test_recursive_intensity_o_n_self_consistency() {
456 use rand::SeedableRng;
460 use rand::rngs::StdRng;
461
462 let kernel = ExponentialKernel::new(2.0, 3.0);
463 let mu = 1.0;
464 let dt = 0.01;
465 let n_steps = 2000;
466 let n_paths = 1000;
467
468 #[derive(Clone)]
469 struct NonRecursiveKernel(ExponentialKernel);
470 impl Kernel for NonRecursiveKernel {
471 fn call(&self, t: f64) -> f64 {
472 self.0.call(t)
473 }
474 }
475
476 let mut sum_a = 0.0f64;
477 let mut sum_b = 0.0f64;
478 let mut count = 0u64;
479
480 for path_i in 0u64..n_paths {
481 let seed_a = path_i;
482 let seed_b = path_i + 1_000_000;
483
484 let mut ha = HawkesProcess::new(mu, NonRecursiveKernel(kernel.clone()));
485 let mut rng_a = StdRng::seed_from_u64(seed_a);
486 for _ in 0..n_steps {
487 sum_a += Simulatable::step(&mut ha, dt, &[], &mut rng_a);
488 count += 1;
489 }
490
491 let mut hb = HawkesProcess::new(mu, NonRecursiveKernel(kernel.clone()));
492 let mut rng_b = StdRng::seed_from_u64(seed_b);
493 for _ in 0..n_steps {
494 sum_b += Simulatable::step(&mut hb, dt, &[], &mut rng_b);
495 }
496 }
497
498 let mean_a = sum_a / count as f64;
499 let mean_b = sum_b / count as f64;
500 let rel_diff = (mean_a - mean_b).abs() / mean_b.max(1e-12);
501
502 eprintln!(
503 "O(n) self-consistency: mean_a={:.6}, mean_b={:.6}, rel_diff={:.4}%",
504 mean_a,
505 mean_b,
506 rel_diff * 100.0
507 );
508
509 assert!(
514 rel_diff < 0.01,
515 "O(n) self-consistency: means differ by {:.4}%",
516 rel_diff * 100.0
517 );
518 }
519}