pub mod euler_maruyama;
pub mod examples;
pub mod fractional_brownian;
pub mod jump_diffusion;
pub mod milstein;
pub mod processes;
pub mod rough_sde;
pub mod runge_kutta_sde;
pub mod srk;
pub mod weak_order2;
pub mod weak_schemes;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
pub struct SdeProblem<F, G>
where
F: Fn(f64, &Array1<f64>) -> Array1<f64>,
G: Fn(f64, &Array1<f64>) -> Array2<f64>,
{
pub x0: Array1<f64>,
pub t_span: [f64; 2],
pub n_brownian: usize,
pub f_drift: F,
pub g_diffusion: G,
}
impl<F, G> SdeProblem<F, G>
where
F: Fn(f64, &Array1<f64>) -> Array1<f64>,
G: Fn(f64, &Array1<f64>) -> Array2<f64>,
{
pub fn new(
x0: Array1<f64>,
t_span: [f64; 2],
n_brownian: usize,
f_drift: F,
g_diffusion: G,
) -> Self {
Self {
x0,
t_span,
n_brownian,
f_drift,
g_diffusion,
}
}
pub fn dim(&self) -> usize {
self.x0.len()
}
pub fn validate(&self) -> IntegrateResult<()> {
if self.t_span[0] >= self.t_span[1] {
return Err(IntegrateError::InvalidInput(format!(
"t_span must satisfy t0 < t1, got [{}, {}]",
self.t_span[0], self.t_span[1]
)));
}
if self.n_brownian == 0 {
return Err(IntegrateError::InvalidInput(
"n_brownian must be at least 1".to_string(),
));
}
if self.x0.is_empty() {
return Err(IntegrateError::InvalidInput(
"Initial state x0 must be non-empty".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SdeSolution {
pub t: Vec<f64>,
pub x: Vec<Array1<f64>>,
}
impl SdeSolution {
pub fn with_capacity(n: usize) -> Self {
Self {
t: Vec::with_capacity(n),
x: Vec::with_capacity(n),
}
}
pub fn push(&mut self, t: f64, x: Array1<f64>) {
self.t.push(t);
self.x.push(x);
}
pub fn len(&self) -> usize {
self.t.len()
}
pub fn is_empty(&self) -> bool {
self.t.is_empty()
}
pub fn t_final(&self) -> Option<f64> {
self.t.last().copied()
}
pub fn x_final(&self) -> Option<&Array1<f64>> {
self.x.last()
}
pub fn ensemble_mean(solutions: &[SdeSolution]) -> IntegrateResult<SdeSolution> {
if solutions.is_empty() {
return Err(IntegrateError::InvalidInput(
"Cannot compute mean of empty ensemble".to_string(),
));
}
let n_steps = solutions[0].len();
let n_ensemble = solutions.len();
let mut result = SdeSolution::with_capacity(n_steps);
for step in 0..n_steps {
let t = solutions[0].t[step];
let dim = solutions[0].x[step].len();
let mut mean_x = Array1::zeros(dim);
for sol in solutions {
if sol.len() != n_steps {
return Err(IntegrateError::DimensionMismatch(
"All solutions in ensemble must have the same number of steps".to_string(),
));
}
mean_x = mean_x + &sol.x[step];
}
mean_x /= n_ensemble as f64;
result.push(t, mean_x);
}
Ok(result)
}
pub fn ensemble_variance(solutions: &[SdeSolution]) -> IntegrateResult<SdeSolution> {
if solutions.is_empty() {
return Err(IntegrateError::InvalidInput(
"Cannot compute variance of empty ensemble".to_string(),
));
}
let n_steps = solutions[0].len();
let n_ensemble = solutions.len();
if n_ensemble < 2 {
return Err(IntegrateError::InvalidInput(
"Need at least 2 solutions to compute variance".to_string(),
));
}
let mean_sol = Self::ensemble_mean(solutions)?;
let mut result = SdeSolution::with_capacity(n_steps);
for step in 0..n_steps {
let t = solutions[0].t[step];
let dim = solutions[0].x[step].len();
let mut var_x = Array1::zeros(dim);
for sol in solutions {
let diff = &sol.x[step] - &mean_sol.x[step];
var_x = var_x + &diff.mapv(|v| v * v);
}
var_x /= (n_ensemble - 1) as f64;
result.push(t, var_x);
}
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct SdeOptions {
pub save_all_steps: bool,
pub max_steps: usize,
}
impl Default for SdeOptions {
fn default() -> Self {
Self {
save_all_steps: true,
max_steps: 10_000_000,
}
}
}
pub(crate) fn compute_n_steps(t0: f64, t1: f64, dt: f64, max_steps: usize) -> IntegrateResult<usize> {
if dt <= 0.0 {
return Err(IntegrateError::InvalidInput(format!(
"Step size dt must be positive, got {}",
dt
)));
}
let n = ((t1 - t0) / dt).ceil() as usize;
if n > max_steps {
return Err(IntegrateError::InvalidInput(format!(
"Required steps {} exceeds maximum {}", n, max_steps
)));
}
Ok(n.max(1))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
#[test]
fn test_sde_problem_creation() {
let x0 = array![1.0_f64];
let prob = SdeProblem::new(
x0,
[0.0, 1.0],
1,
|_t, x| x.clone(),
|_t, _x| Array2::eye(1),
);
assert_eq!(prob.dim(), 1);
assert_eq!(prob.n_brownian, 1);
prob.validate().expect("Validation should pass");
}
#[test]
fn test_sde_problem_invalid_tspan() {
let x0 = array![1.0_f64];
let prob = SdeProblem::new(
x0,
[1.0, 0.0], 1,
|_t, x| x.clone(),
|_t, _x| Array2::eye(1),
);
assert!(prob.validate().is_err());
}
#[test]
fn test_sde_solution_push_and_query() {
let mut sol = SdeSolution::with_capacity(3);
sol.push(0.0, array![1.0_f64]);
sol.push(0.5, array![1.1_f64]);
sol.push(1.0, array![1.2_f64]);
assert_eq!(sol.len(), 3);
assert!(!sol.is_empty());
assert!((sol.t_final().expect("solution has time steps") - 1.0).abs() < 1e-12);
assert!((sol.x_final().expect("solution has state")[0] - 1.2).abs() < 1e-12);
}
#[test]
fn test_ensemble_mean() {
let mut sol1 = SdeSolution::with_capacity(2);
sol1.push(0.0, array![1.0_f64]);
sol1.push(1.0, array![2.0_f64]);
let mut sol2 = SdeSolution::with_capacity(2);
sol2.push(0.0, array![1.0_f64]);
sol2.push(1.0, array![4.0_f64]);
let mean = SdeSolution::ensemble_mean(&[sol1, sol2]).expect("ensemble_mean should succeed");
assert!((mean.x[1][0] - 3.0).abs() < 1e-12);
}
#[test]
fn test_compute_n_steps() {
let n = compute_n_steps(0.0, 1.0, 0.1, 1000).expect("compute_n_steps should succeed");
assert_eq!(n, 10);
}
#[test]
fn test_compute_n_steps_invalid_dt() {
assert!(compute_n_steps(0.0, 1.0, -0.1, 1000).is_err());
assert!(compute_n_steps(0.0, 1.0, 0.0, 1000).is_err());
}
}