Skip to main content

proof_engine/stochastic/
levy.rs

1//! Lévy flights: heavy-tailed random walks using alpha-stable distributions.
2//!
3//! Implements the Chambers-Mallows-Stuck algorithm for stable random variates,
4//! 2D Lévy flights, and the Cauchy flight special case.
5
6use super::brownian::Rng;
7use glam::Vec2;
8
9// ---------------------------------------------------------------------------
10// Stable distribution sampling
11// ---------------------------------------------------------------------------
12
13/// Generate a random variate from a symmetric alpha-stable distribution
14/// using the Chambers-Mallows-Stuck (CMS) algorithm.
15///
16/// * `alpha` - stability parameter in (0, 2]. alpha=2 is Gaussian, alpha=1 is Cauchy.
17/// * `rng` - random number generator
18///
19/// Returns a sample from S(alpha, 0, 1, 0) (symmetric, unit scale).
20pub fn stable_random(alpha: f64, rng: &mut Rng) -> f64 {
21    assert!(alpha > 0.0 && alpha <= 2.0, "alpha must be in (0, 2]");
22
23    if (alpha - 2.0).abs() < 1e-10 {
24        // Gaussian case
25        return rng.normal() * std::f64::consts::SQRT_2;
26    }
27
28    if (alpha - 1.0).abs() < 1e-10 {
29        // Cauchy case: tan(U) where U ~ Uniform(-pi/2, pi/2)
30        let u = (rng.uniform() - 0.5) * std::f64::consts::PI;
31        return u.tan();
32    }
33
34    // General CMS algorithm for symmetric alpha-stable (beta=0)
35    let u = (rng.uniform() - 0.5) * std::f64::consts::PI; // U ~ Uniform(-pi/2, pi/2)
36    let w = -rng.uniform().max(1e-15).ln(); // W ~ Exp(1)
37
38    let factor = (alpha * u).sin() / (u.cos().powf(1.0 / alpha));
39    let tail = ((1.0 - alpha) * u).cos() / w;
40    factor * tail.powf((1.0 - alpha) / alpha)
41}
42
43/// Generate a stable random variate with given scale parameter.
44pub fn stable_random_scaled(alpha: f64, scale: f64, rng: &mut Rng) -> f64 {
45    scale * stable_random(alpha, rng)
46}
47
48// ---------------------------------------------------------------------------
49// LevyFlight
50// ---------------------------------------------------------------------------
51
52/// Lévy flight with alpha-stable step lengths.
53pub struct LevyFlight {
54    /// Stability parameter in (0, 2]. Smaller = heavier tails.
55    pub alpha: f64,
56    /// Scale parameter controlling typical step size.
57    pub scale: f64,
58}
59
60impl LevyFlight {
61    pub fn new(alpha: f64, scale: f64) -> Self {
62        assert!(alpha > 0.0 && alpha <= 2.0);
63        Self { alpha, scale }
64    }
65
66    /// Generate a single step length (absolute value of stable variate).
67    pub fn step_length(&self, rng: &mut Rng) -> f64 {
68        stable_random_scaled(self.alpha, self.scale, rng).abs()
69    }
70
71    /// Generate a 2D step: random direction with Lévy-distributed length.
72    pub fn step_2d(&self, rng: &mut Rng) -> Vec2 {
73        let length = self.step_length(rng) as f32;
74        let angle = rng.uniform() as f32 * 2.0 * std::f32::consts::PI;
75        Vec2::new(length * angle.cos(), length * angle.sin())
76    }
77
78    /// Generate a 2D path starting at the origin.
79    pub fn path_2d(&self, rng: &mut Rng, steps: usize) -> Vec<Vec2> {
80        let mut path = Vec::with_capacity(steps + 1);
81        let mut pos = Vec2::ZERO;
82        path.push(pos);
83        for _ in 0..steps {
84            pos += self.step_2d(rng);
85            path.push(pos);
86        }
87        path
88    }
89
90    /// Generate a 2D path starting from a given position.
91    pub fn path_2d_from(&self, rng: &mut Rng, start: Vec2, steps: usize) -> Vec<Vec2> {
92        let mut path = Vec::with_capacity(steps + 1);
93        let mut pos = start;
94        path.push(pos);
95        for _ in 0..steps {
96            pos += self.step_2d(rng);
97            path.push(pos);
98        }
99        path
100    }
101
102    /// Generate a 1D path.
103    pub fn path_1d(&self, rng: &mut Rng, steps: usize) -> Vec<f64> {
104        let mut path = Vec::with_capacity(steps + 1);
105        let mut x = 0.0;
106        path.push(x);
107        for _ in 0..steps {
108            x += stable_random_scaled(self.alpha, self.scale, rng);
109            path.push(x);
110        }
111        path
112    }
113
114    /// Compute step lengths for a path (useful for analysis).
115    pub fn step_lengths(&self, rng: &mut Rng, count: usize) -> Vec<f64> {
116        (0..count).map(|_| self.step_length(rng)).collect()
117    }
118}
119
120// ---------------------------------------------------------------------------
121// CauchyFlight (alpha = 1)
122// ---------------------------------------------------------------------------
123
124/// Cauchy flight: special case of Lévy flight with alpha = 1.
125/// The Cauchy distribution has undefined mean and variance (extremely heavy tails).
126pub struct CauchyFlight {
127    pub scale: f64,
128}
129
130impl CauchyFlight {
131    pub fn new(scale: f64) -> Self {
132        Self { scale }
133    }
134
135    /// Generate a Cauchy-distributed random variate.
136    pub fn sample(&self, rng: &mut Rng) -> f64 {
137        let u = (rng.uniform() - 0.5) * std::f64::consts::PI;
138        self.scale * u.tan()
139    }
140
141    /// Generate a 2D step.
142    pub fn step_2d(&self, rng: &mut Rng) -> Vec2 {
143        let length = self.sample(rng).abs() as f32;
144        let angle = rng.uniform() as f32 * 2.0 * std::f32::consts::PI;
145        Vec2::new(length * angle.cos(), length * angle.sin())
146    }
147
148    /// Generate a 2D path.
149    pub fn path_2d(&self, rng: &mut Rng, steps: usize) -> Vec<Vec2> {
150        let mut path = Vec::with_capacity(steps + 1);
151        let mut pos = Vec2::ZERO;
152        path.push(pos);
153        for _ in 0..steps {
154            pos += self.step_2d(rng);
155            path.push(pos);
156        }
157        path
158    }
159
160    /// Generate a 1D path.
161    pub fn path_1d(&self, rng: &mut Rng, steps: usize) -> Vec<f64> {
162        let mut path = Vec::with_capacity(steps + 1);
163        let mut x = 0.0;
164        path.push(x);
165        for _ in 0..steps {
166            x += self.sample(rng);
167            path.push(x);
168        }
169        path
170    }
171}
172
173// ---------------------------------------------------------------------------
174// LevyRenderer
175// ---------------------------------------------------------------------------
176
177/// Render a Lévy flight path with step-size-dependent glyph size.
178pub struct LevyRenderer {
179    pub small_character: char,
180    pub large_character: char,
181    pub color: [f32; 4],
182    /// Step length threshold to switch from small to large glyph.
183    pub size_threshold: f32,
184}
185
186impl LevyRenderer {
187    pub fn new() -> Self {
188        Self {
189            small_character: '·',
190            large_character: '●',
191            color: [0.9, 0.5, 0.2, 1.0],
192            size_threshold: 2.0,
193        }
194    }
195
196    /// Render a 2D path, using larger glyphs for longer steps.
197    pub fn render_path_2d(&self, path: &[Vec2]) -> Vec<(Vec2, char, [f32; 4])> {
198        if path.is_empty() {
199            return Vec::new();
200        }
201
202        let mut glyphs = Vec::with_capacity(path.len());
203
204        // First point
205        glyphs.push((path[0], self.small_character, self.color));
206
207        for i in 1..path.len() {
208            let step_len = (path[i] - path[i - 1]).length();
209            let ch = if step_len > self.size_threshold {
210                self.large_character
211            } else {
212                self.small_character
213            };
214
215            // Color intensity based on step size
216            let intensity = (step_len / (self.size_threshold * 5.0)).min(1.0);
217            let color = [
218                self.color[0],
219                self.color[1] * (1.0 - intensity * 0.5),
220                self.color[2] * (1.0 - intensity),
221                self.color[3],
222            ];
223
224            glyphs.push((path[i], ch, color));
225        }
226        glyphs
227    }
228
229    /// Render connecting lines between consecutive points.
230    pub fn render_path_2d_with_lines(&self, path: &[Vec2]) -> Vec<(Vec2, char, [f32; 4])> {
231        let mut glyphs = self.render_path_2d(path);
232
233        // Add intermediate points along each segment
234        for i in 1..path.len() {
235            let from = path[i - 1];
236            let to = path[i];
237            let dist = (to - from).length();
238            let segments = (dist / 0.5).ceil() as usize;
239            if segments > 1 && segments < 100 {
240                let line_color = [self.color[0] * 0.5, self.color[1] * 0.5, self.color[2] * 0.5, 0.3];
241                for s in 1..segments {
242                    let t = s as f32 / segments as f32;
243                    let pos = from.lerp(to, t);
244                    glyphs.push((pos, '·', line_color));
245                }
246            }
247        }
248        glyphs
249    }
250
251    /// Render a step-length histogram.
252    pub fn render_step_histogram(&self, steps: &[f64], bins: usize) -> Vec<(Vec2, char, [f32; 4])> {
253        use super::monte_carlo::Histogram;
254        let hist = Histogram::from_samples(steps, bins);
255        let max_count = hist.max_count().max(1) as f32;
256        let mut glyphs = Vec::new();
257
258        for (i, &count) in hist.bins.iter().enumerate() {
259            let x = i as f32 * 0.5;
260            let height = (count as f32 / max_count * 15.0) as usize;
261            for h in 0..height {
262                glyphs.push((Vec2::new(x, h as f32 * 0.3), '█', self.color));
263            }
264        }
265        glyphs
266    }
267}
268
269impl Default for LevyRenderer {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275// ---------------------------------------------------------------------------
276// Tests
277// ---------------------------------------------------------------------------
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn test_stable_gaussian_case() {
285        // alpha = 2 should be Gaussian * sqrt(2)
286        let mut rng = Rng::new(42);
287        let n = 10_000;
288        let samples: Vec<f64> = (0..n).map(|_| stable_random(2.0, &mut rng)).collect();
289        let mean = samples.iter().sum::<f64>() / n as f64;
290        let var = samples.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n as f64;
291        assert!(mean.abs() < 0.1, "Gaussian stable mean should be ~0, got {}", mean);
292        // Variance should be ~2 (since we return sqrt(2) * N(0,1))
293        assert!((var - 2.0).abs() < 0.3, "Gaussian stable variance should be ~2, got {}", var);
294    }
295
296    #[test]
297    fn test_cauchy_heavy_tails() {
298        // Cauchy distribution should have some very large values
299        let cf = CauchyFlight::new(1.0);
300        let mut rng = Rng::new(42);
301        let samples: Vec<f64> = (0..10_000).map(|_| cf.sample(&mut rng)).collect();
302        let max_abs = samples.iter().map(|x| x.abs()).fold(0.0, f64::max);
303        assert!(
304            max_abs > 10.0,
305            "Cauchy should have heavy tails, max |x| = {}",
306            max_abs
307        );
308    }
309
310    #[test]
311    fn test_levy_flight_heavy_tails() {
312        // With alpha < 2, we should see occasional very large steps
313        let lf = LevyFlight::new(1.5, 1.0);
314        let mut rng = Rng::new(42);
315        let lengths = lf.step_lengths(&mut rng, 10_000);
316
317        let mean = lengths.iter().sum::<f64>() / lengths.len() as f64;
318        let max_step = lengths.iter().cloned().fold(0.0, f64::max);
319
320        // Max step should be much larger than mean (heavy tails)
321        assert!(
322            max_step > mean * 5.0,
323            "heavy tails: max step {} should be >> mean {}",
324            max_step,
325            mean
326        );
327    }
328
329    #[test]
330    fn test_levy_2d_path() {
331        let lf = LevyFlight::new(1.5, 1.0);
332        let mut rng = Rng::new(42);
333        let path = lf.path_2d(&mut rng, 100);
334        assert_eq!(path.len(), 101);
335        assert_eq!(path[0], Vec2::ZERO);
336    }
337
338    #[test]
339    fn test_levy_1d_path() {
340        let lf = LevyFlight::new(1.5, 1.0);
341        let mut rng = Rng::new(42);
342        let path = lf.path_1d(&mut rng, 100);
343        assert_eq!(path.len(), 101);
344        assert_eq!(path[0], 0.0);
345    }
346
347    #[test]
348    fn test_cauchy_path() {
349        let cf = CauchyFlight::new(1.0);
350        let mut rng = Rng::new(42);
351        let path = cf.path_2d(&mut rng, 50);
352        assert_eq!(path.len(), 51);
353        assert_eq!(path[0], Vec2::ZERO);
354    }
355
356    #[test]
357    fn test_levy_vs_gaussian_variance() {
358        // alpha = 2 (Gaussian) should have finite, bounded increments
359        // alpha = 1.2 should have much larger extreme increments
360        let mut rng = Rng::new(42);
361        let gauss_steps: Vec<f64> = (0..5000)
362            .map(|_| stable_random(2.0, &mut rng).abs())
363            .collect();
364        let levy_steps: Vec<f64> = (0..5000)
365            .map(|_| stable_random(1.2, &mut rng).abs())
366            .collect();
367
368        let gauss_max = gauss_steps.iter().cloned().fold(0.0, f64::max);
369        let levy_max = levy_steps.iter().cloned().fold(0.0, f64::max);
370
371        assert!(
372            levy_max > gauss_max,
373            "Lévy max {} should generally exceed Gaussian max {}",
374            levy_max,
375            gauss_max
376        );
377    }
378
379    #[test]
380    fn test_renderer() {
381        let lf = LevyFlight::new(1.5, 1.0);
382        let mut rng = Rng::new(42);
383        let path = lf.path_2d(&mut rng, 50);
384        let renderer = LevyRenderer::new();
385        let glyphs = renderer.render_path_2d(&path);
386        assert_eq!(glyphs.len(), 51);
387    }
388
389    #[test]
390    fn test_renderer_with_lines() {
391        let lf = LevyFlight::new(1.5, 0.5);
392        let mut rng = Rng::new(42);
393        let path = lf.path_2d(&mut rng, 20);
394        let renderer = LevyRenderer::new();
395        let glyphs = renderer.render_path_2d_with_lines(&path);
396        // Should have more glyphs than just the path points due to line interpolation
397        assert!(glyphs.len() >= 21);
398    }
399
400    #[test]
401    fn test_step_histogram() {
402        let lf = LevyFlight::new(1.5, 1.0);
403        let mut rng = Rng::new(42);
404        let lengths = lf.step_lengths(&mut rng, 500);
405        let renderer = LevyRenderer::new();
406        let glyphs = renderer.render_step_histogram(&lengths, 20);
407        assert!(!glyphs.is_empty());
408    }
409}