use crate::error::{IntegrateError, IntegrateResult};
struct Lcg64 {
state: u64,
}
impl Lcg64 {
fn new(seed: u64) -> Self {
let state = seed
.wrapping_mul(6_364_136_223_846_793_005_u64)
.wrapping_add(1_442_695_040_888_963_407_u64);
Self { state }
}
fn next_f64(&mut self) -> f64 {
self.state = self.state.wrapping_add(0x9e37_79b9_7f4a_7c15_u64);
let mut z = self.state;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9_u64);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb_u64);
z ^= z >> 31;
(z >> 11) as f64 * (1.0_f64 / (1u64 << 53) as f64)
}
fn next_normal(&mut self) -> f64 {
loop {
let u1 = self.next_f64();
let u2 = self.next_f64();
if u1 > 1e-300 {
let mag = (-2.0 * u1.ln()).sqrt();
let theta = std::f64::consts::TAU * u2;
return mag * theta.cos();
}
}
}
}
#[derive(Debug, Clone)]
pub struct WeakSdeConfig {
pub dt: f64,
pub n_steps: usize,
pub n_paths: usize,
pub seed: u64,
pub store_all_paths: bool,
pub fd_epsilon: f64,
}
impl Default for WeakSdeConfig {
fn default() -> Self {
Self {
dt: 0.01,
n_steps: 100,
n_paths: 1000,
seed: 42,
store_all_paths: false,
fd_epsilon: 1e-5,
}
}
}
#[derive(Debug, Clone)]
pub struct SdeResult {
pub time: Vec<f64>,
pub mean_path: Vec<f64>,
pub variance_path: Vec<f64>,
pub all_paths: Option<Vec<Vec<f64>>>,
}
pub fn platen_wagner_step(
x: f64,
drift: impl Fn(f64) -> f64,
diffusion: impl Fn(f64) -> f64,
dt: f64,
dw: f64,
dz: f64,
eps: f64,
) -> f64 {
let h = dt;
let sqrt_h = h.sqrt();
let fx = drift(x);
let gx = diffusion(x);
let upsilon = x + fx * h + gx * sqrt_h;
let f_upsilon = drift(upsilon);
let g_upsilon = diffusion(upsilon);
let g_prime = (diffusion(x + eps) - diffusion(x - eps)) / (2.0 * eps);
let t1 = 0.5 * (fx + f_upsilon) * h;
let t2 = gx * dw;
let t3 = 0.5 * (g_upsilon - gx) / sqrt_h * (dw * dw - h);
let t4 = gx * g_prime * dz / h;
x + t1 + t2 + t3 + t4
}
pub fn simulate_weak2(
x0: f64,
drift: impl Fn(f64) -> f64 + Clone,
diffusion: impl Fn(f64) -> f64 + Clone,
config: &WeakSdeConfig,
) -> IntegrateResult<SdeResult> {
if config.dt <= 0.0 {
return Err(IntegrateError::InvalidInput("dt must be positive".into()));
}
if config.n_steps == 0 {
return Err(IntegrateError::InvalidInput(
"n_steps must be at least 1".into(),
));
}
if config.n_paths == 0 {
return Err(IntegrateError::InvalidInput(
"n_paths must be at least 1".into(),
));
}
let n = config.n_steps;
let h = config.dt;
let sqrt_h = h.sqrt();
let np = config.n_paths;
let eps = config.fd_epsilon;
let time: Vec<f64> = (0..=n).map(|k| k as f64 * h).collect();
let mut sum_path = vec![0.0_f64; n + 1];
let mut sum_sq_path = vec![0.0_f64; n + 1];
let mut all_paths: Option<Vec<Vec<f64>>> = if config.store_all_paths {
Some(Vec::with_capacity(np))
} else {
None
};
let mut rng = Lcg64::new(config.seed);
for path_idx in 0..np {
let mut x = x0;
let mut traj = if config.store_all_paths {
Some(Vec::with_capacity(n + 1))
} else {
None
};
if let Some(ref mut t) = traj {
t.push(x);
}
sum_path[0] += x;
sum_sq_path[0] += x * x;
let mut path_rng = Lcg64::new(config.seed.wrapping_add(path_idx as u64 * 1_000_003));
for _step in 0..n {
let z1 = path_rng.next_normal();
let dw = z1 * sqrt_h;
let z2 = path_rng.next_normal();
let dz = 0.5 * (dw * h - z2 * (h * h * h / 3.0_f64).sqrt());
x = platen_wagner_step(x, &drift, &diffusion, h, dw, dz, eps);
let step = _step + 1;
sum_path[step] += x;
sum_sq_path[step] += x * x;
if let Some(ref mut t) = traj {
t.push(x);
}
}
if let Some(ref mut ap) = all_paths {
if let Some(t) = traj {
ap.push(t);
}
}
}
let npf = np as f64;
let mean_path: Vec<f64> = sum_path.iter().map(|&s| s / npf).collect();
let variance_path: Vec<f64> = sum_path
.iter()
.zip(sum_sq_path.iter())
.map(|(&s, &s2)| {
if np < 2 {
0.0
} else {
(s2 / npf - (s / npf).powi(2)).max(0.0)
}
})
.collect();
Ok(SdeResult {
time,
mean_path,
variance_path,
all_paths,
})
}
pub fn weak_convergence_rate(errors_by_dt: &[(f64, f64)]) -> f64 {
if errors_by_dt.len() < 2 {
return 0.0;
}
let mut sx = 0.0_f64;
let mut sy = 0.0_f64;
let mut sxx = 0.0_f64;
let mut sxy = 0.0_f64;
let mut n = 0_usize;
for &(dt, err) in errors_by_dt {
if dt > 0.0 && err > 0.0 {
let lx = dt.ln();
let ly = err.ln();
sx += lx;
sy += ly;
sxx += lx * lx;
sxy += lx * ly;
n += 1;
}
}
if n < 2 {
return 0.0;
}
let nf = n as f64;
(nf * sxy - sx * sy) / (nf * sxx - sx * sx)
}
pub fn expected_value_test(
x0: f64,
t_final: f64,
exact_mean: impl Fn(f64) -> f64,
drift: impl Fn(f64) -> f64 + Clone,
diffusion: impl Fn(f64) -> f64 + Clone,
config: &WeakSdeConfig,
) -> IntegrateResult<f64> {
let result = simulate_weak2(x0, drift, diffusion, config)?;
let mc_mean = result.mean_path.last().copied().unwrap_or(x0);
let exact = exact_mean(t_final);
Ok((mc_mean - exact).abs())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ornstein_uhlenbeck_mean() {
let theta = 1.0_f64;
let sigma = 0.5_f64;
let x0 = 2.0_f64;
let t_final = 1.0_f64;
let exact_mean = x0 * (-theta * t_final).exp();
let cfg = WeakSdeConfig {
dt: 0.01,
n_steps: 100,
n_paths: 5000,
seed: 12345,
..Default::default()
};
let err = expected_value_test(
x0,
t_final,
|_t| x0 * (-theta * t_final).exp(),
move |x| -theta * x,
move |_x| sigma,
&cfg,
)
.expect("expected_value_test should succeed");
assert!(
err < 0.15,
"OU mean error = {} (exact = {}, approx = {})",
err,
exact_mean,
exact_mean - err
);
}
#[test]
fn test_gbm_mean() {
let mu = 0.05_f64;
let sigma = 0.2_f64;
let x0 = 1.0_f64;
let t_final = 0.5_f64;
let exact_mean = x0 * (mu * t_final).exp();
let cfg = WeakSdeConfig {
dt: 0.005,
n_steps: 100,
n_paths: 4000,
seed: 999,
..Default::default()
};
let result = simulate_weak2(x0, move |x| mu * x, move |x| sigma * x, &cfg)
.expect("GBM simulation should succeed");
let mc_mean = *result.mean_path.last().unwrap_or(&x0);
let err = (mc_mean - exact_mean).abs();
assert!(
err < 0.1,
"GBM mean error = {}, exact = {}, mc = {}",
err,
exact_mean,
mc_mean
);
}
#[test]
fn test_weak_convergence_rate_ou() {
let data: Vec<(f64, f64)> = [0.1, 0.05, 0.025, 0.01]
.iter()
.map(|&dt| (dt, 0.5 * dt * dt)) .collect();
let rate = weak_convergence_rate(&data);
assert!(
(rate - 2.0).abs() < 0.05,
"convergence rate estimator: got {}, expected 2.0",
rate
);
let theta = 1.0_f64;
let sigma = 0.3_f64;
let x0 = 1.0_f64;
let t_final = 0.5_f64;
let exact = x0 * (-theta * t_final).exp();
let cfg = WeakSdeConfig {
dt: 0.02,
n_steps: 25,
n_paths: 5000,
seed: 54321,
..Default::default()
};
let result = simulate_weak2(x0, move |x| -theta * x, move |_| sigma, &cfg)
.expect("simulate_weak2 should succeed");
let mc_mean = result.mean_path.last().copied().unwrap_or(x0);
let err = (mc_mean - exact).abs();
assert!(
err < 0.1,
"OU mc_mean = {}, exact = {}, err = {}",
mc_mean,
exact,
err
);
}
#[test]
fn test_platen_wagner_step_additive() {
let x0 = 1.0_f64;
let dw = 0.1_f64;
let dz = 0.0_f64;
let dt = 0.01_f64;
let x1 = platen_wagner_step(x0, |_| 0.0, |_| 1.0, dt, dw, dz, 1e-5);
assert!((x1 - (x0 + dw)).abs() < 1e-12, "x1 = {}", x1);
}
#[test]
fn test_weak_convergence_rate_synthetic() {
let data: Vec<(f64, f64)> = [0.1, 0.05, 0.025, 0.01]
.iter()
.map(|&dt| (dt, dt * dt))
.collect();
let rate = weak_convergence_rate(&data);
assert!((rate - 2.0).abs() < 0.01, "rate = {}, expected 2.0", rate);
}
#[test]
fn test_store_all_paths() {
let cfg = WeakSdeConfig {
dt: 0.1,
n_steps: 5,
n_paths: 10,
seed: 0,
store_all_paths: true,
..Default::default()
};
let result =
simulate_weak2(0.0, |_| 0.0, |_| 1.0, &cfg).expect("simulate_weak2 should succeed");
let ap = result.all_paths.expect("all_paths should be Some");
assert_eq!(ap.len(), 10, "should have 10 paths");
for path in &ap {
assert_eq!(path.len(), 6, "each path should have n_steps+1 = 6 points");
}
}
#[test]
fn test_time_vector_length() {
let cfg = WeakSdeConfig {
dt: 0.01,
n_steps: 50,
n_paths: 100,
seed: 1,
..Default::default()
};
let result =
simulate_weak2(1.0, |x| -x, |_| 0.1, &cfg).expect("simulate_weak2 should succeed");
assert_eq!(result.time.len(), 51, "time should have n_steps+1 entries");
assert_eq!(result.mean_path.len(), 51);
assert_eq!(result.variance_path.len(), 51);
}
}