use numra_core::Scalar;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum NoiseType {
#[default]
Diagonal,
Scalar,
General { n_wiener: usize },
}
pub trait SdeSystem<S: Scalar>: Sync {
fn dim(&self) -> usize;
fn drift(&self, t: S, x: &[S], f: &mut [S]);
fn diffusion(&self, t: S, x: &[S], g: &mut [S]);
fn noise_type(&self) -> NoiseType {
NoiseType::Diagonal
}
fn n_wiener(&self) -> usize {
match self.noise_type() {
NoiseType::Diagonal => self.dim(),
NoiseType::Scalar => 1,
NoiseType::General { n_wiener } => n_wiener,
}
}
fn diffusion_derivative(&self, t: S, x: &[S], gdg: &mut [S]) {
let dim = self.dim();
let h_factor = S::EPSILON.sqrt();
let mut g = vec![S::ZERO; dim];
let mut g_plus = vec![S::ZERO; dim];
let mut x_pert = x.to_vec();
self.diffusion(t, x, &mut g);
for i in 0..dim {
let h = h_factor * (S::ONE + x[i].abs());
x_pert[i] = x[i] + h;
self.diffusion(t, &x_pert, &mut g_plus);
x_pert[i] = x[i];
gdg[i] = (g_plus[i] - g[i]) / h * g[i];
}
}
}
#[derive(Clone, Debug)]
pub struct SdeOptions<S: Scalar> {
pub dt: S,
pub rtol: S,
pub atol: S,
pub dt_max: S,
pub dt_min: S,
pub max_steps: usize,
pub save_trajectory: bool,
pub seed: Option<u64>,
}
impl<S: Scalar> Default for SdeOptions<S> {
fn default() -> Self {
Self {
dt: S::from_f64(0.01),
rtol: S::from_f64(1e-3),
atol: S::from_f64(1e-6),
dt_max: S::INFINITY,
dt_min: S::from_f64(1e-10),
max_steps: 1_000_000,
save_trajectory: true,
seed: None,
}
}
}
impl<S: Scalar> SdeOptions<S> {
pub fn dt(mut self, dt: S) -> Self {
self.dt = dt;
self
}
pub fn rtol(mut self, rtol: S) -> Self {
self.rtol = rtol;
self
}
pub fn atol(mut self, atol: S) -> Self {
self.atol = atol;
self
}
pub fn dt_max(mut self, dt_max: S) -> Self {
self.dt_max = dt_max;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn save_trajectory(mut self, save: bool) -> Self {
self.save_trajectory = save;
self
}
}
#[derive(Clone, Debug, Default)]
pub struct SdeStats {
pub n_drift: usize,
pub n_diffusion: usize,
pub n_accept: usize,
pub n_reject: usize,
}
#[derive(Clone, Debug)]
pub struct SdeResult<S: Scalar> {
pub t: Vec<S>,
pub y: Vec<S>,
pub dim: usize,
pub stats: SdeStats,
pub success: bool,
pub message: String,
}
impl<S: Scalar> SdeResult<S> {
pub fn new(t: Vec<S>, y: Vec<S>, dim: usize, stats: SdeStats) -> Self {
Self {
t,
y,
dim,
stats,
success: true,
message: String::new(),
}
}
pub fn failed(message: String, stats: SdeStats) -> Self {
Self {
t: Vec::new(),
y: Vec::new(),
dim: 0,
stats,
success: false,
message,
}
}
pub fn len(&self) -> usize {
self.t.len()
}
pub fn is_empty(&self) -> bool {
self.t.is_empty()
}
pub fn t_final(&self) -> Option<S> {
self.t.last().copied()
}
pub fn y_final(&self) -> Option<Vec<S>> {
if self.t.is_empty() {
None
} else {
let start = (self.t.len() - 1) * self.dim;
Some(self.y[start..start + self.dim].to_vec())
}
}
pub fn y_at(&self, i: usize) -> &[S] {
let start = i * self.dim;
&self.y[start..start + self.dim]
}
pub fn iter(&self) -> impl Iterator<Item = (S, &[S])> {
self.t
.iter()
.enumerate()
.map(move |(i, &t)| (t, self.y_at(i)))
}
}
pub trait SdeSolver<S: Scalar> {
fn solve<Sys: SdeSystem<S>>(
system: &Sys,
t0: S,
tf: S,
x0: &[S],
options: &SdeOptions<S>,
seed: Option<u64>,
) -> Result<SdeResult<S>, String>;
}
#[cfg(test)]
mod tests {
use super::*;
struct TestSde;
impl SdeSystem<f64> for TestSde {
fn dim(&self) -> usize {
1
}
fn drift(&self, _t: f64, x: &[f64], f: &mut [f64]) {
f[0] = -x[0];
}
fn diffusion(&self, _t: f64, x: &[f64], g: &mut [f64]) {
g[0] = 0.1 * x[0];
}
}
#[test]
fn test_sde_system_trait() {
let sys = TestSde;
assert_eq!(sys.dim(), 1);
assert_eq!(sys.n_wiener(), 1);
let mut f = [0.0];
let mut g = [0.0];
sys.drift(0.0, &[1.0], &mut f);
sys.diffusion(0.0, &[1.0], &mut g);
assert!((f[0] - (-1.0)).abs() < 1e-10);
assert!((g[0] - 0.1).abs() < 1e-10);
}
#[test]
fn test_sde_options() {
let opts: SdeOptions<f64> = SdeOptions::default().dt(0.001).seed(42);
assert!((opts.dt - 0.001).abs() < 1e-10);
assert_eq!(opts.seed, Some(42));
}
struct LinearDiffusionSde {
alpha: f64,
}
impl SdeSystem<f64> for LinearDiffusionSde {
fn dim(&self) -> usize {
2
}
fn drift(&self, _t: f64, _x: &[f64], f: &mut [f64]) {
f[0] = 0.0;
f[1] = 0.0;
}
fn diffusion(&self, _t: f64, x: &[f64], g: &mut [f64]) {
g[0] = self.alpha * x[0];
g[1] = self.alpha * x[1];
}
}
#[test]
fn test_diffusion_derivative_default_large_x_no_scaling_bug() {
let alpha = 0.5_f64;
let sys = LinearDiffusionSde { alpha };
let x = [1e8, 1e8];
let mut gdg = [0.0; 2];
sys.diffusion_derivative(0.0, &x, &mut gdg);
let expected = alpha * alpha * 1e8;
assert!(
(gdg[0] - expected).abs() < 1e-3 * expected.abs(),
"gdg[0] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
gdg[0],
expected
);
assert!(
(gdg[1] - expected).abs() < 1e-3 * expected.abs(),
"gdg[1] = {} should be ≈ {} (within 1e-3 relative); old unscaled formula returns 0",
gdg[1],
expected
);
}
}