use crate::rng::Pcg;
#[inline]
pub fn step_coord(f: f64, sigma: f64, dt: f64, dx: f64, rng: &mut Pcg) -> f64 {
let (p_minus, p_plus) = probs(f, sigma, dt, dx);
let u = rng.f64();
if u < p_minus {
-dx
} else if u < p_minus + p_plus {
dx
} else {
0.0
}
}
#[inline]
pub fn probs(f: f64, sigma: f64, dt: f64, dx: f64) -> (f64, f64) {
let s2 = (sigma * sigma).min(dx * dx / dt);
let fmax = s2 / dx;
let fc = f.clamp(-fmax, fmax);
let base = 0.5 * (dt / dx) * (s2 / dx);
let tilt = 0.5 * (dt / dx) * fc;
(base - tilt, base + tilt)
}
pub fn integrate<D, S>(
x: &mut [f64],
t0: f64,
dt: f64,
dx_lat: &[f64],
n_steps: usize,
drift: D,
sigma: S,
rng: &mut Pcg,
) where
D: Fn(&[f64], f64, usize) -> f64,
S: Fn(&[f64], f64, usize) -> f64,
{
let d = x.len();
let mut incr = vec![0.0; d];
let mut t = t0;
for _ in 0..n_steps {
for i in 0..d {
incr[i] = step_coord(drift(x, t, i), sigma(x, t, i), dt, dx_lat[i], rng);
}
for i in 0..d {
x[i] += incr[i];
}
t += dt;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_moment_identities() {
let mut rng = Pcg::new(0x11A7, 9);
for _ in 0..1000 {
let sigma = 0.2 + rng.f64() * 2.0;
let dt = 0.001 + rng.f64() * 0.05;
let dx = (dt.sqrt() * sigma) * (1.0 + rng.f64());
let fmax = sigma * sigma / dx;
let f = (rng.f64() * 2.0 - 1.0) * 0.9 * fmax;
let (pm, pp) = probs(f, sigma, dt, dx);
let mean = pp * dx - pm * dx;
let second = (pp + pm) * dx * dx;
assert!((mean - dt * f).abs() < 1e-14, "E[Delta] {} vs {}", mean, dt * f);
assert!(
(second - dt * sigma * sigma).abs() < 1e-13,
"E[Delta^2] {} vs {}",
second,
dt * sigma * sigma
);
let dxb = dt.sqrt() * sigma;
let fb = (rng.f64() * 2.0 - 1.0) * 0.9 * (sigma * sigma / dxb);
let (bm, bp) = probs(fb, sigma, dt, dxb);
assert!((bm + bp - 1.0).abs() < 1e-12, "binary: p-+p+ = {}", bm + bp);
}
}
#[test]
fn constant_drift_law() {
let (c, sigma, dt): (f64, f64, f64) = (0.7, 1.3, 0.01);
let dx = dt.sqrt() * sigma;
let n_steps = 2000usize;
let n_paths = 4000usize;
let mut rng = Pcg::new(0xC0157, 2);
let (mut sum, mut sum2) = (0.0, 0.0);
for _ in 0..n_paths {
let mut x = [0.0f64];
integrate(&mut x, 0.0, dt, &[dx], n_steps, |_, _, _| c, |_, _, _| sigma, &mut rng);
sum += x[0];
sum2 += x[0] * x[0];
}
let mean = sum / n_paths as f64;
let var = sum2 / n_paths as f64 - mean * mean;
let want_mean = n_steps as f64 * dt * c;
let want_var = n_steps as f64 * (dt * sigma * sigma - dt * dt * c * c);
let se_mean = (want_var / n_paths as f64).sqrt();
assert!((mean - want_mean).abs() < 4.0 * se_mean, "mean {mean} vs {want_mean}");
assert!((var - want_var).abs() / want_var < 0.1, "var {var} vs {want_var}");
}
#[test]
fn cubic_drift_stays_bounded() {
let (sigma, dt): (f64, f64) = (2.0f64.sqrt(), 0.1);
let dx = dt.sqrt() * sigma;
let x0 = 5.0f64; let mut rng = Pcg::new(0xC3B1C, 4);
let mut x = [x0];
let mut max_abs: f64 = 0.0;
for _ in 0..100_000 {
integrate(&mut x, 0.0, dt, &[dx], 1, |x, _, _| -x[0].powi(3), |_, _, _| sigma, &mut rng);
max_abs = max_abs.max(x[0].abs());
}
assert!(max_abs <= x0 + dx + 1e-12, "walk escaped: max |x| = {max_abs}");
let mut rng2 = Pcg::new(0xC3B1C, 4);
let mut xe = x0;
let mut diverged = false;
for _ in 0..100 {
let a = rng2.f64().max(1e-15);
let b = rng2.f64();
let g = (-2.0 * a.ln()).sqrt() * (std::f64::consts::TAU * b).cos();
xe += -xe.powi(3) * dt + sigma * dt.sqrt() * g;
if !xe.is_finite() || xe.abs() > 1e6 {
diverged = true;
break;
}
}
assert!(diverged, "EM did not diverge from x0 = {x0} (reached {xe}); contrast unearned");
}
#[test]
fn ou_stationary_mean() {
let a = [[2.0, 1.0], [1.0, 2.0]];
let b = [1.0, 1.0];
let t_temp = 0.5f64;
let sigma = (2.0 * t_temp).sqrt();
let dt = 0.002f64;
let dx = dt.sqrt() * sigma;
let mut rng = Pcg::new(0x0057A7, 6);
let mut x = [0.0f64, 0.0];
integrate(&mut x, 0.0, dt, &[dx, dx], 200_000, |x, _, i| b[i] - (a[i][0] * x[0] + a[i][1] * x[1]),
|_, _, _| sigma, &mut rng);
let (mut m0, mut m1) = (0.0, 0.0);
let n = 2_000_000usize;
for _ in 0..n {
integrate(&mut x, 0.0, dt, &[dx, dx], 1,
|x, _, i| b[i] - (a[i][0] * x[0] + a[i][1] * x[1]),
|_, _, _| sigma, &mut rng);
m0 += x[0];
m1 += x[1];
}
let (m0, m1) = (m0 / n as f64, m1 / n as f64);
assert!((m0 - 1.0 / 3.0).abs() < 0.02, "x0 {m0}");
assert!((m1 - 1.0 / 3.0).abs() < 0.02, "x1 {m1}");
}
}