Skip to main content

proof_engine/stochastic/
brownian.rs

1//! Brownian motion / Wiener process implementations.
2//!
3//! Provides standard Brownian motion in arbitrary dimensions, 2D convenience
4//! wrappers returning `glam::Vec2`, Brownian bridges, fractional Brownian
5//! motion, and a glyph-based renderer.
6
7use glam::Vec2;
8
9// ---------------------------------------------------------------------------
10// LCG-based RNG
11// ---------------------------------------------------------------------------
12
13/// Simple linear congruential generator for reproducible stochastic simulations.
14/// Uses the Numerical Recipes constants: a = 1664525, c = 1013904223, m = 2^32.
15pub struct Rng {
16    state: u64,
17}
18
19impl Rng {
20    /// Create a new RNG with the given seed.
21    pub fn new(seed: u64) -> Self {
22        Self { state: seed.wrapping_add(1) }
23    }
24
25    /// Return the next raw u32 value.
26    pub fn next_u32(&mut self) -> u32 {
27        // LCG step
28        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
29        ((self.state >> 33) ^ (self.state >> 17)) as u32
30    }
31
32    /// Uniform random f64 in [0, 1).
33    pub fn uniform(&mut self) -> f64 {
34        (self.next_u32() as f64) / (u32::MAX as f64 + 1.0)
35    }
36
37    /// Standard normal via Box-Muller transform.
38    pub fn normal(&mut self) -> f64 {
39        let u1 = self.uniform().max(1e-15);
40        let u2 = self.uniform();
41        (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
42    }
43
44    /// Normal with given mean and standard deviation.
45    pub fn normal_with(&mut self, mean: f64, std_dev: f64) -> f64 {
46        mean + std_dev * self.normal()
47    }
48}
49
50// ---------------------------------------------------------------------------
51// BrownianMotion (n-dimensional)
52// ---------------------------------------------------------------------------
53
54/// Standard Brownian motion (Wiener process) in arbitrary dimension.
55///
56/// Each coordinate performs an independent random walk with increments
57/// drawn from N(0, variance * dt).
58pub struct BrownianMotion {
59    /// Number of spatial dimensions.
60    pub dimension: usize,
61    /// Time step size.
62    pub dt: f64,
63    /// Per-step variance multiplier (default 1.0 for standard Wiener process).
64    pub variance: f64,
65}
66
67impl BrownianMotion {
68    /// Create a new standard Brownian motion.
69    pub fn new(dimension: usize, dt: f64) -> Self {
70        Self { dimension, dt, variance: 1.0 }
71    }
72
73    /// Create with custom variance.
74    pub fn with_variance(dimension: usize, dt: f64, variance: f64) -> Self {
75        Self { dimension, dt, variance }
76    }
77
78    /// Single step: returns the increment dW as a Vec<f64>.
79    pub fn step(&self, rng: &mut Rng) -> Vec<f64> {
80        let scale = (self.variance * self.dt).sqrt();
81        (0..self.dimension).map(|_| rng.normal() * scale).collect()
82    }
83
84    /// Generate a full trajectory of the given number of steps, starting at the origin.
85    pub fn path(&self, rng: &mut Rng, steps: usize) -> Vec<Vec<f64>> {
86        let mut trajectory = Vec::with_capacity(steps + 1);
87        let mut current = vec![0.0; self.dimension];
88        trajectory.push(current.clone());
89        for _ in 0..steps {
90            let dw = self.step(rng);
91            for (c, d) in current.iter_mut().zip(dw.iter()) {
92                *c += d;
93            }
94            trajectory.push(current.clone());
95        }
96        trajectory
97    }
98
99    /// Generate a path starting from a given position.
100    pub fn path_from(&self, rng: &mut Rng, start: &[f64], steps: usize) -> Vec<Vec<f64>> {
101        let mut trajectory = Vec::with_capacity(steps + 1);
102        let mut current = start.to_vec();
103        trajectory.push(current.clone());
104        for _ in 0..steps {
105            let dw = self.step(rng);
106            for (c, d) in current.iter_mut().zip(dw.iter()) {
107                *c += d;
108            }
109            trajectory.push(current.clone());
110        }
111        trajectory
112    }
113}
114
115// ---------------------------------------------------------------------------
116// BrownianMotion2D — convenience wrapper returning Vec2
117// ---------------------------------------------------------------------------
118
119/// 2D Brownian motion returning `glam::Vec2` positions.
120pub struct BrownianMotion2D {
121    pub dt: f64,
122    pub variance: f64,
123}
124
125impl BrownianMotion2D {
126    pub fn new(dt: f64) -> Self {
127        Self { dt, variance: 1.0 }
128    }
129
130    pub fn with_variance(dt: f64, variance: f64) -> Self {
131        Self { dt, variance }
132    }
133
134    /// Single step increment as Vec2.
135    pub fn step(&self, rng: &mut Rng) -> Vec2 {
136        let scale = (self.variance * self.dt).sqrt() as f32;
137        Vec2::new(rng.normal() as f32 * scale, rng.normal() as f32 * scale)
138    }
139
140    /// Full 2D trajectory starting at the origin.
141    pub fn path(&self, rng: &mut Rng, steps: usize) -> Vec<Vec2> {
142        let mut trajectory = Vec::with_capacity(steps + 1);
143        let mut pos = Vec2::ZERO;
144        trajectory.push(pos);
145        for _ in 0..steps {
146            pos += self.step(rng);
147            trajectory.push(pos);
148        }
149        trajectory
150    }
151
152    /// Full 2D trajectory starting from a given position.
153    pub fn path_from(&self, rng: &mut Rng, start: Vec2, steps: usize) -> Vec<Vec2> {
154        let mut trajectory = Vec::with_capacity(steps + 1);
155        let mut pos = start;
156        trajectory.push(pos);
157        for _ in 0..steps {
158            pos += self.step(rng);
159            trajectory.push(pos);
160        }
161        trajectory
162    }
163}
164
165// ---------------------------------------------------------------------------
166// BrownianBridge
167// ---------------------------------------------------------------------------
168
169/// Brownian bridge: a Brownian motion conditioned to reach a specified endpoint.
170///
171/// B(t) = (1 - t/T)*start + (t/T)*end + W(t) - (t/T)*W(T)
172/// which is implemented by generating a free Wiener path then pinning it.
173pub struct BrownianBridge {
174    /// Starting value.
175    pub start: f64,
176    /// Ending value (the bridge is conditioned to hit this).
177    pub end: f64,
178    /// Number of discrete steps.
179    pub steps: usize,
180}
181
182impl BrownianBridge {
183    pub fn new(start: f64, end: f64, steps: usize) -> Self {
184        Self { start, end, steps }
185    }
186
187    /// Generate the bridge path as a vector of length `steps + 1`.
188    pub fn path(&self, rng: &mut Rng) -> Vec<f64> {
189        let n = self.steps;
190        if n == 0 {
191            return vec![self.start];
192        }
193
194        let dt = 1.0 / n as f64;
195        let scale = dt.sqrt();
196
197        // Generate free Wiener path
198        let mut w = Vec::with_capacity(n + 1);
199        w.push(0.0);
200        for _ in 0..n {
201            let prev = *w.last().unwrap();
202            w.push(prev + rng.normal() * scale);
203        }
204        let w_t = w[n];
205
206        // Pin: B(k) = start + (end - start) * (k/n) + W(k) - (k/n) * W(T)
207        let mut bridge = Vec::with_capacity(n + 1);
208        for k in 0..=n {
209            let t_frac = k as f64 / n as f64;
210            let val = self.start + (self.end - self.start) * t_frac + w[k] - t_frac * w_t;
211            bridge.push(val);
212        }
213        bridge
214    }
215
216    /// Generate a 2D bridge path (each coordinate independently bridged).
217    pub fn path_2d(&self, rng: &mut Rng, start: Vec2, end: Vec2) -> Vec<Vec2> {
218        let bx = BrownianBridge::new(start.x as f64, end.x as f64, self.steps);
219        let by = BrownianBridge::new(start.y as f64, end.y as f64, self.steps);
220        let px = bx.path(rng);
221        let py = by.path(rng);
222        px.iter()
223            .zip(py.iter())
224            .map(|(&x, &y)| Vec2::new(x as f32, y as f32))
225            .collect()
226    }
227}
228
229// ---------------------------------------------------------------------------
230// Fractional Brownian Motion
231// ---------------------------------------------------------------------------
232
233/// Generate fractional Brownian motion with Hurst parameter H in (0, 1).
234///
235/// Uses the Cholesky decomposition of the covariance matrix:
236/// Cov(B_H(s), B_H(t)) = 0.5 * (|s|^{2H} + |t|^{2H} - |t-s|^{2H})
237pub fn fractional_brownian(h: f64, steps: usize, rng: &mut Rng) -> Vec<f64> {
238    if steps == 0 {
239        return vec![0.0];
240    }
241
242    let n = steps;
243    let two_h = 2.0 * h;
244
245    // Build the covariance matrix
246    let mut cov = vec![vec![0.0; n]; n];
247    for i in 0..n {
248        for j in 0..n {
249            let si = (i + 1) as f64;
250            let sj = (j + 1) as f64;
251            let diff = (si - sj).abs();
252            cov[i][j] = 0.5 * (si.powf(two_h) + sj.powf(two_h) - diff.powf(two_h));
253        }
254    }
255
256    // Cholesky decomposition (covariance matrix is symmetric positive definite)
257    let mut l = vec![vec![0.0; n]; n];
258    for i in 0..n {
259        for j in 0..=i {
260            let mut sum = 0.0;
261            for k in 0..j {
262                sum += l[i][k] * l[j][k];
263            }
264            if i == j {
265                let diag = cov[i][i] - sum;
266                l[i][j] = if diag > 0.0 { diag.sqrt() } else { 0.0 };
267            } else {
268                l[i][j] = if l[j][j].abs() > 1e-15 {
269                    (cov[i][j] - sum) / l[j][j]
270                } else {
271                    0.0
272                };
273            }
274        }
275    }
276
277    // Generate independent normals
278    let z: Vec<f64> = (0..n).map(|_| rng.normal()).collect();
279
280    // Multiply L * z to get correlated increments, then cumsum
281    let mut path = Vec::with_capacity(n + 1);
282    path.push(0.0);
283    for i in 0..n {
284        let mut val = 0.0;
285        for j in 0..=i {
286            val += l[i][j] * z[j];
287        }
288        path.push(val);
289    }
290    path
291}
292
293// ---------------------------------------------------------------------------
294// BrownianRenderer
295// ---------------------------------------------------------------------------
296
297/// Render a Brownian path as a connected trail of glyphs.
298pub struct BrownianRenderer {
299    /// Character to use for the trail.
300    pub character: char,
301    /// Base color (r, g, b, a).
302    pub color: [f32; 4],
303    /// Glow radius for each glyph.
304    pub glow_radius: f32,
305}
306
307impl BrownianRenderer {
308    pub fn new() -> Self {
309        Self {
310            character: '·',
311            color: [0.4, 0.8, 1.0, 1.0],
312            glow_radius: 0.6,
313        }
314    }
315
316    pub fn with_character(mut self, ch: char) -> Self {
317        self.character = ch;
318        self
319    }
320
321    pub fn with_color(mut self, color: [f32; 4]) -> Self {
322        self.color = color;
323        self
324    }
325
326    /// Convert a 2D path into glyph data: Vec of (position, character, color).
327    pub fn render_path_2d(&self, path: &[Vec2]) -> Vec<(Vec2, char, [f32; 4])> {
328        let len = path.len();
329        if len == 0 {
330            return Vec::new();
331        }
332
333        let mut glyphs = Vec::with_capacity(len);
334        for (i, &pos) in path.iter().enumerate() {
335            // Fade alpha along the trail
336            let alpha = (i as f32 / len as f32) * self.color[3];
337            let color = [self.color[0], self.color[1], self.color[2], alpha];
338            glyphs.push((pos, self.character, color));
339        }
340        glyphs
341    }
342
343    /// Convert a generic n-dimensional path (projected to first 2 dims) into glyph data.
344    pub fn render_path_nd(&self, path: &[Vec<f64>]) -> Vec<(Vec2, char, [f32; 4])> {
345        let len = path.len();
346        if len == 0 {
347            return Vec::new();
348        }
349
350        let mut glyphs = Vec::with_capacity(len);
351        for (i, point) in path.iter().enumerate() {
352            let x = point.first().copied().unwrap_or(0.0) as f32;
353            let y = point.get(1).copied().unwrap_or(0.0) as f32;
354            let pos = Vec2::new(x, y);
355            let alpha = (i as f32 / len as f32) * self.color[3];
356            let color = [self.color[0], self.color[1], self.color[2], alpha];
357            glyphs.push((pos, self.character, color));
358        }
359        glyphs
360    }
361
362    /// Render a 1D path as a line chart (index on x-axis, value on y-axis).
363    pub fn render_path_1d(&self, path: &[f64], x_scale: f32, y_scale: f32) -> Vec<(Vec2, char, [f32; 4])> {
364        let len = path.len();
365        if len == 0 {
366            return Vec::new();
367        }
368
369        let mut glyphs = Vec::with_capacity(len);
370        for (i, &val) in path.iter().enumerate() {
371            let pos = Vec2::new(i as f32 * x_scale, val as f32 * y_scale);
372            let alpha = ((i as f32 + 1.0) / len as f32) * self.color[3];
373            let color = [self.color[0], self.color[1], self.color[2], alpha];
374            glyphs.push((pos, self.character, color));
375        }
376        glyphs
377    }
378}
379
380impl Default for BrownianRenderer {
381    fn default() -> Self {
382        Self::new()
383    }
384}
385
386// ---------------------------------------------------------------------------
387// Tests
388// ---------------------------------------------------------------------------
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_rng_uniform_range() {
396        let mut rng = Rng::new(42);
397        for _ in 0..1000 {
398            let u = rng.uniform();
399            assert!(u >= 0.0 && u < 1.0, "uniform out of range: {}", u);
400        }
401    }
402
403    #[test]
404    fn test_rng_normal_mean_and_variance() {
405        let mut rng = Rng::new(123);
406        let n = 50_000;
407        let samples: Vec<f64> = (0..n).map(|_| rng.normal()).collect();
408        let mean = samples.iter().sum::<f64>() / n as f64;
409        let var = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
410        assert!(mean.abs() < 0.05, "normal mean too far from 0: {}", mean);
411        assert!((var - 1.0).abs() < 0.1, "normal variance too far from 1: {}", var);
412    }
413
414    #[test]
415    fn test_brownian_motion_dimensions() {
416        let bm = BrownianMotion::new(3, 0.01);
417        let mut rng = Rng::new(7);
418        let step = bm.step(&mut rng);
419        assert_eq!(step.len(), 3);
420
421        let path = bm.path(&mut rng, 100);
422        assert_eq!(path.len(), 101);
423        assert_eq!(path[0], vec![0.0, 0.0, 0.0]);
424    }
425
426    #[test]
427    fn test_brownian_variance_scales_with_time() {
428        // For standard BM, Var(W(t)) = t. After N steps of size dt, t = N*dt.
429        let dt = 0.01;
430        let steps = 1000; // t = 10.0
431        let trials = 2000;
432        let mut rng = Rng::new(999);
433        let bm = BrownianMotion::new(1, dt);
434
435        let mut endpoints = Vec::with_capacity(trials);
436        for _ in 0..trials {
437            let path = bm.path(&mut rng, steps);
438            endpoints.push(path[steps][0]);
439        }
440
441        let mean = endpoints.iter().sum::<f64>() / trials as f64;
442        let var = endpoints.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / trials as f64;
443        let expected_var = dt * steps as f64; // = 10.0
444
445        assert!(mean.abs() < 0.5, "BM mean should be ~0, got {}", mean);
446        assert!(
447            (var - expected_var).abs() < 2.0,
448            "BM variance should be ~{}, got {}",
449            expected_var,
450            var
451        );
452    }
453
454    #[test]
455    fn test_brownian_motion_2d() {
456        let bm = BrownianMotion2D::new(0.01);
457        let mut rng = Rng::new(55);
458        let path = bm.path(&mut rng, 200);
459        assert_eq!(path.len(), 201);
460        assert_eq!(path[0], Vec2::ZERO);
461    }
462
463    #[test]
464    fn test_brownian_bridge_endpoints() {
465        let bridge = BrownianBridge::new(0.0, 5.0, 500);
466        let mut rng = Rng::new(42);
467        let path = bridge.path(&mut rng);
468        assert_eq!(path.len(), 501);
469        assert!((path[0] - 0.0).abs() < 1e-10, "bridge should start at 0");
470        assert!((path[500] - 5.0).abs() < 1e-10, "bridge should end at 5, got {}", path[500]);
471    }
472
473    #[test]
474    fn test_fractional_brownian_standard() {
475        // H = 0.5 should behave like standard Brownian motion
476        let mut rng = Rng::new(42);
477        let path = fractional_brownian(0.5, 100, &mut rng);
478        assert_eq!(path.len(), 101);
479        assert_eq!(path[0], 0.0);
480    }
481
482    #[test]
483    fn test_fractional_brownian_persistent() {
484        // H > 0.5 should produce positively correlated increments (persistent)
485        let mut rng = Rng::new(42);
486        let path = fractional_brownian(0.8, 200, &mut rng);
487        assert_eq!(path.len(), 201);
488        assert_eq!(path[0], 0.0);
489        // Just verify it runs and produces values
490        assert!(path.iter().any(|&v| v != 0.0));
491    }
492
493    #[test]
494    fn test_fractional_brownian_antipersistent() {
495        // H < 0.5 should produce negatively correlated increments
496        let mut rng = Rng::new(42);
497        let path = fractional_brownian(0.2, 200, &mut rng);
498        assert_eq!(path.len(), 201);
499        assert_eq!(path[0], 0.0);
500    }
501
502    #[test]
503    fn test_brownian_renderer() {
504        let renderer = BrownianRenderer::new().with_character('*');
505        let bm = BrownianMotion2D::new(0.01);
506        let mut rng = Rng::new(10);
507        let path = bm.path(&mut rng, 50);
508        let glyphs = renderer.render_path_2d(&path);
509        assert_eq!(glyphs.len(), 51);
510        assert_eq!(glyphs[0].1, '*');
511    }
512
513    #[test]
514    fn test_brownian_bridge_2d() {
515        let bridge = BrownianBridge::new(0.0, 0.0, 100);
516        let mut rng = Rng::new(77);
517        let start = Vec2::new(1.0, 2.0);
518        let end = Vec2::new(5.0, 8.0);
519        let path = bridge.path_2d(&mut rng, start, end);
520        assert_eq!(path.len(), 101);
521        assert!((path[0].x - 1.0).abs() < 1e-4);
522        assert!((path[0].y - 2.0).abs() < 1e-4);
523        assert!((path[100].x - 5.0).abs() < 1e-4);
524        assert!((path[100].y - 8.0).abs() < 1e-4);
525    }
526
527    #[test]
528    fn test_render_path_1d() {
529        let renderer = BrownianRenderer::new();
530        let path = vec![0.0, 1.0, -0.5, 2.0];
531        let glyphs = renderer.render_path_1d(&path, 1.0, 1.0);
532        assert_eq!(glyphs.len(), 4);
533        assert!((glyphs[0].0.x - 0.0).abs() < 1e-5);
534        assert!((glyphs[2].0.y - (-0.5)).abs() < 1e-5);
535    }
536}