1use super::brownian::Rng;
7use glam::Vec2;
8
9pub 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 return rng.normal() * std::f64::consts::SQRT_2;
26 }
27
28 if (alpha - 1.0).abs() < 1e-10 {
29 let u = (rng.uniform() - 0.5) * std::f64::consts::PI;
31 return u.tan();
32 }
33
34 let u = (rng.uniform() - 0.5) * std::f64::consts::PI; let w = -rng.uniform().max(1e-15).ln(); 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
43pub fn stable_random_scaled(alpha: f64, scale: f64, rng: &mut Rng) -> f64 {
45 scale * stable_random(alpha, rng)
46}
47
48pub struct LevyFlight {
54 pub alpha: f64,
56 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 pub fn step_length(&self, rng: &mut Rng) -> f64 {
68 stable_random_scaled(self.alpha, self.scale, rng).abs()
69 }
70
71 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 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 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 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 pub fn step_lengths(&self, rng: &mut Rng, count: usize) -> Vec<f64> {
116 (0..count).map(|_| self.step_length(rng)).collect()
117 }
118}
119
120pub struct CauchyFlight {
127 pub scale: f64,
128}
129
130impl CauchyFlight {
131 pub fn new(scale: f64) -> Self {
132 Self { scale }
133 }
134
135 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 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 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 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
173pub struct LevyRenderer {
179 pub small_character: char,
180 pub large_character: char,
181 pub color: [f32; 4],
182 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 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 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 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 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 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 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#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn test_stable_gaussian_case() {
285 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 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 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 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 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 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 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}