Skip to main content

oxiphysics_core/
stochastic_processes.rs

1#![allow(clippy::needless_range_loop)]
2// Copyright 2026 COOLJAPAN OU (Team KitaSan)
3// SPDX-License-Identifier: Apache-2.0
4
5//! Stochastic processes for simulation and financial/physical modeling.
6//!
7//! This module provides implementations of key stochastic processes used in
8//! computational physics, quantitative finance, and statistical modeling:
9//!
10//! - [`BrownianMotion`] — standard Wiener process path generation
11//! - [`GeometricBrownianMotion`] — log-normal GBM (Black-Scholes)
12//! - [`OrnsteinUhlenbeck`] — mean-reverting SDE (OU process)
13//! - [`JumpDiffusion`] — Merton model with Poisson jumps
14//! - [`FractionalBrownianMotion`] — fBm with Hurst exponent H
15//! - [`CoxIngersollRoss`] — CIR interest rate model (Milstein scheme)
16//! - [`HestonModel`] — stochastic volatility (Euler-Maruyama)
17//! - [`LevyStableProcess`] — stable distribution parameters
18//! - [`BranchingProcess`] — Galton-Watson branching process
19//! - [`MarkovChain`] — discrete-time Markov chain utilities
20//! - [`PoissonProcess`] — homogeneous and inhomogeneous Poisson
21//! - Variance reduction: [`antithetic_variates`], [`control_variate_estimator`], [`quasi_monte_carlo`]
22
23#![allow(dead_code)]
24#![allow(clippy::too_many_arguments)]
25
26use std::f64::consts::PI;
27
28// ─── Internal LCG RNG ────────────────────────────────────────────────────────
29
30/// Minimal linear congruential generator for reproducible stochastic simulations.
31///
32/// Uses Knuth multiplicative constants. Suitable for Monte Carlo work where
33/// external rand crate is not available.
34pub(crate) struct Lcg {
35    state: u64,
36}
37
38impl Lcg {
39    fn new(seed: u64) -> Self {
40        Self {
41            state: seed.wrapping_add(1),
42        }
43    }
44
45    fn next_u64(&mut self) -> u64 {
46        self.state = self
47            .state
48            .wrapping_mul(6_364_136_223_846_793_005)
49            .wrapping_add(1_442_695_040_888_963_407);
50        self.state
51    }
52
53    /// Uniform sample in \[0, 1).
54    fn uniform(&mut self) -> f64 {
55        (self.next_u64() >> 11) as f64 / (1u64 << 53) as f64
56    }
57
58    /// Standard normal via Box-Muller transform.
59    fn normal(&mut self) -> f64 {
60        let u1 = self.uniform().max(1e-300);
61        let u2 = self.uniform();
62        (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
63    }
64
65    /// Exponential with rate lambda.
66    fn exponential(&mut self, lambda: f64) -> f64 {
67        let u = self.uniform().max(1e-300);
68        -u.ln() / lambda
69    }
70
71    /// Poisson(lambda) sample via Knuth algorithm.
72    fn poisson(&mut self, lambda: f64) -> usize {
73        if lambda <= 0.0 {
74            return 0;
75        }
76        let threshold = (-lambda).exp();
77        let mut k = 0usize;
78        let mut p = 1.0_f64;
79        loop {
80            p *= self.uniform();
81            if p < threshold {
82                break;
83            }
84            k += 1;
85        }
86        k
87    }
88
89    /// Bernoulli(p) sample: returns true with probability p.
90    fn bernoulli(&mut self, p: f64) -> bool {
91        self.uniform() < p
92    }
93}
94
95// ─── BrownianMotion ──────────────────────────────────────────────────────────
96
97/// Standard Wiener process (Brownian motion) W(t).
98///
99/// Generates paths where increments dW ~ N(0, dt). This is the foundation
100/// of most continuous-time stochastic models.
101pub struct BrownianMotion {
102    /// Time step for path simulation.
103    pub dt: f64,
104    /// Diffusion coefficient (sigma). Default is 1.0 for standard BM.
105    pub sigma: f64,
106    seed: u64,
107}
108
109impl BrownianMotion {
110    /// Creates a new standard Brownian motion with given time step and seed.
111    pub fn new(dt: f64, seed: u64) -> Self {
112        Self {
113            dt,
114            sigma: 1.0,
115            seed,
116        }
117    }
118
119    /// Creates a scaled Brownian motion with diffusion coefficient sigma.
120    pub fn with_sigma(dt: f64, sigma: f64, seed: u64) -> Self {
121        Self { dt, sigma, seed }
122    }
123
124    /// Generate a single increment dW ~ N(0, sigma^2 * dt).
125    fn increment(&self, rng: &mut Lcg) -> f64 {
126        self.sigma * self.dt.sqrt() * rng.normal()
127    }
128
129    /// Generate a path of `n_steps` from W(0) = 0.
130    ///
131    /// Returns `n_steps + 1` values (including initial W = 0).
132    pub fn path(&self, n_steps: usize) -> Vec<f64> {
133        let mut rng = Lcg::new(self.seed);
134        let mut w = vec![0.0_f64; n_steps + 1];
135        for i in 1..=n_steps {
136            w[i] = w[i - 1] + self.increment(&mut rng);
137        }
138        w
139    }
140
141    /// Compute theoretical variance at time t = n_steps * dt.
142    ///
143    /// For standard BM, Var\[W(t)\] = sigma^2 * t.
144    pub fn variance_at(&self, t: f64) -> f64 {
145        self.sigma * self.sigma * t
146    }
147
148    /// Generate multiple paths for Monte Carlo. Returns matrix \[path_idx\]\[step\].
149    pub fn multi_path(&self, n_paths: usize, n_steps: usize) -> Vec<Vec<f64>> {
150        (0..n_paths)
151            .map(|i| {
152                let mut bm = BrownianMotion::with_sigma(
153                    self.dt,
154                    self.sigma,
155                    self.seed ^ (i as u64 * 2654435761),
156                );
157                bm.seed ^= i as u64;
158                bm.path(n_steps)
159            })
160            .collect()
161    }
162
163    /// Quadratic variation estimate for a path (should converge to sigma^2 * T).
164    pub fn quadratic_variation(path: &[f64]) -> f64 {
165        path.windows(2).map(|w| (w[1] - w[0]).powi(2)).sum()
166    }
167}
168
169// ─── GeometricBrownianMotion ─────────────────────────────────────────────────
170
171/// Geometric Brownian Motion (GBM) for log-normal asset/particle dynamics.
172///
173/// SDE: dS = mu * S * dt + sigma * S * dW
174/// Exact solution: S(t) = S0 * exp((mu - sigma^2/2)*t + sigma*W(t))
175pub struct GeometricBrownianMotion {
176    /// Initial value S(0).
177    pub s0: f64,
178    /// Drift coefficient mu.
179    pub mu: f64,
180    /// Volatility coefficient sigma.
181    pub sigma: f64,
182    seed: u64,
183}
184
185impl GeometricBrownianMotion {
186    /// Creates a new GBM process.
187    pub fn new(s0: f64, mu: f64, sigma: f64, seed: u64) -> Self {
188        Self {
189            s0,
190            mu,
191            sigma,
192            seed,
193        }
194    }
195
196    /// Exact path using the analytical solution (no discretisation error).
197    ///
198    /// Returns `n_steps + 1` values including S(0) = s0.
199    pub fn exact_path(&self, t_end: f64, n_steps: usize) -> Vec<f64> {
200        let dt = t_end / n_steps as f64;
201        let mut rng = Lcg::new(self.seed);
202        let mut path = vec![0.0_f64; n_steps + 1];
203        path[0] = self.s0;
204        let drift = (self.mu - 0.5 * self.sigma * self.sigma) * dt;
205        let vol = self.sigma * dt.sqrt();
206        let mut log_s = self.s0.ln();
207        for i in 1..=n_steps {
208            log_s += drift + vol * rng.normal();
209            path[i] = log_s.exp();
210        }
211        path
212    }
213
214    /// Euler-Maruyama discretisation path.
215    pub fn euler_path(&self, t_end: f64, n_steps: usize) -> Vec<f64> {
216        let dt = t_end / n_steps as f64;
217        let mut rng = Lcg::new(self.seed);
218        let mut path = vec![0.0_f64; n_steps + 1];
219        path[0] = self.s0;
220        for i in 1..=n_steps {
221            let s = path[i - 1];
222            let dw = dt.sqrt() * rng.normal();
223            path[i] = s + self.mu * s * dt + self.sigma * s * dw;
224            if path[i] < 0.0 {
225                path[i] = 0.0;
226            }
227        }
228        path
229    }
230
231    /// Theoretical mean E\[S(t)\] = S0 * exp(mu * t).
232    pub fn mean_at(&self, t: f64) -> f64 {
233        self.s0 * (self.mu * t).exp()
234    }
235
236    /// Theoretical variance Var\[S(t)\] = S0^2 * exp(2*mu*t) * (exp(sigma^2*t) - 1).
237    pub fn variance_at(&self, t: f64) -> f64 {
238        let e2mu = (2.0 * self.mu * t).exp();
239        let esig2 = (self.sigma * self.sigma * t).exp() - 1.0;
240        self.s0 * self.s0 * e2mu * esig2
241    }
242}
243
244// ─── OrnsteinUhlenbeck ───────────────────────────────────────────────────────
245
246/// Ornstein-Uhlenbeck mean-reverting process.
247///
248/// SDE: dX = theta*(mu - X)*dt + sigma*dW
249///
250/// Uses exact discretisation: X(t+dt) = X(t)*exp(-theta*dt) + mu*(1-exp(-theta*dt))
251///                                       + sigma*sqrt((1-exp(-2*theta*dt))/(2*theta)) * N(0,1)
252pub struct OrnsteinUhlenbeck {
253    /// Mean reversion speed (theta > 0).
254    pub theta: f64,
255    /// Long-run mean (mu).
256    pub mu: f64,
257    /// Volatility (sigma).
258    pub sigma: f64,
259    /// Initial value X(0).
260    pub x0: f64,
261    seed: u64,
262}
263
264impl OrnsteinUhlenbeck {
265    /// Creates a new OU process.
266    pub fn new(theta: f64, mu: f64, sigma: f64, x0: f64, seed: u64) -> Self {
267        Self {
268            theta,
269            mu,
270            sigma,
271            x0,
272            seed,
273        }
274    }
275
276    /// Exact simulation path using the conditional Gaussian formula.
277    pub fn path(&self, t_end: f64, n_steps: usize) -> Vec<f64> {
278        let dt = t_end / n_steps as f64;
279        let mut rng = Lcg::new(self.seed);
280        let mut path = vec![0.0_f64; n_steps + 1];
281        path[0] = self.x0;
282        let exp_neg = (-self.theta * dt).exp();
283        let var =
284            self.sigma * self.sigma * (1.0 - (-2.0 * self.theta * dt).exp()) / (2.0 * self.theta);
285        let std_dev = var.sqrt();
286        for i in 1..=n_steps {
287            let mean = path[i - 1] * exp_neg + self.mu * (1.0 - exp_neg);
288            path[i] = mean + std_dev * rng.normal();
289        }
290        path
291    }
292
293    /// Stationary variance: sigma^2 / (2*theta).
294    pub fn stationary_variance(&self) -> f64 {
295        self.sigma * self.sigma / (2.0 * self.theta)
296    }
297
298    /// Autocorrelation at lag tau: exp(-theta * tau).
299    pub fn autocorrelation(&self, tau: f64) -> f64 {
300        (-self.theta * tau).exp()
301    }
302}
303
304// ─── JumpDiffusion ───────────────────────────────────────────────────────────
305
306/// Merton jump-diffusion model: GBM plus compound Poisson jumps.
307///
308/// SDE: dS = (mu - lambda*k_bar)*S*dt + sigma*S*dW + S*(J-1)*dN
309/// where J = exp(N(mu_j, sigma_j^2)), k_bar = E\[J-1\] = exp(mu_j + sigma_j^2/2) - 1.
310pub struct JumpDiffusion {
311    /// Initial value.
312    pub s0: f64,
313    /// Drift (excluding jump compensation).
314    pub mu: f64,
315    /// Diffusion volatility.
316    pub sigma: f64,
317    /// Poisson intensity (average jumps per unit time).
318    pub lambda: f64,
319    /// Mean of log-jump size (normal distributed).
320    pub mu_j: f64,
321    /// Std dev of log-jump size.
322    pub sigma_j: f64,
323    seed: u64,
324}
325
326impl JumpDiffusion {
327    /// Creates a new Merton jump-diffusion model.
328    pub fn new(
329        s0: f64,
330        mu: f64,
331        sigma: f64,
332        lambda: f64,
333        mu_j: f64,
334        sigma_j: f64,
335        seed: u64,
336    ) -> Self {
337        Self {
338            s0,
339            mu,
340            sigma,
341            lambda,
342            mu_j,
343            sigma_j,
344            seed,
345        }
346    }
347
348    /// Expected jump multiplier E\[J\] = exp(mu_j + sigma_j^2/2).
349    pub fn expected_jump(&self) -> f64 {
350        (self.mu_j + 0.5 * self.sigma_j * self.sigma_j).exp()
351    }
352
353    /// Jump compensation term k_bar = E\[J\] - 1.
354    pub fn jump_compensation(&self) -> f64 {
355        self.expected_jump() - 1.0
356    }
357
358    /// Simulate a path using Euler-Maruyama with Poisson jump thinning.
359    pub fn path(&self, t_end: f64, n_steps: usize) -> Vec<f64> {
360        let dt = t_end / n_steps as f64;
361        let mut rng = Lcg::new(self.seed);
362        let mut path = vec![0.0_f64; n_steps + 1];
363        path[0] = self.s0;
364        let k_bar = self.jump_compensation();
365        let compensated_mu = self.mu - self.lambda * k_bar;
366        for i in 1..=n_steps {
367            let s = path[i - 1];
368            let dw = dt.sqrt() * rng.normal();
369            let n_jumps = rng.poisson(self.lambda * dt);
370            let mut jump_factor = 1.0_f64;
371            for _ in 0..n_jumps {
372                let log_j = self.mu_j + self.sigma_j * rng.normal();
373                jump_factor *= log_j.exp();
374            }
375            path[i] = s * (1.0 + compensated_mu * dt + self.sigma * dw) * jump_factor;
376            if path[i] < 0.0 {
377                path[i] = 0.0;
378            }
379        }
380        path
381    }
382}
383
384// ─── FractionalBrownianMotion ────────────────────────────────────────────────
385
386/// Fractional Brownian motion with Hurst exponent H in (0, 1).
387///
388/// H = 0.5 gives standard BM. H > 0.5 gives long-range dependence (persistent).
389/// H < 0.5 gives anti-persistent behaviour.
390///
391/// Uses the Cholesky method via exact covariance matrix for short paths,
392/// or the Wood-Chan circulant embedding for longer paths (approximation here).
393pub struct FractionalBrownianMotion {
394    /// Hurst exponent in (0, 1).
395    pub hurst: f64,
396    /// Scaling parameter.
397    pub sigma: f64,
398    seed: u64,
399}
400
401impl FractionalBrownianMotion {
402    /// Creates a new fBm with given Hurst exponent.
403    ///
404    /// # Panics
405    /// Panics if hurst is not in (0, 1).
406    pub fn new(hurst: f64, seed: u64) -> Self {
407        assert!(
408            hurst > 0.0 && hurst < 1.0,
409            "Hurst exponent must be in (0, 1)"
410        );
411        Self {
412            hurst,
413            sigma: 1.0,
414            seed,
415        }
416    }
417
418    /// Creates a scaled fBm.
419    pub fn with_sigma(hurst: f64, sigma: f64, seed: u64) -> Self {
420        assert!(
421            hurst > 0.0 && hurst < 1.0,
422            "Hurst exponent must be in (0, 1)"
423        );
424        Self { hurst, sigma, seed }
425    }
426
427    /// Covariance kernel: C(s, t) = sigma^2/2 * (|s|^{2H} + |t|^{2H} - |s-t|^{2H}).
428    pub fn covariance(&self, s: f64, t: f64) -> f64 {
429        let h2 = 2.0 * self.hurst;
430        0.5 * self.sigma
431            * self.sigma
432            * (s.abs().powf(h2) + t.abs().powf(h2) - (s - t).abs().powf(h2))
433    }
434
435    /// Generate a path on \[0, 1\] with n_steps points using Cholesky decomposition.
436    ///
437    /// This is exact but O(n^3) in memory/compute. Suitable for n <= ~500.
438    pub fn path(&self, n_steps: usize) -> Vec<f64> {
439        let n = n_steps + 1;
440        let dt = 1.0 / n_steps as f64;
441
442        // Build covariance matrix C
443        let times: Vec<f64> = (0..n).map(|i| i as f64 * dt).collect();
444        let mut cov = vec![0.0_f64; n * n];
445        for i in 0..n {
446            for j in 0..n {
447                cov[i * n + j] = self.covariance(times[i], times[j]);
448            }
449        }
450
451        // Cholesky decomposition L such that L*L^T = C
452        let mut l = vec![0.0_f64; n * n];
453        for i in 0..n {
454            for j in 0..=i {
455                let mut sum = cov[i * n + j];
456                for k in 0..j {
457                    sum -= l[i * n + k] * l[j * n + k];
458                }
459                if i == j {
460                    l[i * n + j] = sum.max(0.0).sqrt();
461                } else if l[j * n + j].abs() > 1e-14 {
462                    l[i * n + j] = sum / l[j * n + j];
463                }
464            }
465        }
466
467        // Sample z ~ N(0, I_n), then x = L*z
468        let mut rng = Lcg::new(self.seed);
469        let z: Vec<f64> = (0..n).map(|_| rng.normal()).collect();
470        let mut x = vec![0.0_f64; n];
471        for i in 0..n {
472            for j in 0..=i {
473                x[i] += l[i * n + j] * z[j];
474            }
475        }
476        x
477    }
478}
479
480// ─── CoxIngersollRoss ────────────────────────────────────────────────────────
481
482/// Cox-Ingersoll-Ross (CIR) interest rate model.
483///
484/// SDE: dr = kappa*(theta - r)*dt + sigma*sqrt(r)*dW
485///
486/// The Feller condition 2*kappa*theta >= sigma^2 ensures r stays positive.
487/// Milstein scheme is used for better accuracy.
488pub struct CoxIngersollRoss {
489    /// Mean reversion speed kappa.
490    pub kappa: f64,
491    /// Long-run mean theta.
492    pub theta: f64,
493    /// Volatility sigma.
494    pub sigma: f64,
495    /// Initial rate r(0).
496    pub r0: f64,
497    seed: u64,
498}
499
500impl CoxIngersollRoss {
501    /// Creates a new CIR process.
502    pub fn new(kappa: f64, theta: f64, sigma: f64, r0: f64, seed: u64) -> Self {
503        Self {
504            kappa,
505            theta,
506            sigma,
507            r0,
508            seed,
509        }
510    }
511
512    /// Whether the Feller condition 2*kappa*theta >= sigma^2 is satisfied.
513    pub fn feller_satisfied(&self) -> bool {
514        2.0 * self.kappa * self.theta >= self.sigma * self.sigma
515    }
516
517    /// Simulate path using Milstein scheme.
518    ///
519    /// The reflection r -> |r| is used after each step to guarantee non-negativity.
520    pub fn path(&self, t_end: f64, n_steps: usize) -> Vec<f64> {
521        let dt = t_end / n_steps as f64;
522        let mut rng = Lcg::new(self.seed);
523        let mut path = vec![0.0_f64; n_steps + 1];
524        path[0] = self.r0;
525        let sqrt_dt = dt.sqrt();
526        for i in 1..=n_steps {
527            let r = path[i - 1].max(0.0);
528            let dw = sqrt_dt * rng.normal();
529            let sqrt_r = r.sqrt();
530            // Milstein: dr = kappa*(theta-r)*dt + sigma*sqrt(r)*dW + sigma^2/4*(dW^2 - dt)
531            let milstein_correction = 0.25 * self.sigma * self.sigma * (dw * dw - dt);
532            path[i] = (r
533                + self.kappa * (self.theta - r) * dt
534                + self.sigma * sqrt_r * dw
535                + milstein_correction)
536                .abs();
537        }
538        path
539    }
540
541    /// Stationary mean E\[r\] = theta.
542    pub fn stationary_mean(&self) -> f64 {
543        self.theta
544    }
545
546    /// Stationary variance Var\[r\] = sigma^2 * theta / (2 * kappa).
547    pub fn stationary_variance(&self) -> f64 {
548        self.sigma * self.sigma * self.theta / (2.0 * self.kappa)
549    }
550}
551
552// ─── HestonModel ─────────────────────────────────────────────────────────────
553
554/// Heston stochastic volatility model.
555///
556/// System of SDEs:
557///   dS = mu*S*dt + sqrt(v)*S*dW_S
558///   dv = kappa*(theta - v)*dt + xi*sqrt(v)*dW_v
559///   Corr(dW_S, dW_v) = rho
560pub struct HestonModel {
561    /// Initial asset price.
562    pub s0: f64,
563    /// Initial variance.
564    pub v0: f64,
565    /// Drift of asset.
566    pub mu: f64,
567    /// Mean reversion speed of variance.
568    pub kappa: f64,
569    /// Long-run variance.
570    pub theta: f64,
571    /// Vol of vol.
572    pub xi: f64,
573    /// Correlation between asset and variance Brownians.
574    pub rho: f64,
575    seed: u64,
576}
577
578impl HestonModel {
579    /// Creates a new Heston model.
580    pub fn new(
581        s0: f64,
582        v0: f64,
583        mu: f64,
584        kappa: f64,
585        theta: f64,
586        xi: f64,
587        rho: f64,
588        seed: u64,
589    ) -> Self {
590        Self {
591            s0,
592            v0,
593            mu,
594            kappa,
595            theta,
596            xi,
597            rho,
598            seed,
599        }
600    }
601
602    /// Simulate correlated paths (S, v) using Euler-Maruyama.
603    ///
604    /// Returns `(price_path, variance_path)` each of length `n_steps + 1`.
605    pub fn path(&self, t_end: f64, n_steps: usize) -> (Vec<f64>, Vec<f64>) {
606        let dt = t_end / n_steps as f64;
607        let sqrt_dt = dt.sqrt();
608        let mut rng = Lcg::new(self.seed);
609        let mut s_path = vec![0.0_f64; n_steps + 1];
610        let mut v_path = vec![0.0_f64; n_steps + 1];
611        s_path[0] = self.s0;
612        v_path[0] = self.v0;
613        let rho2 = (1.0 - self.rho * self.rho).max(0.0).sqrt();
614        for i in 1..=n_steps {
615            let z1 = rng.normal();
616            let z2 = rng.normal();
617            let dw_v = sqrt_dt * z1;
618            let dw_s = sqrt_dt * (self.rho * z1 + rho2 * z2);
619            let v = v_path[i - 1].max(0.0);
620            let sqrt_v = v.sqrt();
621            v_path[i] = (v + self.kappa * (self.theta - v) * dt + self.xi * sqrt_v * dw_v).abs();
622            let s = s_path[i - 1];
623            s_path[i] = s + self.mu * s * dt + sqrt_v * s * dw_s;
624            if s_path[i] < 0.0 {
625                s_path[i] = 0.0;
626            }
627        }
628        (s_path, v_path)
629    }
630}
631
632// ─── LevyStableProcess ───────────────────────────────────────────────────────
633
634/// Lévy stable process with four parameters.
635///
636/// A Lévy stable distribution is characterised by (alpha, beta, c, mu) where:
637/// - alpha in (0, 2]: stability index (2 = Gaussian, 1 = Cauchy)
638/// - beta in \[-1, 1\]: skewness parameter
639/// - c > 0: scale parameter
640/// - mu: location parameter
641pub struct LevyStableProcess {
642    /// Stability index alpha in (0, 2].
643    pub alpha: f64,
644    /// Skewness beta in \[-1, 1\].
645    pub beta: f64,
646    /// Scale c > 0.
647    pub c: f64,
648    /// Location mu.
649    pub mu: f64,
650    seed: u64,
651}
652
653impl LevyStableProcess {
654    /// Creates a new Lévy stable process.
655    pub fn new(alpha: f64, beta: f64, c: f64, mu: f64, seed: u64) -> Self {
656        assert!(alpha > 0.0 && alpha <= 2.0, "alpha must be in (0, 2]");
657        assert!((-1.0..=1.0).contains(&beta), "beta must be in [-1, 1]");
658        assert!(c > 0.0, "scale c must be positive");
659        Self {
660            alpha,
661            beta,
662            c,
663            mu,
664            seed,
665        }
666    }
667
668    /// Sample from S(alpha, beta, 1, 0) using the Chambers-Mallows-Stuck algorithm.
669    fn sample_standard(&self, rng: &mut Lcg) -> f64 {
670        let u = PI * (rng.uniform() - 0.5);
671        let w = rng.exponential(1.0);
672        if (self.alpha - 1.0).abs() < 1e-10 {
673            // Cauchy case
674            let b_pi = self.beta * PI / 2.0;
675            (1.0 + self.beta * u.tan()) * u.tan() - self.beta * (b_pi.cos() / b_pi.cos()).ln()
676        } else {
677            let zeta = -self.beta * (PI * self.alpha / 2.0).tan();
678            let xi = zeta.atan() / self.alpha;
679            let term1 = (1.0 + zeta * zeta).powf(1.0 / (2.0 * self.alpha));
680            let term2 = (self.alpha * (u + xi)).sin()
681                / ((self.alpha * xi).cos() * u.cos()).powf(1.0 / self.alpha);
682            let term3 =
683                ((u - self.alpha * (u + xi)).cos() / w).powf((1.0 - self.alpha) / self.alpha);
684            term1 * term2 * term3
685        }
686    }
687
688    /// Generate a path of increments (not cumulative).
689    pub fn increments(&self, n: usize) -> Vec<f64> {
690        let mut rng = Lcg::new(self.seed);
691        (0..n)
692            .map(|_| self.mu + self.c * self.sample_standard(&mut rng))
693            .collect()
694    }
695
696    /// Generate a cumulative path (random walk with Lévy increments).
697    pub fn path(&self, n: usize) -> Vec<f64> {
698        let incs = self.increments(n);
699        let mut path = vec![0.0_f64; n + 1];
700        for i in 0..n {
701            path[i + 1] = path[i] + incs[i];
702        }
703        path
704    }
705}
706
707// ─── BranchingProcess ────────────────────────────────────────────────────────
708
709/// Galton-Watson branching process.
710///
711/// Each individual in generation n independently produces a random number of
712/// offspring in generation n+1 according to an offspring distribution.
713pub struct BranchingProcess {
714    /// Initial population Z(0).
715    pub z0: usize,
716    /// Mean offspring per individual.
717    pub mean_offspring: f64,
718    seed: u64,
719    /// Offspring distribution type.
720    dist: OffspringDist,
721}
722
723/// Supported offspring distributions for branching processes.
724#[derive(Clone, Copy)]
725pub enum OffspringDist {
726    /// Poisson(lambda) offspring.
727    Poisson(f64),
728    /// Geometric(p) offspring: P(k) = (1-p)^k * p.
729    Geometric(f64),
730    /// Bernoulli(p): 0 or 1 offspring.
731    Bernoulli(f64),
732}
733
734impl BranchingProcess {
735    /// Creates a new Galton-Watson process with Poisson offspring.
736    pub fn poisson(z0: usize, lambda: f64, seed: u64) -> Self {
737        Self {
738            z0,
739            mean_offspring: lambda,
740            seed,
741            dist: OffspringDist::Poisson(lambda),
742        }
743    }
744
745    /// Creates a new process with Geometric offspring.
746    pub fn geometric(z0: usize, p: f64, seed: u64) -> Self {
747        let mean = (1.0 - p) / p;
748        Self {
749            z0,
750            mean_offspring: mean,
751            seed,
752            dist: OffspringDist::Geometric(p),
753        }
754    }
755
756    /// Creates a new process with Bernoulli offspring.
757    pub fn bernoulli(z0: usize, p: f64, seed: u64) -> Self {
758        Self {
759            z0,
760            mean_offspring: p,
761            seed,
762            dist: OffspringDist::Bernoulli(p),
763        }
764    }
765
766    fn sample_offspring(&self, rng: &mut Lcg) -> usize {
767        match self.dist {
768            OffspringDist::Poisson(lambda) => rng.poisson(lambda),
769            OffspringDist::Geometric(p) => {
770                let u = rng.uniform().max(1e-300);
771                (u.ln() / (1.0 - p).ln()).floor() as usize
772            }
773            OffspringDist::Bernoulli(p) => rng.bernoulli(p) as usize,
774        }
775    }
776
777    /// Simulate the process for `n_generations` generations.
778    ///
779    /// Returns population sizes Z(0), Z(1), ..., Z(n_generations).
780    pub fn simulate(&self, n_generations: usize) -> Vec<usize> {
781        let mut rng = Lcg::new(self.seed);
782        let mut pop = vec![0usize; n_generations + 1];
783        pop[0] = self.z0;
784        for g in 0..n_generations {
785            let mut next_pop = 0usize;
786            for _ in 0..pop[g] {
787                next_pop = next_pop.saturating_add(self.sample_offspring(&mut rng));
788            }
789            pop[g + 1] = next_pop;
790        }
791        pop
792    }
793
794    /// Theoretical extinction probability for Poisson offspring.
795    ///
796    /// For Poisson(lambda) offspring:
797    /// - If lambda <= 1: extinction probability = 1.
798    /// - If lambda > 1: extinction probability = q where q = exp(lambda*(q-1)).
799    ///   Solved iteratively.
800    pub fn extinction_probability(&self) -> f64 {
801        if self.mean_offspring <= 1.0 {
802            return 1.0;
803        }
804        match self.dist {
805            OffspringDist::Poisson(lambda) => {
806                // Iterate q_{n+1} = exp(lambda*(q_n - 1)) starting from q_0 = 0
807                let mut q = 0.0_f64;
808                for _ in 0..1000 {
809                    let q_new = (lambda * (q - 1.0)).exp();
810                    if (q_new - q).abs() < 1e-12 {
811                        return q_new;
812                    }
813                    q = q_new;
814                }
815                q
816            }
817            OffspringDist::Geometric(p) => {
818                // Geometric: mean = (1-p)/p. Extinction prob = p/(1-p) if mean > 1, else 1
819                let mean = (1.0 - p) / p;
820                if mean <= 1.0 { 1.0 } else { p / (1.0 - p) }
821            }
822            OffspringDist::Bernoulli(p) => {
823                if p <= 0.5 {
824                    1.0
825                } else {
826                    (1.0 - p) / p
827                }
828            }
829        }
830    }
831
832    /// Estimate extinction probability by Monte Carlo simulation.
833    pub fn mc_extinction_probability(&self, n_paths: usize, n_generations: usize) -> f64 {
834        let mut extinct = 0usize;
835        for k in 0..n_paths {
836            let mut bp = BranchingProcess {
837                z0: self.z0,
838                mean_offspring: self.mean_offspring,
839                seed: self.seed ^ (k as u64 * 2654435761),
840                dist: self.dist,
841            };
842            bp.seed ^= k as u64;
843            let pop = bp.simulate(n_generations);
844            if *pop.last().unwrap_or(&1) == 0 {
845                extinct += 1;
846            }
847        }
848        extinct as f64 / n_paths as f64
849    }
850}
851
852// ─── MarkovChain ─────────────────────────────────────────────────────────────
853
854/// Discrete-time finite Markov chain.
855///
856/// Stores the transition matrix P where P\[i\]\[j\] = P(X_{n+1} = j | X_n = i).
857pub struct MarkovChain {
858    /// Transition matrix (n_states x n_states), row-major.
859    pub transition: Vec<Vec<f64>>,
860    /// Number of states.
861    pub n_states: usize,
862    seed: u64,
863}
864
865impl MarkovChain {
866    /// Creates a new Markov chain from a transition matrix.
867    ///
868    /// Rows must sum to 1 (stochastic matrix).
869    pub fn new(transition: Vec<Vec<f64>>, seed: u64) -> Self {
870        let n_states = transition.len();
871        Self {
872            transition,
873            n_states,
874            seed,
875        }
876    }
877
878    /// Sample next state given current state using the transition matrix.
879    fn next_state(&self, state: usize, rng: &mut Lcg) -> usize {
880        let u = rng.uniform();
881        let mut cumul = 0.0;
882        for (j, &p) in self.transition[state].iter().enumerate() {
883            cumul += p;
884            if u < cumul {
885                return j;
886            }
887        }
888        self.n_states - 1
889    }
890
891    /// Simulate a trajectory of length `n_steps` starting from `start_state`.
892    pub fn simulate(&self, start_state: usize, n_steps: usize) -> Vec<usize> {
893        let mut rng = Lcg::new(self.seed);
894        let mut traj = vec![0usize; n_steps + 1];
895        traj[0] = start_state;
896        for i in 1..=n_steps {
897            traj[i] = self.next_state(traj[i - 1], &mut rng);
898        }
899        traj
900    }
901
902    /// Compute stationary distribution by power iteration.
903    ///
904    /// Returns pi such that pi * P = pi with sum(pi) = 1.
905    pub fn stationary_distribution(&self) -> Vec<f64> {
906        let n = self.n_states;
907        let mut pi: Vec<f64> = vec![1.0 / n as f64; n];
908        for _ in 0..10_000 {
909            let mut pi_new = vec![0.0_f64; n];
910            for i in 0..n {
911                for j in 0..n {
912                    pi_new[j] += pi[i] * self.transition[i][j];
913                }
914            }
915            let sum: f64 = pi_new.iter().sum();
916            for x in &mut pi_new {
917                *x /= sum;
918            }
919            let diff: f64 = pi_new.iter().zip(&pi).map(|(a, b)| (a - b).abs()).sum();
920            pi = pi_new;
921            if diff < 1e-12 {
922                break;
923            }
924        }
925        pi
926    }
927
928    /// Estimate expected hitting time to `target_state` from `start_state` by simulation.
929    pub fn hitting_time(&self, start_state: usize, target_state: usize, n_samples: usize) -> f64 {
930        let mut rng = Lcg::new(self.seed ^ 0xdeadbeef);
931        let mut total = 0usize;
932        let max_steps = 100_000;
933        for _ in 0..n_samples {
934            let mut state = start_state;
935            for step in 1..=max_steps {
936                state = self.next_state(state, &mut rng);
937                if state == target_state {
938                    total += step;
939                    break;
940                }
941            }
942        }
943        total as f64 / n_samples as f64
944    }
945
946    /// Return the n-step transition matrix P^n.
947    pub fn n_step_matrix(&self, n: usize) -> Vec<Vec<f64>> {
948        let size = self.n_states;
949        // Start with identity
950        let mut result = vec![vec![0.0_f64; size]; size];
951        for i in 0..size {
952            result[i][i] = 1.0;
953        }
954        let mut base = self.transition.clone();
955        let mut power = n;
956        while power > 0 {
957            if power % 2 == 1 {
958                result = mat_mul(&result, &base, size);
959            }
960            base = mat_mul(&base, &base, size);
961            power /= 2;
962        }
963        result
964    }
965}
966
967/// Multiply two square matrices of given size.
968fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>], n: usize) -> Vec<Vec<f64>> {
969    let mut c = vec![vec![0.0_f64; n]; n];
970    for i in 0..n {
971        for k in 0..n {
972            if a[i][k].abs() < 1e-15 {
973                continue;
974            }
975            for j in 0..n {
976                c[i][j] += a[i][k] * b[k][j];
977            }
978        }
979    }
980    c
981}
982
983// ─── PoissonProcess ──────────────────────────────────────────────────────────
984
985/// Homogeneous and inhomogeneous Poisson processes.
986pub struct PoissonProcess {
987    /// Constant rate for homogeneous process.
988    pub rate: f64,
989    seed: u64,
990}
991
992impl PoissonProcess {
993    /// Creates a homogeneous Poisson process with constant rate.
994    pub fn new(rate: f64, seed: u64) -> Self {
995        Self { rate, seed }
996    }
997
998    /// Generate arrival times for a homogeneous Poisson process on \[0, t_end\].
999    ///
1000    /// Inter-arrival times are Exp(rate) distributed.
1001    pub fn arrivals(&self, t_end: f64) -> Vec<f64> {
1002        let mut rng = Lcg::new(self.seed);
1003        let mut times = Vec::new();
1004        let mut t = 0.0_f64;
1005        loop {
1006            let inter = rng.exponential(self.rate);
1007            t += inter;
1008            if t > t_end {
1009                break;
1010            }
1011            times.push(t);
1012        }
1013        times
1014    }
1015
1016    /// Count events in \[0, t_end\]. E\[N(t)\] = rate * t.
1017    pub fn count(&self, t_end: f64) -> usize {
1018        self.arrivals(t_end).len()
1019    }
1020
1021    /// Generate arrival times for an inhomogeneous Poisson process with intensity lambda(t)
1022    /// using Lewis-Shedler thinning. `lambda_max` must bound lambda(t) from above.
1023    pub fn inhomogeneous_arrivals<F>(&self, t_end: f64, lambda_max: f64, lambda_fn: F) -> Vec<f64>
1024    where
1025        F: Fn(f64) -> f64,
1026    {
1027        let mut rng = Lcg::new(self.seed ^ 0xabcdef12);
1028        let mut times = Vec::new();
1029        let mut t = 0.0_f64;
1030        loop {
1031            let inter = rng.exponential(lambda_max);
1032            t += inter;
1033            if t > t_end {
1034                break;
1035            }
1036            let accept_prob = lambda_fn(t) / lambda_max;
1037            if rng.uniform() < accept_prob {
1038                times.push(t);
1039            }
1040        }
1041        times
1042    }
1043
1044    /// Inter-arrival times for a homogeneous Poisson process (exactly n arrivals).
1045    pub fn inter_arrival_times(&self, n: usize) -> Vec<f64> {
1046        let mut rng = Lcg::new(self.seed ^ 0x12345678);
1047        (0..n).map(|_| rng.exponential(self.rate)).collect()
1048    }
1049}
1050
1051// ─── Variance Reduction Techniques ───────────────────────────────────────────
1052
1053/// Antithetic variates estimator.
1054///
1055/// For a Monte Carlo estimate E\[f(X)\], uses paired samples (U, 1-U) to
1056/// reduce variance. If f is monotone in X, this halves the variance.
1057///
1058/// `evaluator` is called with `n_pairs` pairs (x, x_antithetic).
1059/// Returns (estimate, variance_reduction_factor).
1060pub fn antithetic_variates<F>(evaluator: F, n_pairs: usize, seed: u64) -> (f64, f64)
1061where
1062    F: Fn(f64, f64) -> f64,
1063{
1064    let mut rng = Lcg::new(seed);
1065    let mut sum = 0.0_f64;
1066    let mut sum_sq = 0.0_f64;
1067    for _ in 0..n_pairs {
1068        let u = rng.uniform();
1069        let val = (evaluator(u, 1.0 - u) + evaluator(1.0 - u, u)) / 2.0;
1070        sum += val;
1071        sum_sq += val * val;
1072    }
1073    let mean = sum / n_pairs as f64;
1074    let variance = (sum_sq / n_pairs as f64 - mean * mean).max(0.0);
1075    (mean, variance)
1076}
1077
1078/// Control variate estimator.
1079///
1080/// Reduces variance by exploiting a correlated control variate C with known mean mu_c.
1081/// Optimal coefficient: b* = Cov(Y, C) / Var(C)
1082/// Estimator: Y_cv = Y - b*(C - mu_c)
1083///
1084/// `pairs`: slice of (Y_i, C_i) samples.
1085/// `mu_c`: known expectation of C.
1086/// Returns corrected estimate.
1087pub fn control_variate_estimator(pairs: &[(f64, f64)], mu_c: f64) -> f64 {
1088    let n = pairs.len() as f64;
1089    let mean_y: f64 = pairs.iter().map(|(y, _)| y).sum::<f64>() / n;
1090    let mean_c: f64 = pairs.iter().map(|(_, c)| c).sum::<f64>() / n;
1091    let cov_yc: f64 = pairs
1092        .iter()
1093        .map(|(y, c)| (y - mean_y) * (c - mean_c))
1094        .sum::<f64>()
1095        / n;
1096    let var_c: f64 = pairs.iter().map(|(_, c)| (c - mean_c).powi(2)).sum::<f64>() / n;
1097    if var_c.abs() < 1e-15 {
1098        return mean_y;
1099    }
1100    let b = cov_yc / var_c;
1101    mean_y - b * (mean_c - mu_c)
1102}
1103
1104/// Quasi-Monte Carlo: Halton sequence in base 2 and 3 for 2D integration.
1105///
1106/// Generates `n` low-discrepancy points in \[0,1)^2 using Halton sequence.
1107/// Much better convergence than pseudo-random for smooth integrands.
1108pub fn quasi_monte_carlo<F>(integrand: F, n: usize) -> f64
1109where
1110    F: Fn(f64, f64) -> f64,
1111{
1112    let sum: f64 = (1..=n).map(|i| integrand(halton(i, 2), halton(i, 3))).sum();
1113    sum / n as f64
1114}
1115
1116/// Compute the i-th element of the Halton sequence in the given base.
1117pub fn halton(mut i: usize, base: usize) -> f64 {
1118    let mut result = 0.0_f64;
1119    let mut denominator = 1.0_f64;
1120    while i > 0 {
1121        denominator *= base as f64;
1122        result += (i % base) as f64 / denominator;
1123        i /= base;
1124    }
1125    result
1126}
1127
1128// ─── Tests ────────────────────────────────────────────────────────────────────
1129
1130#[cfg(test)]
1131mod tests {
1132    use super::*;
1133
1134    // ── BrownianMotion tests ──────────────────────────────────────────────────
1135
1136    #[test]
1137    fn test_bm_path_length() {
1138        let bm = BrownianMotion::new(0.01, 42);
1139        let path = bm.path(100);
1140        assert_eq!(path.len(), 101);
1141    }
1142
1143    #[test]
1144    fn test_bm_starts_at_zero() {
1145        let bm = BrownianMotion::new(0.01, 42);
1146        let path = bm.path(100);
1147        assert_eq!(path[0], 0.0);
1148    }
1149
1150    #[test]
1151    fn test_bm_variance_grows_with_time() {
1152        // Generate many paths and verify that E[W(t)^2] ≈ sigma^2 * t
1153        let sigma = 1.0;
1154        let dt = 0.1;
1155        let n_steps = 10;
1156        let t = n_steps as f64 * dt;
1157        let n_paths = 5000;
1158        let mut sum_sq = 0.0_f64;
1159        for k in 0..n_paths {
1160            let bm = BrownianMotion::with_sigma(dt, sigma, k as u64 * 1234 + 1);
1161            let path = bm.path(n_steps);
1162            let w_t = path[n_steps];
1163            sum_sq += w_t * w_t;
1164        }
1165        let empirical_var = sum_sq / n_paths as f64;
1166        let theoretical_var = sigma * sigma * t;
1167        assert!(
1168            (empirical_var - theoretical_var).abs() < 0.15,
1169            "BM variance: empirical={:.6} theoretical={:.6}",
1170            empirical_var,
1171            theoretical_var
1172        );
1173    }
1174
1175    #[test]
1176    fn test_bm_quadratic_variation_converges() {
1177        // Quadratic variation should converge to sigma^2 * T as dt -> 0
1178        let bm = BrownianMotion::with_sigma(0.001, 2.0, 7777);
1179        let path = bm.path(1000);
1180        let qv = BrownianMotion::quadratic_variation(&path);
1181        // sigma^2 * T = 4 * 1.0 = 4.0
1182        assert!(
1183            (qv - 4.0).abs() < 1.0,
1184            "QV should be near 4.0, got {:.6}",
1185            qv
1186        );
1187    }
1188
1189    #[test]
1190    fn test_bm_multi_path_count() {
1191        let bm = BrownianMotion::new(0.01, 1);
1192        let paths = bm.multi_path(10, 50);
1193        assert_eq!(paths.len(), 10);
1194        for path in &paths {
1195            assert_eq!(path.len(), 51);
1196        }
1197    }
1198
1199    #[test]
1200    fn test_bm_theoretical_variance() {
1201        let bm = BrownianMotion::with_sigma(0.01, 3.0, 1);
1202        let var = bm.variance_at(2.0);
1203        assert!((var - 18.0).abs() < 1e-10); // sigma^2 * t = 9 * 2 = 18
1204    }
1205
1206    // ── GBM tests ─────────────────────────────────────────────────────────────
1207
1208    #[test]
1209    fn test_gbm_starts_at_s0() {
1210        let gbm = GeometricBrownianMotion::new(100.0, 0.05, 0.2, 99);
1211        let path = gbm.exact_path(1.0, 100);
1212        assert!((path[0] - 100.0).abs() < 1e-10);
1213    }
1214
1215    #[test]
1216    fn test_gbm_stays_positive() {
1217        let gbm = GeometricBrownianMotion::new(10.0, 0.1, 0.5, 55);
1218        let path = gbm.exact_path(2.0, 200);
1219        assert!(path.iter().all(|&s| s >= 0.0));
1220    }
1221
1222    #[test]
1223    fn test_gbm_mean_convergence() {
1224        // E[S(T)] = S0 * exp(mu * T)
1225        let s0 = 100.0;
1226        let mu = 0.10;
1227        let sigma = 0.2;
1228        let t_end = 1.0;
1229        let n_steps = 252;
1230        let n_paths = 5000;
1231        let mut sum = 0.0_f64;
1232        for k in 0..n_paths {
1233            let gbm = GeometricBrownianMotion::new(s0, mu, sigma, k as u64 + 1);
1234            let path = gbm.exact_path(t_end, n_steps);
1235            sum += path[n_steps];
1236        }
1237        let empirical_mean = sum / n_paths as f64;
1238        let theoretical_mean = s0 * (mu * t_end).exp();
1239        let rel_err = (empirical_mean - theoretical_mean).abs() / theoretical_mean;
1240        assert!(rel_err < 0.05, "GBM mean rel error: {:.6}", rel_err);
1241    }
1242
1243    #[test]
1244    fn test_gbm_log_normality() {
1245        // Log-returns should be approximately normal
1246        let gbm = GeometricBrownianMotion::new(1.0, 0.0, 1.0, 123);
1247        let path = gbm.exact_path(1.0, 1000);
1248        let log_return = path[1000].ln();
1249        // Should be finite
1250        assert!(log_return.is_finite());
1251    }
1252
1253    #[test]
1254    fn test_gbm_euler_stays_positive() {
1255        let gbm = GeometricBrownianMotion::new(50.0, 0.05, 0.3, 333);
1256        let path = gbm.euler_path(1.0, 100);
1257        assert!(path.iter().all(|&s| s >= 0.0));
1258    }
1259
1260    // ── OrnsteinUhlenbeck tests ───────────────────────────────────────────────
1261
1262    #[test]
1263    fn test_ou_starts_at_x0() {
1264        let ou = OrnsteinUhlenbeck::new(2.0, 0.5, 0.3, 1.0, 42);
1265        let path = ou.path(1.0, 100);
1266        assert!((path[0] - 1.0).abs() < 1e-10);
1267    }
1268
1269    #[test]
1270    fn test_ou_mean_reversion() {
1271        // Long-run mean should converge to mu
1272        let theta = 5.0;
1273        let mu = 3.0;
1274        let ou = OrnsteinUhlenbeck::new(theta, mu, 0.5, 10.0, 77);
1275        let path = ou.path(10.0, 10000);
1276        let tail_mean: f64 = path[8000..].iter().sum::<f64>() / 2000.0;
1277        assert!(
1278            (tail_mean - mu).abs() < 0.3,
1279            "OU mean: {:.6} vs {:.6}",
1280            tail_mean,
1281            mu
1282        );
1283    }
1284
1285    #[test]
1286    fn test_ou_stationary_variance() {
1287        let ou = OrnsteinUhlenbeck::new(2.0, 0.0, 1.0, 0.0, 1);
1288        let sv = ou.stationary_variance();
1289        // sigma^2/(2*theta) = 1/4 = 0.25
1290        assert!((sv - 0.25).abs() < 1e-10);
1291    }
1292
1293    #[test]
1294    fn test_ou_autocorrelation_decay() {
1295        let ou = OrnsteinUhlenbeck::new(1.0, 0.0, 1.0, 0.0, 1);
1296        let ac0 = ou.autocorrelation(0.0);
1297        let ac1 = ou.autocorrelation(1.0);
1298        assert!((ac0 - 1.0).abs() < 1e-10);
1299        assert!((ac1 - (-1.0_f64).exp()).abs() < 1e-10);
1300    }
1301
1302    // ── JumpDiffusion tests ───────────────────────────────────────────────────
1303
1304    #[test]
1305    fn test_jump_diffusion_starts_at_s0() {
1306        let jd = JumpDiffusion::new(100.0, 0.05, 0.2, 1.0, -0.1, 0.2, 42);
1307        let path = jd.path(1.0, 100);
1308        assert!((path[0] - 100.0).abs() < 1e-10);
1309    }
1310
1311    #[test]
1312    fn test_jump_diffusion_stays_nonneg() {
1313        let jd = JumpDiffusion::new(50.0, 0.05, 0.3, 2.0, 0.0, 0.3, 7);
1314        let path = jd.path(1.0, 252);
1315        assert!(path.iter().all(|&s| s >= 0.0));
1316    }
1317
1318    #[test]
1319    fn test_jump_diffusion_compensation() {
1320        let jd = JumpDiffusion::new(1.0, 0.0, 0.1, 1.0, 0.0, 0.1, 1);
1321        let comp = jd.jump_compensation();
1322        // E[J] = exp(0 + 0.01/2) = exp(0.005) ≈ 1.005012
1323        let expected = (0.0_f64 + 0.5 * 0.01_f64).exp() - 1.0;
1324        assert!((comp - expected).abs() < 1e-10);
1325    }
1326
1327    // ── FractionalBrownianMotion tests ────────────────────────────────────────
1328
1329    #[test]
1330    fn test_fbm_path_length() {
1331        let fbm = FractionalBrownianMotion::new(0.7, 1);
1332        let path = fbm.path(10);
1333        assert_eq!(path.len(), 11);
1334    }
1335
1336    #[test]
1337    fn test_fbm_starts_at_zero() {
1338        let fbm = FractionalBrownianMotion::new(0.5, 42);
1339        let path = fbm.path(20);
1340        assert!(path[0].abs() < 1e-10);
1341    }
1342
1343    #[test]
1344    fn test_fbm_hurst_validation() {
1345        let result = std::panic::catch_unwind(|| FractionalBrownianMotion::new(1.5, 1));
1346        assert!(result.is_err());
1347    }
1348
1349    #[test]
1350    fn test_fbm_covariance_positive_definite() {
1351        let fbm = FractionalBrownianMotion::new(0.7, 1);
1352        let cov = fbm.covariance(1.0, 1.0);
1353        // C(t,t) = sigma^2 * t^{2H} > 0
1354        assert!(cov > 0.0);
1355    }
1356
1357    // ── CoxIngersollRoss tests ────────────────────────────────────────────────
1358
1359    #[test]
1360    fn test_cir_stays_positive() {
1361        let cir = CoxIngersollRoss::new(2.0, 0.05, 0.1, 0.04, 99);
1362        let path = cir.path(10.0, 1000);
1363        assert!(path.iter().all(|&r| r >= 0.0));
1364    }
1365
1366    #[test]
1367    fn test_cir_feller_condition() {
1368        // 2*kappa*theta >= sigma^2 ensures strict positivity
1369        let cir = CoxIngersollRoss::new(2.0, 0.05, 0.1, 0.04, 1);
1370        // 2*2.0*0.05 = 0.2 >= 0.01 -> satisfied
1371        assert!(cir.feller_satisfied());
1372    }
1373
1374    #[test]
1375    fn test_cir_mean_reversion() {
1376        let theta = 0.05;
1377        let cir = CoxIngersollRoss::new(3.0, theta, 0.1, 0.2, 55);
1378        let path = cir.path(20.0, 20000);
1379        let tail_mean: f64 = path[15000..].iter().sum::<f64>() / 5000.0;
1380        assert!(
1381            (tail_mean - theta).abs() < 0.01,
1382            "CIR mean: {:.6} vs {:.6}",
1383            tail_mean,
1384            theta
1385        );
1386    }
1387
1388    #[test]
1389    fn test_cir_stationary_variance() {
1390        let cir = CoxIngersollRoss::new(2.0, 0.05, 0.1, 0.04, 1);
1391        let sv = cir.stationary_variance();
1392        // sigma^2 * theta / (2 * kappa) = 0.01 * 0.05 / 4 = 0.000125
1393        assert!((sv - 0.000125).abs() < 1e-10);
1394    }
1395
1396    // ── HestonModel tests ─────────────────────────────────────────────────────
1397
1398    #[test]
1399    fn test_heston_path_lengths() {
1400        let heston = HestonModel::new(100.0, 0.04, 0.05, 2.0, 0.04, 0.3, -0.7, 11);
1401        let (s, v) = heston.path(1.0, 100);
1402        assert_eq!(s.len(), 101);
1403        assert_eq!(v.len(), 101);
1404    }
1405
1406    #[test]
1407    fn test_heston_starts_correctly() {
1408        let heston = HestonModel::new(100.0, 0.04, 0.05, 2.0, 0.04, 0.3, -0.7, 11);
1409        let (s, v) = heston.path(1.0, 100);
1410        assert!((s[0] - 100.0).abs() < 1e-10);
1411        assert!((v[0] - 0.04).abs() < 1e-10);
1412    }
1413
1414    #[test]
1415    fn test_heston_variance_stays_nonneg() {
1416        let heston = HestonModel::new(50.0, 0.1, 0.05, 1.0, 0.1, 0.5, -0.5, 77);
1417        let (_, v) = heston.path(2.0, 500);
1418        assert!(v.iter().all(|&vi| vi >= 0.0));
1419    }
1420
1421    // ── LevyStableProcess tests ───────────────────────────────────────────────
1422
1423    #[test]
1424    fn test_levy_path_length() {
1425        let levy = LevyStableProcess::new(1.5, 0.0, 1.0, 0.0, 42);
1426        let path = levy.path(100);
1427        assert_eq!(path.len(), 101);
1428    }
1429
1430    #[test]
1431    fn test_levy_path_starts_at_zero() {
1432        let levy = LevyStableProcess::new(1.8, 0.0, 1.0, 0.0, 1);
1433        let path = levy.path(50);
1434        assert_eq!(path[0], 0.0);
1435    }
1436
1437    #[test]
1438    fn test_levy_gaussian_case_finite() {
1439        // alpha=2 gives Gaussian
1440        let levy = LevyStableProcess::new(2.0, 0.0, 1.0, 0.0, 88);
1441        let incs = levy.increments(1000);
1442        assert!(incs.iter().all(|x| x.is_finite()));
1443    }
1444
1445    #[test]
1446    fn test_levy_alpha_validation() {
1447        let result = std::panic::catch_unwind(|| LevyStableProcess::new(0.0, 0.0, 1.0, 0.0, 1));
1448        assert!(result.is_err());
1449    }
1450
1451    // ── BranchingProcess tests ────────────────────────────────────────────────
1452
1453    #[test]
1454    fn test_branching_path_length() {
1455        let bp = BranchingProcess::poisson(10, 1.5, 42);
1456        let pop = bp.simulate(20);
1457        assert_eq!(pop.len(), 21);
1458    }
1459
1460    #[test]
1461    fn test_branching_starts_at_z0() {
1462        let bp = BranchingProcess::poisson(5, 1.2, 1);
1463        let pop = bp.simulate(10);
1464        assert_eq!(pop[0], 5);
1465    }
1466
1467    #[test]
1468    fn test_branching_subcritical_extinction() {
1469        // lambda < 1: extinction probability = 1
1470        let bp = BranchingProcess::poisson(1, 0.5, 100);
1471        let q = bp.extinction_probability();
1472        assert!((q - 1.0).abs() < 1e-10);
1473    }
1474
1475    #[test]
1476    fn test_branching_supercritical_extinction_less_than_one() {
1477        // lambda > 1: extinction probability < 1
1478        let bp = BranchingProcess::poisson(1, 2.0, 42);
1479        let q = bp.extinction_probability();
1480        assert!(q < 1.0 && q > 0.0);
1481    }
1482
1483    #[test]
1484    fn test_branching_mc_extinction_subcritical() {
1485        // Sub-critical should go extinct with high probability
1486        let bp = BranchingProcess::poisson(1, 0.5, 42);
1487        let q = bp.mc_extinction_probability(500, 50);
1488        assert!(
1489            q > 0.8,
1490            "Subcritical extinction prob should be high, got {:.6}",
1491            q
1492        );
1493    }
1494
1495    // ── MarkovChain tests ─────────────────────────────────────────────────────
1496
1497    fn two_state_chain(seed: u64) -> MarkovChain {
1498        MarkovChain::new(vec![vec![0.7, 0.3], vec![0.4, 0.6]], seed)
1499    }
1500
1501    #[test]
1502    fn test_markov_simulate_length() {
1503        let mc = two_state_chain(1);
1504        let traj = mc.simulate(0, 100);
1505        assert_eq!(traj.len(), 101);
1506    }
1507
1508    #[test]
1509    fn test_markov_states_in_range() {
1510        let mc = two_state_chain(5);
1511        let traj = mc.simulate(0, 1000);
1512        assert!(traj.iter().all(|&s| s < 2));
1513    }
1514
1515    #[test]
1516    fn test_markov_stationary_distribution() {
1517        // For P = [[0.7, 0.3],[0.4, 0.6]], stationary: pi[0] = 4/7, pi[1] = 3/7
1518        let mc = two_state_chain(1);
1519        let pi = mc.stationary_distribution();
1520        let expected = [4.0 / 7.0, 3.0 / 7.0];
1521        assert!((pi[0] - expected[0]).abs() < 1e-6, "pi[0]={:.8}", pi[0]);
1522        assert!((pi[1] - expected[1]).abs() < 1e-6, "pi[1]={:.8}", pi[1]);
1523    }
1524
1525    #[test]
1526    fn test_markov_stationary_sums_to_one() {
1527        let mc = two_state_chain(7);
1528        let pi = mc.stationary_distribution();
1529        let sum: f64 = pi.iter().sum();
1530        assert!((sum - 1.0).abs() < 1e-10);
1531    }
1532
1533    #[test]
1534    fn test_markov_n_step_matrix() {
1535        let mc = two_state_chain(1);
1536        let p1 = mc.n_step_matrix(1);
1537        // Should match transition matrix
1538        assert!((p1[0][0] - 0.7).abs() < 1e-10);
1539        assert!((p1[0][1] - 0.3).abs() < 1e-10);
1540    }
1541
1542    #[test]
1543    fn test_markov_hitting_time_positive() {
1544        let mc = two_state_chain(42);
1545        let ht = mc.hitting_time(0, 1, 100);
1546        assert!(ht > 0.0);
1547    }
1548
1549    // ── PoissonProcess tests ──────────────────────────────────────────────────
1550
1551    #[test]
1552    fn test_poisson_arrivals_ordered() {
1553        let pp = PoissonProcess::new(5.0, 1234);
1554        let arrivals = pp.arrivals(10.0);
1555        for i in 1..arrivals.len() {
1556            assert!(arrivals[i] > arrivals[i - 1]);
1557        }
1558    }
1559
1560    #[test]
1561    fn test_poisson_arrivals_in_range() {
1562        let pp = PoissonProcess::new(3.0, 99);
1563        let arrivals = pp.arrivals(5.0);
1564        assert!(arrivals.iter().all(|&t| t > 0.0 && t <= 5.0));
1565    }
1566
1567    #[test]
1568    fn test_poisson_mean_count() {
1569        // E[N(t)] = lambda * t
1570        let rate = 4.0;
1571        let t = 10.0;
1572        let n_samples = 2000;
1573        let mut total = 0usize;
1574        for k in 0..n_samples {
1575            let pp = PoissonProcess::new(rate, k as u64 + 1);
1576            total += pp.count(t);
1577        }
1578        let empirical_mean = total as f64 / n_samples as f64;
1579        let expected = rate * t;
1580        assert!(
1581            (empirical_mean - expected).abs() < 2.0,
1582            "Poisson mean: {:.6} vs {:.6}",
1583            empirical_mean,
1584            expected
1585        );
1586    }
1587
1588    #[test]
1589    fn test_poisson_inter_arrival_exponential() {
1590        // Inter-arrival times should be exponential(rate)
1591        // Mean should be 1/rate
1592        let rate = 2.0;
1593        let n = 10000;
1594        let pp = PoissonProcess::new(rate, 42);
1595        let iat = pp.inter_arrival_times(n);
1596        let mean: f64 = iat.iter().sum::<f64>() / n as f64;
1597        let expected_mean = 1.0 / rate;
1598        assert!(
1599            (mean - expected_mean).abs() < 0.05,
1600            "Mean inter-arrival: {:.6} vs {:.6}",
1601            mean,
1602            expected_mean
1603        );
1604    }
1605
1606    #[test]
1607    fn test_poisson_inhomogeneous() {
1608        // Inhomogeneous with constant lambda should match homogeneous
1609        let pp = PoissonProcess::new(2.0, 1);
1610        let arrivals = pp.inhomogeneous_arrivals(10.0, 2.0, |_t| 2.0);
1611        assert!(arrivals.iter().all(|&t| (0.0..=10.0).contains(&t)));
1612    }
1613
1614    // ── Variance reduction tests ──────────────────────────────────────────────
1615
1616    #[test]
1617    fn test_antithetic_variates_constant() {
1618        // For f(u, u_anti) = 1 (constant), estimate should be 1
1619        let (mean, _var) = antithetic_variates(|_u, _u_anti| 1.0, 1000, 42);
1620        assert!((mean - 1.0).abs() < 1e-10);
1621    }
1622
1623    #[test]
1624    fn test_antithetic_variates_uniform_mean() {
1625        // E[U] = 0.5 where f(u, u_anti) = u
1626        let (mean, _var) = antithetic_variates(|u, _| u, 5000, 7);
1627        assert!((mean - 0.5).abs() < 0.02, "Antithetic mean: {:.6}", mean);
1628    }
1629
1630    #[test]
1631    fn test_control_variate_identity() {
1632        // If Y = C with known mean, estimator should return mu_c
1633        let pairs: Vec<(f64, f64)> = (1..=100).map(|i| (i as f64, i as f64)).collect();
1634        let mu_c = 50.5_f64;
1635        let est = control_variate_estimator(&pairs, mu_c);
1636        assert!((est - mu_c).abs() < 0.01, "CV estimate: {:.6}", est);
1637    }
1638
1639    #[test]
1640    fn test_quasi_monte_carlo_unit_square() {
1641        // Integral of 1 over [0,1]^2 = 1
1642        let est = quasi_monte_carlo(|_x, _y| 1.0, 1000);
1643        assert!((est - 1.0).abs() < 1e-10);
1644    }
1645
1646    #[test]
1647    fn test_quasi_monte_carlo_linear() {
1648        // Integral of (x + y) over [0,1]^2 = 1.0
1649        let est = quasi_monte_carlo(|x, y| x + y, 10000);
1650        assert!((est - 1.0).abs() < 0.01, "QMC estimate: {:.6}", est);
1651    }
1652
1653    #[test]
1654    fn test_halton_sequence_base2() {
1655        // First few Halton base 2: 1/2, 1/4, 3/4, 1/8, ...
1656        assert!((halton(1, 2) - 0.5).abs() < 1e-10);
1657        assert!((halton(2, 2) - 0.25).abs() < 1e-10);
1658        assert!((halton(3, 2) - 0.75).abs() < 1e-10);
1659    }
1660
1661    #[test]
1662    fn test_halton_in_unit_interval() {
1663        for i in 1..=1000 {
1664            let h = halton(i, 2);
1665            assert!((0.0..1.0).contains(&h), "Halton({}, 2) = {:.6}", i, h);
1666        }
1667    }
1668}