Skip to main content

pathwise_core/scheme/
sri.rs

1// pathwise-core/src/scheme/sri.rs
2use super::Scheme;
3use crate::process::markov::Drift;
4use crate::state::{Diffusion, Increment};
5
6/// Kloeden-Platen strong order 1.5 Taylor scheme for scalar autonomous SDEs.
7///
8/// For the Ito SDE `dX = f(X) dt + g(X) dW`, the step is:
9///
10///   x_{n+1} = x + f*dt + g*dW
11///             + 0.5*g*g'*(dW^2 - dt)                         [Milstein: L^1 g * I_{(1,1)}]
12///             + g*f' * (dt*dW - dZ)                           [L^1 f * I_{(1,0)} term]
13///             + (f*g' + 0.5*g^2*g'') * dZ                    [L^0 g * I_{(0,1)} term]
14///             + 0.5*f*f' * dt^2                               [L^0 f * dt^2/2 term]
15///             + (g*g'^2 + g^2*g'') * (dW^3 - 3*dt*dW)/6     [(L^1)^2 g * I_{(1,1,1)}]
16///
17/// where:
18///   `dZ = integral_0^dt W(s) ds = I_{(0,1)}`
19///   `I_{(1,0)} = dt*dW - dZ`
20///   `I_{(1,1)} = (dW^2 - dt)/2`
21///   `I_{(1,1,1)} = (dW^3 - 3*dt*dW)/6`
22///
23/// All spatial derivatives are approximated by central finite differences with step `h`.
24///
25/// # Reference
26/// Kloeden & Platen, "Numerical Solution of Stochastic Differential Equations", 1992,
27/// Chapter 5.5, the strong Taylor 1.5 scheme (Theorem 5.5.1 / eq. 5.5.4).
28///
29/// # Notes
30/// - The `I_{(1,1,1)}` triple iterated integral is expressible in terms of `dW` alone:
31///   `I_{(1,1,1)} = (dW^3 - 3*dt*dW)/6`. This is the key 1.5-order correction beyond Milstein.
32/// - For BM (constant diffusion), all correction terms vanish, recovering Euler.
33/// - For GBM (`g = sigma*x`, `g' = sigma`, `g'' = 0`):
34///   - `d111 = g*g'^2 + g^2*g'' = sigma^2*x*sigma = sigma^3*x`
35///   - `I_{(1,1,1)} = (dW^3 - 3*dt*dW)/6`
36///   - Combined with the other 1.5 terms, local error reduces to O(dt^2), giving O(dt^{1.5}) globally.
37/// - Requires scalar (1D) SDE with diagonal noise.
38pub struct Sri {
39    h: f64,
40}
41
42impl Sri {
43    pub fn new(h: f64) -> Self {
44        Self { h }
45    }
46}
47
48impl Scheme<f64> for Sri {
49    type Noise = f64;
50
51    fn step<D, G>(
52        &self,
53        drift: &D,
54        diffusion: &G,
55        x: &f64,
56        t: f64,
57        dt: f64,
58        inc: &Increment<f64>,
59    ) -> f64
60    where
61        D: Drift<f64>,
62        G: Diffusion<f64, f64>,
63    {
64        let dw = inc.dw;
65        let dz = inc.dz; // I_{(0,1)} = integral_0^dt W(s) ds
66        let h = self.h;
67
68        // f(x,t) and its central finite difference
69        let f = drift(x, t);
70        let f_plus = drift(&(x + h), t);
71        let f_minus = drift(&(x - h), t);
72        let df_dx = (f_plus - f_minus) / (2.0 * h);
73
74        // g(x,t) and its finite-difference derivatives (unit noise via blanket impl)
75        let g = diffusion.apply(x, t, &1.0_f64);
76        let g_plus = diffusion.apply(&(x + h), t, &1.0_f64);
77        let g_minus = diffusion.apply(&(x - h), t, &1.0_f64);
78        let dg_dx = (g_plus - g_minus) / (2.0 * h);
79        let d2g_dx2 = (g_plus - 2.0 * g + g_minus) / (h * h);
80
81        // I_{(1,0)} = dt*dW - I_{(0,1)} = dt*dW - dZ
82        let i10 = dt * dw - dz;
83        // I_{(1,1)} = (dW^2 - dt)/2  (Milstein)
84        let i11 = (dw * dw - dt) * 0.5;
85        // I_{(1,1,1)} = (dW^3 - 3*dt*dW)/6  (triple iterated Ito integral, expressed via dW)
86        let i111 = (dw * dw * dw - 3.0 * dt * dw) / 6.0;
87
88        // Milstein correction: L^1 g * I_{(1,1)}
89        let milstein = g * dg_dx * i11;
90
91        // L^1 f * I_{(1,0)}: g * f' * (dt*dW - dZ)
92        let term_l1f = g * df_dx * i10;
93
94        // L^0 g * I_{(0,1)}: (f * g' + 0.5 * g^2 * g'') * dZ
95        let term_l0g = (f * dg_dx + 0.5 * g * g * d2g_dx2) * dz;
96
97        // L^0 f * dt^2/2: f * f' * dt^2/2
98        let term_l0f = 0.5 * f * df_dx * dt * dt;
99
100        // (L^1)^2 g * I_{(1,1,1)}: (g*g'^2 + g^2*g'') * (dW^3 - 3*dt*dW)/6
101        let d111 = g * dg_dx * dg_dx + g * g * d2g_dx2;
102        let term_l11g = d111 * i111;
103
104        x + f * dt + g * dw + milstein + term_l1f + term_l0g + term_l0f + term_l11g
105    }
106}
107
108pub fn sri() -> Sri {
109    Sri::new(1e-4)
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use crate::process::markov::bm;
116    use crate::state::Increment;
117
118    #[test]
119    fn sri_equals_euler_for_constant_diffusion() {
120        // For constant g: g'=0, g''=0, so all derivative-based corrections vanish.
121        // For BM: f=0, so L^0f and L^1f terms also vanish. SRI == Euler for BM.
122        let b = bm();
123        let s = sri();
124        let e = crate::scheme::euler::euler();
125        let x = 1.0_f64;
126        let inc = Increment { dw: 0.3, dz: 0.001 };
127        let x_euler = e.step(&b.drift, &b.diffusion, &x, 0.0, 0.01, &inc);
128        let x_sri = s.step(&b.drift, &b.diffusion, &x, 0.0, 0.01, &inc);
129        assert!(
130            (x_euler - x_sri).abs() < 1e-6,
131            "BM: euler={} sri={}",
132            x_euler,
133            x_sri
134        );
135    }
136
137    #[test]
138    fn sri_differs_from_milstein_for_state_dependent_diffusion() {
139        // GBM: f'=mu, g'=sigma, g''=0.
140        // SRI adds L^1 f (drift-noise cross), L^0 g (noise-drift), and I_{(1,1,1)} terms.
141        let gbm = crate::process::markov::gbm(0.05, 0.3);
142        let s = sri();
143        let m = crate::scheme::milstein::milstein();
144        let x = 1.0_f64;
145        let inc = Increment {
146            dw: 0.05,
147            dz: 0.0001,
148        };
149        let dt = 0.01;
150        let x_sri = s.step(&gbm.drift, &gbm.diffusion, &x, 0.0, dt, &inc);
151        let x_mil = m.step(&gbm.drift, &gbm.diffusion, &x, 0.0, dt, &inc);
152        // They differ because SRI includes the I_{(1,0)}, I_{(0,1)}, and I_{(1,1,1)} terms
153        assert!(
154            (x_sri - x_mil).abs() > 1e-10,
155            "SRI and Milstein should differ"
156        );
157    }
158}