Skip to main content

lau_diffusion_agents/
brownian.rs

1//! Brownian motion on manifolds, Wiener process, stochastic calculus, Ito's lemma.
2
3use nalgebra::DVector;
4use rand::Rng;
5use serde::{Serialize, Deserialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct BrownianConfig {
9    pub dimension: usize,
10    pub dt: f64,
11    pub sigma: f64,
12    pub drift: f64,
13}
14
15impl Default for BrownianConfig {
16    fn default() -> Self {
17        Self { dimension: 1, dt: 0.01, sigma: 1.0, drift: 0.0 }
18    }
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct WienerStep {
23    pub time: f64,
24    pub position: DVector<f64>,
25    pub increment: DVector<f64>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct BrownianPath {
30    pub steps: Vec<WienerStep>,
31    pub config: BrownianConfig,
32}
33
34impl BrownianPath {
35    pub fn simulate(config: &BrownianConfig, n_steps: usize) -> Self {
36        let mut rng = rand::thread_rng();
37        let mut steps = Vec::with_capacity(n_steps);
38        let mut position = DVector::zeros(config.dimension);
39        let sqrt_dt = config.dt.sqrt();
40
41        for i in 0..n_steps {
42            let t = i as f64 * config.dt;
43            let mut inc = DVector::zeros(config.dimension);
44            for j in 0..config.dimension {
45                let z: f64 = rng.gen_range(-1.0..1.0);
46                inc[j] = config.sigma * sqrt_dt * z * std::f64::consts::SQRT_2
47                    + config.drift * config.dt;
48            }
49            position += &inc;
50            steps.push(WienerStep { time: t, position: position.clone(), increment: inc });
51        }
52        BrownianPath { steps, config: config.clone() }
53    }
54
55    pub fn final_position(&self) -> Option<&DVector<f64>> {
56        self.steps.last().map(|s| &s.position)
57    }
58
59    pub fn quadratic_variation(&self) -> f64 {
60        self.steps.iter().map(|s| s.increment.iter().map(|x| x * x).sum::<f64>()).sum()
61    }
62}
63
64pub fn ito_lemma(f_value: f64, f_t: f64, f_w: f64, f_ww: f64, dt: f64, dw: f64) -> f64 {
65    f_value + f_t * dt + f_w * dw + 0.5 * f_ww * dw * dw
66}
67
68pub fn ito_integral(f_values: &[f64], increments: &[f64]) -> f64 {
69    f_values.iter().zip(increments.iter()).map(|(f, dw)| f * dw).sum()
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct GeometricBrownian {
74    pub initial_value: f64,
75    pub mu: f64,
76    pub sigma: f64,
77    pub path: Vec<(f64, f64)>,
78}
79
80impl GeometricBrownian {
81    pub fn simulate(s0: f64, mu: f64, sigma: f64, dt: f64, n_steps: usize) -> Self {
82        let mut rng = rand::thread_rng();
83        let mut path = Vec::with_capacity(n_steps + 1);
84        let mut s = s0;
85        path.push((0.0, s));
86        let sqrt_dt = dt.sqrt();
87
88        for i in 1..=n_steps {
89            let z: f64 = rng.gen_range(-1.0..1.0);
90            let dw = z * sqrt_dt * std::f64::consts::SQRT_2;
91            s = s * ((mu - 0.5 * sigma * sigma) * dt + sigma * dw).exp();
92            path.push((i as f64 * dt, s));
93        }
94        GeometricBrownian { initial_value: s0, mu, sigma, path }
95    }
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct OrnsteinUhlenbeck {
100    pub theta: f64,
101    pub mu: f64,
102    pub sigma: f64,
103    pub path: Vec<(f64, f64)>,
104}
105
106impl OrnsteinUhlenbeck {
107    pub fn simulate(x0: f64, theta: f64, mu: f64, sigma: f64, dt: f64, n_steps: usize) -> Self {
108        let mut rng = rand::thread_rng();
109        let mut path = Vec::with_capacity(n_steps + 1);
110        let mut x = x0;
111        path.push((0.0, x));
112        let sqrt_dt = dt.sqrt();
113
114        for i in 1..=n_steps {
115            let z: f64 = rng.gen_range(-1.0..1.0);
116            let dw = z * sqrt_dt * std::f64::consts::SQRT_2;
117            x = x + theta * (mu - x) * dt + sigma * dw;
118            path.push((i as f64 * dt, x));
119        }
120        OrnsteinUhlenbeck { theta, mu, sigma, path }
121    }
122
123    pub fn stationary_variance(&self) -> f64 {
124        self.sigma * self.sigma / (2.0 * self.theta)
125    }
126}
127
128pub fn reflecting_brownian(x0: f64, sigma: f64, dt: f64, n_steps: usize, lower: f64, upper: f64) -> Vec<(f64, f64)> {
129    let mut rng = rand::thread_rng();
130    let mut path = Vec::with_capacity(n_steps + 1);
131    let mut x = x0;
132    let sqrt_dt = dt.sqrt();
133    path.push((0.0, x));
134
135    for i in 1..=n_steps {
136        let z: f64 = rng.gen_range(-1.0..1.0);
137        x += sigma * z * sqrt_dt * std::f64::consts::SQRT_2;
138        while x < lower || x > upper {
139            if x < lower { x = 2.0 * lower - x; }
140            if x > upper { x = 2.0 * upper - x; }
141        }
142        path.push((i as f64 * dt, x));
143    }
144    path
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use approx::assert_relative_eq;
151
152    #[test]
153    fn test_brownian_default_config() {
154        let cfg = BrownianConfig::default();
155        assert_eq!(cfg.dimension, 1);
156        assert_eq!(cfg.sigma, 1.0);
157        assert_eq!(cfg.drift, 0.0);
158    }
159
160    #[test]
161    fn test_brownian_simulation() {
162        let cfg = BrownianConfig { dimension: 2, dt: 0.01, sigma: 1.0, drift: 0.0 };
163        let path = BrownianPath::simulate(&cfg, 100);
164        assert_eq!(path.steps.len(), 100);
165        assert!(path.final_position().is_some());
166    }
167
168    #[test]
169    fn test_quadratic_variation() {
170        let cfg = BrownianConfig { dimension: 1, dt: 0.001, sigma: 1.0, drift: 0.0 };
171        let path = BrownianPath::simulate(&cfg, 10000);
172        let qv = path.quadratic_variation();
173        let t = 10000.0 * 0.001;
174        assert!(qv > 0.0 && (qv - t).abs() / t < 1.0, "QV={} vs T={}", qv, t);
175    }
176
177    #[test]
178    fn test_ito_lemma_constant() {
179        let result = ito_lemma(0.0, 0.0, 1.0, 0.0, 0.01, 0.1);
180        assert_relative_eq!(result, 0.1, epsilon = 1e-10);
181    }
182
183    #[test]
184    fn test_ito_lemma_quadratic() {
185        let w = 1.0;
186        let dw = 0.05;
187        let result = ito_lemma(w * w, 1.0, 2.0 * w, 2.0, 0.01, dw);
188        let expected = w * w + 0.01 + 2.0 * w * dw + 0.5 * 2.0 * dw * dw;
189        assert_relative_eq!(result, expected, epsilon = 1e-10);
190    }
191
192    #[test]
193    fn test_ito_integral() {
194        let f = vec![1.0, 2.0, 3.0];
195        let dw = vec![0.1, -0.2, 0.3];
196        let result = ito_integral(&f, &dw);
197        assert_relative_eq!(result, 1.0 * 0.1 + 2.0 * (-0.2) + 3.0 * 0.3);
198    }
199
200    #[test]
201    fn test_geometric_brownian() {
202        let gbm = GeometricBrownian::simulate(100.0, 0.05, 0.2, 0.01, 1000);
203        assert_eq!(gbm.path.len(), 1001);
204        assert_relative_eq!(gbm.path[0].1, 100.0);
205        assert!(gbm.path.last().unwrap().1 > 0.0);
206    }
207
208    #[test]
209    fn test_ou_stationary_variance() {
210        let ou = OrnsteinUhlenbeck { theta: 1.0, mu: 0.0, sigma: 1.0, path: vec![] };
211        let var = ou.stationary_variance();
212        assert_relative_eq!(var, 0.5, epsilon = 1e-10);
213    }
214
215    #[test]
216    fn test_ou_simulation() {
217        let ou = OrnsteinUhlenbeck::simulate(5.0, 1.0, 0.0, 1.0, 0.01, 500);
218        assert_eq!(ou.path.len(), 501);
219        assert_relative_eq!(ou.path[0].1, 5.0);
220    }
221
222    #[test]
223    fn test_reflecting_brownian_bounds() {
224        let path = reflecting_brownian(0.5, 1.0, 0.01, 1000, 0.0, 1.0);
225        for (_, x) in &path {
226            assert!(*x >= -1e-10 && *x <= 1.0 + 1e-10, "x={}", x);
227        }
228    }
229}