use crate::error::{IntegrateError, IntegrateResult};
use crate::sde::fractional_brownian::{FbmConfig, FbmMethod, FractionalBrownianMotion};
use scirs2_core::ndarray::Array1;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RoughScheme {
EulerMaruyama,
Milstein,
}
#[derive(Debug, Clone)]
pub struct RoughSdeConfig {
pub hurst: f64,
pub n_steps: usize,
pub t_end: f64,
pub scheme: RoughScheme,
pub seed: u64,
}
impl Default for RoughSdeConfig {
fn default() -> Self {
Self {
hurst: 0.7,
n_steps: 500,
t_end: 1.0,
scheme: RoughScheme::EulerMaruyama,
seed: 42,
}
}
}
#[derive(Debug, Clone)]
pub struct RoughSdeResult {
pub times: Array1<f64>,
pub path: Array1<f64>,
pub fbm_increments: Array1<f64>,
}
pub fn rough_sde_solve<F, G>(
f: F,
g: G,
x0: f64,
config: &RoughSdeConfig,
) -> IntegrateResult<RoughSdeResult>
where
F: Fn(f64, f64) -> f64,
G: Fn(f64, f64) -> f64,
{
validate_config(config)?;
let n = config.n_steps;
let dt = config.t_end / n as f64;
let fbm_cfg = FbmConfig {
hurst: config.hurst,
n_steps: n,
dt,
seed: config.seed,
method: FbmMethod::DaviesHarte,
};
let fbm = FractionalBrownianMotion::new(fbm_cfg);
let db = fbm.increments().map_err(|e| {
IntegrateError::ComputationError(format!("fBm increment generation failed: {e}"))
})?;
let times_vec: Vec<f64> = (0..=n).map(|i| i as f64 * dt).collect();
let times = Array1::from_vec(times_vec);
let mut path = Array1::zeros(n + 1);
path[0] = x0;
let h = config.hurst;
let wz_coeff = 0.5 * (2.0 * h - 1.0) * dt.powf(2.0 * h);
let eps = 1e-6_f64;
for i in 0..n {
let t_i = times[i];
let x_i = path[i];
let db_i = db[i];
let f_val = f(t_i, x_i);
let g_val = g(t_i, x_i);
let euler_step = f_val * dt + g_val * db_i;
#[allow(unreachable_patterns)]
let correction = match config.scheme {
RoughScheme::EulerMaruyama => 0.0,
RoughScheme::Milstein => {
let g_plus = g(t_i, x_i + eps);
let g_prime = (g_plus - g_val) / eps;
g_val * g_prime * wz_coeff
}
_ => 0.0,
};
path[i + 1] = x_i + euler_step + correction;
}
Ok(RoughSdeResult {
times,
path,
fbm_increments: db,
})
}
pub fn bergomi_model(
nu: f64,
v0: f64,
hurst: f64,
n_steps: usize,
t_end: f64,
seed: u64,
) -> IntegrateResult<(Array1<f64>, Array1<f64>)> {
if v0 <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Initial variance v0 must be positive, got {}",
v0
)));
}
if hurst <= 0.0 || hurst >= 1.0 {
return Err(IntegrateError::InvalidInput(format!(
"Hurst exponent must be in (0, 1), got {}",
hurst
)));
}
if n_steps == 0 {
return Err(IntegrateError::InvalidInput(
"n_steps must be at least 1".to_string(),
));
}
if t_end <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"t_end must be positive, got {}",
t_end
)));
}
let dt = t_end / n_steps as f64;
let fbm_cfg = FbmConfig {
hurst,
n_steps,
dt,
seed,
method: FbmMethod::DaviesHarte,
};
let fbm_path = FractionalBrownianMotion::new(fbm_cfg)
.sample_path()
.map_err(|e| IntegrateError::ComputationError(format!("Bergomi fBm path failed: {e}")))?;
let h2 = 2.0 * hurst;
let vol_path: Vec<f64> = (0..=n_steps)
.map(|i| {
let t_i = i as f64 * dt;
let bh = fbm_path[i];
let log_vol = 0.5 * v0.ln() + nu * bh - 0.5 * nu * nu * t_i.powf(h2);
log_vol.exp()
})
.collect();
let times: Vec<f64> = (0..=n_steps).map(|i| i as f64 * dt).collect();
Ok((Array1::from_vec(times), Array1::from_vec(vol_path)))
}
fn validate_config(cfg: &RoughSdeConfig) -> IntegrateResult<()> {
if cfg.hurst <= 0.0 || cfg.hurst >= 1.0 {
return Err(IntegrateError::InvalidInput(format!(
"Hurst exponent must be in (0, 1), got {}",
cfg.hurst
)));
}
if cfg.n_steps == 0 {
return Err(IntegrateError::InvalidInput(
"n_steps must be at least 1".to_string(),
));
}
if cfg.t_end <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"t_end must be positive, got {}",
cfg.t_end
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn default_cfg() -> RoughSdeConfig {
RoughSdeConfig::default()
}
fn em_cfg(h: f64, n: usize) -> RoughSdeConfig {
RoughSdeConfig {
hurst: h,
n_steps: n,
t_end: 1.0,
scheme: RoughScheme::EulerMaruyama,
seed: 42,
}
}
#[test]
fn test_default_config_runs() {
let res = rough_sde_solve(|_t, _x| 0.0, |_t, _x| 1.0, 0.0, &default_cfg())
.expect("default config should succeed");
assert_eq!(res.path.len(), default_cfg().n_steps + 1);
}
#[test]
fn test_path_length() {
let cfg = em_cfg(0.7, 200);
let res =
rough_sde_solve(|_t, _x| 0.5, |_t, _x| 0.1, 1.0, &cfg).expect("solve should succeed");
assert_eq!(res.path.len(), 201); }
#[test]
fn test_times_length_and_end_value() {
let cfg = em_cfg(0.6, 100);
let res =
rough_sde_solve(|_t, _x| 0.0, |_t, _x| 0.0, 0.0, &cfg).expect("solve should succeed");
assert_eq!(res.times.len(), 101);
let t_last = res.times[100];
assert!(
(t_last - 1.0).abs() < 1e-10,
"last time = {}, expected 1.0",
t_last
);
}
#[test]
fn test_zero_diffusion_matches_ode() {
let f_val = 2.0_f64;
let x0 = 1.0_f64;
let n = 100;
let t_end = 1.0;
let cfg = em_cfg(0.7, n);
let res = rough_sde_solve(move |_t, _x| f_val, |_t, _x| 0.0, x0, &cfg)
.expect("solve should succeed");
let expected_end = x0 + f_val * t_end;
let actual_end = res.path[n];
let rel = (actual_end - expected_end).abs() / expected_end;
assert!(
rel < 1e-10,
"zero-diffusion ODE: got {}, expected {}, rel err {}",
actual_end,
expected_end,
rel
);
}
#[test]
fn test_zero_drift_pure_fbm_integral() {
let cfg = em_cfg(0.7, 256);
let res =
rough_sde_solve(|_t, _x| 0.0, |_t, _x| 1.0, 0.0, &cfg).expect("solve should succeed");
assert!((res.path[0]).abs() < 1e-15);
let sum_sq: f64 = res.path.iter().map(|&v| v * v).sum();
assert!(sum_sq > 1e-20, "zero-drift path should be non-trivial");
}
#[test]
fn test_h_half_euler_matches_fbm_path() {
let n = 128;
let dt = 1.0 / n as f64;
let seed = 0;
let cfg = RoughSdeConfig {
hurst: 0.5,
n_steps: n,
t_end: 1.0,
scheme: RoughScheme::EulerMaruyama,
seed,
};
let res = rough_sde_solve(|_t, _x| 0.0, |_t, _x| 1.0, 0.0, &cfg).expect("solve");
let fbm_cfg = FbmConfig {
hurst: 0.5,
n_steps: n,
dt,
seed,
method: FbmMethod::DaviesHarte,
};
let fbm_path = FractionalBrownianMotion::new(fbm_cfg)
.sample_path()
.expect("fbm path");
for i in 0..=n {
assert!(
(res.path[i] - fbm_path[i]).abs() < 1e-12,
"mismatch at i={}: rough={}, fbm={}",
i,
res.path[i],
fbm_path[i]
);
}
}
#[test]
fn test_milstein_differs_from_euler_for_h_ne_half() {
let n = 128;
let cfg_em = RoughSdeConfig {
hurst: 0.3,
n_steps: n,
t_end: 1.0,
scheme: RoughScheme::EulerMaruyama,
seed: 99,
};
let cfg_mil = RoughSdeConfig {
scheme: RoughScheme::Milstein,
..cfg_em.clone()
};
let res_em =
rough_sde_solve(|_t, x| 0.1 * x, |_t, x| 0.2 * x, 1.0, &cfg_em).expect("EM solve");
let res_mil = rough_sde_solve(|_t, x| 0.1 * x, |_t, x| 0.2 * x, 1.0, &cfg_mil)
.expect("Milstein solve");
let max_diff: f64 = res_em
.path
.iter()
.zip(res_mil.path.iter())
.map(|(a, b)| (a - b).abs())
.fold(0.0_f64, f64::max);
assert!(
max_diff > 1e-12,
"Milstein and Euler should differ for H=0.3, max_diff={}",
max_diff
);
}
#[test]
fn test_bergomi_vol_always_positive() {
let (times, vol) =
bergomi_model(0.3, 0.04, 0.1, 256, 1.0, 0).expect("bergomi_model should succeed");
assert_eq!(vol.len(), 257);
assert_eq!(times.len(), 257);
for (i, &v) in vol.iter().enumerate() {
assert!(
v > 0.0,
"volatility must be strictly positive at step {}, got {}",
i,
v
);
}
}
#[test]
fn test_result_times_length() {
let cfg = em_cfg(0.7, 50);
let res = rough_sde_solve(|_t, _x| 0.0, |_t, _x| 1.0, 0.5, &cfg).expect("solve");
assert_eq!(res.times.len(), 51);
assert_eq!(res.fbm_increments.len(), 50);
}
#[test]
fn test_trivial_single_step() {
let cfg = RoughSdeConfig {
hurst: 0.7,
n_steps: 1,
t_end: 0.1,
scheme: RoughScheme::EulerMaruyama,
seed: 0,
};
let res =
rough_sde_solve(|_t, _x| 1.0, |_t, _x| 0.0, 2.0, &cfg).expect("single-step solve");
assert_eq!(res.path.len(), 2);
let expected = 2.0 + 1.0 * 0.1;
assert!(
(res.path[1] - expected).abs() < 1e-10,
"single-step: got {}, expected {}",
res.path[1],
expected
);
}
}