use ndarray::Array1;
use ndarray::Array2;
use ndarray::Array3;
use ndarray::ArrayView1;
use ndarray::Axis;
use ndarray::s;
use rand::Rng;
use super::noise::fgn::Fgn;
use crate::traits::FloatExt;
use crate::traits::ProcessExt;
pub enum NoiseModel {
Gaussian,
Fractional,
}
pub enum SdeMethod {
Euler,
Milstein,
SRK2,
SRK4,
}
pub struct Sde<T: FloatExt, F, G>
where
F: Fn(&Array1<T>, T) -> Array1<T>,
G: Fn(&Array1<T>, T) -> Array2<T>,
{
pub drift: F,
pub diffusion: G,
pub noise: NoiseModel,
pub hursts: Option<Array1<T>>,
}
impl<T: FloatExt, F, G> Sde<T, F, G>
where
F: Fn(&Array1<T>, T) -> Array1<T>,
G: Fn(&Array1<T>, T) -> Array2<T>,
{
pub fn new(drift: F, diffusion: G, noise: NoiseModel, hursts: Option<Array1<T>>) -> Self {
Self {
drift,
diffusion,
noise,
hursts,
}
}
pub fn solve(
&self,
x0: &Array1<T>,
t0: T,
t1: T,
dt: T,
n_paths: usize,
method: SdeMethod,
rng: &mut impl Rng,
) -> Array3<T> {
match self.noise {
NoiseModel::Gaussian => match method {
SdeMethod::Euler => self.solve_euler_gauss(x0, t0, t1, dt, n_paths, rng),
SdeMethod::Milstein => self.solve_milstein_gauss(x0, t0, t1, dt, n_paths, rng),
SdeMethod::SRK2 => self.solve_srk2_gauss(x0, t0, t1, dt, n_paths, rng),
SdeMethod::SRK4 => self.solve_srk4_gauss(x0, t0, t1, dt, n_paths, rng),
},
NoiseModel::Fractional => {
let steps = ((t1 - t0) / dt).ceil().to_usize().unwrap();
let dim = x0.len();
let mut incs = Array3::zeros((n_paths, steps, dim));
if let Some(h) = &self.hursts {
let fgns: Vec<Fgn<T>> = (0..dim)
.map(|d| Fgn::new(h[d], steps, Some(t1 - t0)))
.collect();
for p in 0..n_paths {
for (d, fgn) in fgns.iter().enumerate().take(dim) {
let data = fgn.sample();
incs.slice_mut(s![p, .., d]).assign(&data);
}
}
}
match method {
SdeMethod::Euler => self.solve_euler_fractional(x0, t0, dt, &incs),
SdeMethod::Milstein => self.solve_milstein_fractional(x0, t0, dt, &incs),
SdeMethod::SRK2 => self.solve_srk2_fractional(x0, t0, dt, &incs),
SdeMethod::SRK4 => self.solve_srk4_fractional(x0, t0, dt, &incs),
}
}
}
}
fn fill_gauss_increment(&self, out: &mut [T], sqrt_dt: T, _rng: &mut impl Rng) {
T::fill_standard_normal_scaled_slice(out, sqrt_dt);
}
fn solve_euler_gauss(
&self,
x0: &Array1<T>,
t0: T,
t1: T,
dt: T,
n_paths: usize,
rng: &mut impl Rng,
) -> Array3<T> {
let steps = ((t1 - t0) / dt).ceil().to_usize().unwrap();
let dim = x0.len();
let sqrt_dt = dt.sqrt();
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut d_w = vec![T::zero(); dim];
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i in 1..=steps {
self.fill_gauss_increment(&mut d_w, sqrt_dt, rng);
let mu_val = (self.drift)(&x, time);
let sigma_val = (self.diffusion)(&x, time);
for i_dim in 0..dim {
let mut incr = mu_val[i_dim] * dt;
for j_dim in 0..dim {
incr += sigma_val[[i_dim, j_dim]] * d_w[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i, ..]).assign(&x);
}
}
out
}
fn milstein_correction(
&self,
x: &Array1<T>,
time: T,
d_w: ArrayView1<'_, T>,
dt: T,
) -> Array1<T> {
let dim = x.len();
let eps = T::from_f64_fast(1e-7);
let half = T::from_f64_fast(0.5);
let sigma_val = (self.diffusion)(x, time);
let mut correction = Array1::zeros(dim);
let mut dsigma_dx: Vec<Array2<T>> = Vec::with_capacity(dim);
for k in 0..dim {
let mut x_plus = x.clone();
x_plus[k] += eps;
let sigma_plus = (self.diffusion)(&x_plus, time);
dsigma_dx.push((&sigma_plus - &sigma_val) / eps);
}
for j1 in 0..dim {
for j2 in 0..dim {
let i_j1j2 = if j1 == j2 {
half * (d_w[j1] * d_w[j2] - dt)
} else {
half * d_w[j1] * d_w[j2]
};
for i_dim in 0..dim {
let mut lj1_sigma_ij2 = T::zero();
for k in 0..dim {
lj1_sigma_ij2 += sigma_val[[k, j1]] * dsigma_dx[k][[i_dim, j2]];
}
correction[i_dim] += lj1_sigma_ij2 * i_j1j2;
}
}
}
correction
}
fn solve_milstein_gauss(
&self,
x0: &Array1<T>,
t0: T,
t1: T,
dt: T,
n_paths: usize,
rng: &mut impl Rng,
) -> Array3<T> {
let steps = ((t1 - t0) / dt).ceil().to_usize().unwrap();
let dim = x0.len();
let sqrt_dt = dt.sqrt();
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut d_w = vec![T::zero(); dim];
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i in 1..=steps {
self.fill_gauss_increment(&mut d_w, sqrt_dt, rng);
let mu_val = (self.drift)(&x, time);
let sigma_val = (self.diffusion)(&x, time);
let correction = self.milstein_correction(&x, time, ArrayView1::from(&d_w[..]), dt);
for i_dim in 0..dim {
let mut incr = mu_val[i_dim] * dt;
for j_dim in 0..dim {
incr += sigma_val[[i_dim, j_dim]] * d_w[j_dim];
}
incr += correction[i_dim];
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i, ..]).assign(&x);
}
}
out
}
fn solve_srk2_gauss(
&self,
x0: &Array1<T>,
t0: T,
t1: T,
dt: T,
n_paths: usize,
rng: &mut impl Rng,
) -> Array3<T> {
let steps = ((t1 - t0) / dt).ceil().to_usize().unwrap();
let dim = x0.len();
let sqrt_dt = dt.sqrt();
let half = T::from_f64_fast(0.5);
let half_dt = half * dt;
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut d_w = vec![T::zero(); dim];
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i in 1..=steps {
self.fill_gauss_increment(&mut d_w, sqrt_dt, rng);
let mu1 = (self.drift)(&x, time);
let sig1 = (self.diffusion)(&x, time);
let mut x_half = x.clone();
for i_dim in 0..dim {
let mut incr = mu1[i_dim] * half_dt;
for j_dim in 0..dim {
incr += sig1[[i_dim, j_dim]] * (half * d_w[j_dim]);
}
x_half[i_dim] += incr;
}
let mu2 = (self.drift)(&x_half, time + half_dt);
let sig2 = (self.diffusion)(&x_half, time + half_dt);
for i_dim in 0..dim {
let mut incr = mu2[i_dim] * dt;
for j_dim in 0..dim {
incr += sig2[[i_dim, j_dim]] * d_w[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i, ..]).assign(&x);
}
}
out
}
fn solve_srk4_gauss(
&self,
x0: &Array1<T>,
t0: T,
t1: T,
dt: T,
n_paths: usize,
rng: &mut impl Rng,
) -> Array3<T> {
let steps = ((t1 - t0) / dt).ceil().to_usize().unwrap();
let dim = x0.len();
let sqrt_dt = dt.sqrt();
let half = T::from_f64_fast(0.5);
let two = T::from_f64_fast(2.0);
let six = T::from_f64_fast(6.0);
let half_dt = half * dt;
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut d_w_full = vec![T::zero(); dim];
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i in 1..=steps {
self.fill_gauss_increment(&mut d_w_full, sqrt_dt, rng);
let k1_mu = (self.drift)(&x, time);
let k1_sig = (self.diffusion)(&x, time);
let mut x1 = x.clone();
for i_dim in 0..dim {
let mut incr = k1_mu[i_dim] * half_dt;
for j_dim in 0..dim {
incr += k1_sig[[i_dim, j_dim]] * (d_w_full[j_dim] * half);
}
x1[i_dim] += incr;
}
let k2_mu = (self.drift)(&x1, time + half_dt);
let k2_sig = (self.diffusion)(&x1, time + half_dt);
let mut x2 = x.clone();
for i_dim in 0..dim {
let mut incr = k2_mu[i_dim] * half_dt;
for j_dim in 0..dim {
incr += k2_sig[[i_dim, j_dim]] * (d_w_full[j_dim] * half);
}
x2[i_dim] += incr;
}
let k3_mu = (self.drift)(&x2, time + half_dt);
let k3_sig = (self.diffusion)(&x2, time + half_dt);
let mut x3 = x.clone();
for i_dim in 0..dim {
let mut incr = k3_mu[i_dim] * dt;
for j_dim in 0..dim {
incr += k3_sig[[i_dim, j_dim]] * d_w_full[j_dim];
}
x3[i_dim] += incr;
}
let k4_mu = (self.drift)(&x3, time + dt);
let k4_sig = (self.diffusion)(&x3, time + dt);
for i_dim in 0..dim {
let drift_avg =
(k1_mu[i_dim] + two * k2_mu[i_dim] + two * k3_mu[i_dim] + k4_mu[i_dim]) / six;
let mut incr = drift_avg * dt;
for j_dim in 0..dim {
let diff_ij = (k1_sig[[i_dim, j_dim]]
+ two * k2_sig[[i_dim, j_dim]]
+ two * k3_sig[[i_dim, j_dim]]
+ k4_sig[[i_dim, j_dim]])
/ six;
incr += diff_ij * d_w_full[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i, ..]).assign(&x);
}
}
out
}
fn solve_euler_fractional(&self, x0: &Array1<T>, t0: T, dt: T, incs: &Array3<T>) -> Array3<T> {
let (n_paths, steps, dim) = (
incs.len_of(Axis(0)),
incs.len_of(Axis(1)),
incs.len_of(Axis(2)),
);
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i_step in 1..=steps {
let d_w = incs.slice(s![p, i_step - 1, ..]);
let mu_val = (self.drift)(&x, time);
let sigma_val = (self.diffusion)(&x, time);
for i_dim in 0..dim {
let mut incr = mu_val[i_dim] * dt;
for j_dim in 0..dim {
incr += sigma_val[[i_dim, j_dim]] * d_w[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i_step, ..]).assign(&x);
}
}
out
}
fn solve_milstein_fractional(&self, x0: &Array1<T>, t0: T, dt: T, incs: &Array3<T>) -> Array3<T> {
let (n_paths, steps, dim) = (
incs.len_of(Axis(0)),
incs.len_of(Axis(1)),
incs.len_of(Axis(2)),
);
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i_step in 1..=steps {
let d_w = incs.slice(s![p, i_step - 1, ..]);
let mu_val = (self.drift)(&x, time);
let sigma_val = (self.diffusion)(&x, time);
let correction = self.milstein_correction(&x, time, d_w, dt);
for i_dim in 0..dim {
let mut incr = mu_val[i_dim] * dt;
for j_dim in 0..dim {
incr += sigma_val[[i_dim, j_dim]] * d_w[j_dim];
}
incr += correction[i_dim];
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i_step, ..]).assign(&x);
}
}
out
}
fn solve_srk2_fractional(&self, x0: &Array1<T>, t0: T, dt: T, incs: &Array3<T>) -> Array3<T> {
let (n_paths, steps, dim) = (
incs.len_of(Axis(0)),
incs.len_of(Axis(1)),
incs.len_of(Axis(2)),
);
let half = T::from_f64_fast(0.5);
let half_dt = half * dt;
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i_step in 1..=steps {
let d_w = incs.slice(s![p, i_step - 1, ..]);
let mu1 = (self.drift)(&x, time);
let sig1 = (self.diffusion)(&x, time);
let mut x_half = x.clone();
for i_dim in 0..dim {
let mut incr = mu1[i_dim] * half_dt;
for j_dim in 0..dim {
incr += sig1[[i_dim, j_dim]] * (half * d_w[j_dim]);
}
x_half[i_dim] += incr;
}
let mu2 = (self.drift)(&x_half, time + half_dt);
let sig2 = (self.diffusion)(&x_half, time + half_dt);
for i_dim in 0..dim {
let mut incr = mu2[i_dim] * dt;
for j_dim in 0..dim {
incr += sig2[[i_dim, j_dim]] * d_w[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i_step, ..]).assign(&x);
}
}
out
}
fn solve_srk4_fractional(&self, x0: &Array1<T>, t0: T, dt: T, incs: &Array3<T>) -> Array3<T> {
let (n_paths, steps, dim) = (
incs.len_of(Axis(0)),
incs.len_of(Axis(1)),
incs.len_of(Axis(2)),
);
let half = T::from_f64_fast(0.5);
let two = T::from_f64_fast(2.0);
let six = T::from_f64_fast(6.0);
let half_dt = half * dt;
let mut out = Array3::zeros((n_paths, steps + 1, dim));
for p in 0..n_paths {
let mut x = x0.clone();
let mut time = t0;
out.slice_mut(s![p, 0, ..]).assign(&x);
for i_step in 1..=steps {
let d_w_full = incs.slice(s![p, i_step - 1, ..]);
let k1_mu = (self.drift)(&x, time);
let k1_sig = (self.diffusion)(&x, time);
let mut x1 = x.clone();
for i_dim in 0..dim {
let mut incr = k1_mu[i_dim] * half_dt;
for j_dim in 0..dim {
incr += k1_sig[[i_dim, j_dim]] * (d_w_full[j_dim] * half);
}
x1[i_dim] += incr;
}
let k2_mu = (self.drift)(&x1, time + half_dt);
let k2_sig = (self.diffusion)(&x1, time + half_dt);
let mut x2 = x.clone();
for i_dim in 0..dim {
let mut incr = k2_mu[i_dim] * half_dt;
for j_dim in 0..dim {
incr += k2_sig[[i_dim, j_dim]] * (d_w_full[j_dim] * half);
}
x2[i_dim] += incr;
}
let k3_mu = (self.drift)(&x2, time + half_dt);
let k3_sig = (self.diffusion)(&x2, time + half_dt);
let mut x3 = x.clone();
for i_dim in 0..dim {
let mut incr = k3_mu[i_dim] * dt;
for j_dim in 0..dim {
incr += k3_sig[[i_dim, j_dim]] * d_w_full[j_dim];
}
x3[i_dim] += incr;
}
let k4_mu = (self.drift)(&x3, time + dt);
let k4_sig = (self.diffusion)(&x3, time + dt);
for i_dim in 0..dim {
let drift_avg =
(k1_mu[i_dim] + two * k2_mu[i_dim] + two * k3_mu[i_dim] + k4_mu[i_dim]) / six;
let mut incr = drift_avg * dt;
for j_dim in 0..dim {
let diff_ij = (k1_sig[[i_dim, j_dim]]
+ two * k2_sig[[i_dim, j_dim]]
+ two * k3_sig[[i_dim, j_dim]]
+ k4_sig[[i_dim, j_dim]])
/ six;
incr += diff_ij * d_w_full[j_dim];
}
x[i_dim] += incr;
}
time += dt;
out.slice_mut(s![p, i_step, ..]).assign(&x);
}
}
out
}
}
#[cfg(test)]
mod tests {
use ndarray::Array2;
use ndarray::array;
use rand::SeedableRng;
use rand::rngs::StdRng;
use super::*;
fn gbm_sde()
-> Sde<f64, impl Fn(&Array1<f64>, f64) -> Array1<f64>, impl Fn(&Array1<f64>, f64) -> Array2<f64>>
{
let mu = 0.05;
let sigma = 0.20;
Sde::new(
move |x: &Array1<f64>, _t: f64| array![mu * x[0]],
move |x: &Array1<f64>, _t: f64| Array2::from_elem((1, 1), sigma * x[0]),
NoiseModel::Gaussian,
None,
)
}
fn final_mean(paths: &ndarray::Array3<f64>) -> f64 {
let n_paths = paths.shape()[0];
let last = paths.shape()[1] - 1;
let sum: f64 = (0..n_paths).map(|p| paths[[p, last, 0]]).sum();
sum / n_paths as f64
}
#[test]
fn euler_gbm_recovers_analytical_mean() {
let sde = gbm_sde();
let s0 = array![100.0_f64];
let t = 1.0_f64;
let dt = 1e-3_f64;
let mut rng = StdRng::seed_from_u64(0x5DE_F00D);
let paths = sde.solve(&s0, 0.0, t, dt, 4_000, SdeMethod::Euler, &mut rng);
let analytic = 100.0_f64 * (0.05_f64 * t).exp();
let m = final_mean(&paths);
let rel_err = (m - analytic).abs() / analytic;
assert!(rel_err < 0.02, "Euler relative error {rel_err}");
}
#[test]
fn milstein_gbm_recovers_analytical_mean() {
let sde = gbm_sde();
let s0 = array![100.0_f64];
let t = 1.0_f64;
let dt = 1e-3_f64;
let mut rng = StdRng::seed_from_u64(0xC0FFEE);
let paths = sde.solve(&s0, 0.0, t, dt, 4_000, SdeMethod::Milstein, &mut rng);
let analytic = 100.0_f64 * (0.05_f64 * t).exp();
let m = final_mean(&paths);
let rel_err = (m - analytic).abs() / analytic;
assert!(rel_err < 0.02, "Milstein relative error {rel_err}");
}
#[test]
fn srk2_and_srk4_produce_finite_paths() {
let sde = gbm_sde();
let s0 = array![100.0_f64];
let mut rng = StdRng::seed_from_u64(0xBADCAFE);
for method in [SdeMethod::SRK2, SdeMethod::SRK4] {
let paths = sde.solve(&s0, 0.0, 0.5, 1e-3, 200, method, &mut rng);
assert!(paths.iter().all(|v| v.is_finite()));
for p in 0..paths.shape()[0] {
assert!((paths[[p, 0, 0]] - 100.0).abs() < 1e-12);
}
}
}
#[test]
fn pure_drift_decay_tracks_exponential() {
let k = 1.0;
let sde = Sde::new(
move |x: &Array1<f64>, _t: f64| array![-k * x[0]],
|_x: &Array1<f64>, _t: f64| Array2::<f64>::zeros((1, 1)),
NoiseModel::Gaussian,
None,
);
let s0 = array![1.0_f64];
let t = 1.0_f64;
let dt = 1e-3_f64;
let mut rng = StdRng::seed_from_u64(0xDEC0DE);
let paths = sde.solve(&s0, 0.0, t, dt, 1, SdeMethod::Euler, &mut rng);
let last = paths.shape()[1] - 1;
let computed = paths[[0, last, 0]];
let analytic = (-k * t).exp();
assert!(
(computed - analytic).abs() < 5e-3,
"Euler decay {computed} vs analytic {analytic}"
);
}
#[test]
fn initial_condition_preserved() {
let sde = gbm_sde();
let s0 = array![100.0_f64];
let mut rng = StdRng::seed_from_u64(7);
let paths = sde.solve(&s0, 0.0, 0.1, 1e-3, 5, SdeMethod::Euler, &mut rng);
for p in 0..5 {
assert_eq!(paths[[p, 0, 0]], 100.0);
}
}
}