1use glam::Vec3;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum AttractorType {
17 Lorenz,
18 Rossler,
19 Chen,
20 Halvorsen,
21 Aizawa,
22 Thomas,
23 Dadras,
24 Sprott,
25 Rabinovich,
26 Burke,
27}
28
29impl AttractorType {
30 pub fn name(self) -> &'static str {
32 match self {
33 AttractorType::Lorenz => "Lorenz",
34 AttractorType::Rossler => "Rössler",
35 AttractorType::Chen => "Chen",
36 AttractorType::Halvorsen => "Halvorsen",
37 AttractorType::Aizawa => "Aizawa",
38 AttractorType::Thomas => "Thomas",
39 AttractorType::Dadras => "Dadras",
40 AttractorType::Sprott => "Sprott B",
41 AttractorType::Rabinovich => "Rabinovich–Fabrikant",
42 AttractorType::Burke => "Burke–Shaw",
43 }
44 }
45
46 pub fn all() -> &'static [AttractorType] {
48 &[
49 AttractorType::Lorenz,
50 AttractorType::Rossler,
51 AttractorType::Chen,
52 AttractorType::Halvorsen,
53 AttractorType::Aizawa,
54 AttractorType::Thomas,
55 AttractorType::Dadras,
56 AttractorType::Sprott,
57 AttractorType::Rabinovich,
58 AttractorType::Burke,
59 ]
60 }
61
62 pub fn normalization_scale(self) -> f32 {
64 match self {
65 AttractorType::Lorenz => 1.0 / 30.0,
66 AttractorType::Rossler => 1.0 / 12.0,
67 AttractorType::Chen => 1.0 / 30.0,
68 AttractorType::Halvorsen => 1.0 / 8.0,
69 AttractorType::Aizawa => 1.0 / 1.5,
70 AttractorType::Thomas => 1.0 / 3.5,
71 AttractorType::Dadras => 1.0 / 10.0,
72 AttractorType::Sprott => 1.0 / 2.0,
73 AttractorType::Rabinovich => 1.0 / 3.0,
74 AttractorType::Burke => 1.0 / 3.5,
75 }
76 }
77
78 pub fn recommended_dt(self) -> f32 {
80 match self {
81 AttractorType::Lorenz => 0.005,
82 AttractorType::Rossler => 0.01,
83 AttractorType::Chen => 0.005,
84 AttractorType::Halvorsen => 0.01,
85 AttractorType::Aizawa => 0.01,
86 AttractorType::Thomas => 0.05,
87 AttractorType::Dadras => 0.005,
88 AttractorType::Sprott => 0.01,
89 AttractorType::Rabinovich => 0.005,
90 AttractorType::Burke => 0.005,
91 }
92 }
93
94 pub fn lyapunov_estimate(self) -> f32 {
96 match self {
97 AttractorType::Lorenz => 0.9056,
98 AttractorType::Rossler => 0.0714,
99 AttractorType::Chen => 2.0272,
100 AttractorType::Halvorsen => 0.8042,
101 AttractorType::Aizawa => 0.0721,
102 AttractorType::Thomas => 0.0550,
103 AttractorType::Dadras => 0.5100,
104 AttractorType::Sprott => 0.3600,
105 AttractorType::Rabinovich => 0.1600,
106 AttractorType::Burke => 0.3300,
107 }
108 }
109}
110
111pub fn derivatives(attractor: AttractorType, s: Vec3) -> Vec3 {
116 let (x, y, z) = (s.x, s.y, s.z);
117 let (dx, dy, dz) = match attractor {
118 AttractorType::Lorenz => {
119 const SIGMA: f32 = 10.0;
120 const RHO: f32 = 28.0;
121 const BETA: f32 = 8.0 / 3.0;
122 (SIGMA * (y - x), x * (RHO - z) - y, x * y - BETA * z)
123 }
124 AttractorType::Rossler => {
125 const A: f32 = 0.2;
126 const B: f32 = 0.2;
127 const C: f32 = 5.7;
128 (-y - z, x + A * y, B + z * (x - C))
129 }
130 AttractorType::Chen => {
131 const A: f32 = 35.0;
132 const B: f32 = 3.0;
133 const C: f32 = 28.0;
134 (A * (y - x), (C - A) * x - x * z + C * y, x * y - B * z)
135 }
136 AttractorType::Halvorsen => {
137 const A: f32 = 1.4;
138 (
139 -A * x - 4.0 * y - 4.0 * z - y * y,
140 -A * y - 4.0 * z - 4.0 * x - z * z,
141 -A * z - 4.0 * x - 4.0 * y - x * x,
142 )
143 }
144 AttractorType::Aizawa => {
145 const A: f32 = 0.95;
146 const B: f32 = 0.7;
147 const C: f32 = 0.6;
148 const D: f32 = 3.5;
149 const E: f32 = 0.25;
150 const F: f32 = 0.1;
151 (
152 (z - B) * x - D * y,
153 D * x + (z - B) * y,
154 C + A * z - z.powi(3) / 3.0
155 - (x * x + y * y) * (1.0 + E * z)
156 + F * z * x.powi(3),
157 )
158 }
159 AttractorType::Thomas => {
160 const B: f32 = 0.208_186;
161 (y.sin() - B * x, z.sin() - B * y, x.sin() - B * z)
162 }
163 AttractorType::Dadras => {
164 const P: f32 = 3.0;
165 const Q: f32 = 2.7;
166 const R: f32 = 1.7;
167 const S: f32 = 2.0;
168 const H: f32 = 9.0;
169 (y - P * x + Q * y * z, R * y - x * z + z, S * x * y - H * z)
170 }
171 AttractorType::Sprott => {
172 (y * z, x - y, 1.0 - x * y)
174 }
175 AttractorType::Rabinovich => {
176 const GAMMA: f32 = 0.87;
178 const ALPHA: f32 = 1.1;
179 (
180 y * (z - 1.0 + x * x) + GAMMA * x,
181 x * (3.0 * z + 1.0 - x * x) + GAMMA * y,
182 -2.0 * z * (ALPHA + x * y),
183 )
184 }
185 AttractorType::Burke => {
186 const S: f32 = 10.0;
188 const V: f32 = 4.272;
189 (-S * (x + y), -y - S * x * z, S * x * y + V)
190 }
191 };
192 Vec3::new(dx, dy, dz)
193}
194
195#[inline]
200pub fn rk4_step(attractor: AttractorType, state: Vec3, dt: f32) -> Vec3 {
201 let k1 = derivatives(attractor, state);
202 let k2 = derivatives(attractor, state + k1 * (dt * 0.5));
203 let k3 = derivatives(attractor, state + k2 * (dt * 0.5));
204 let k4 = derivatives(attractor, state + k3 * dt);
205 state + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0)
206}
207
208pub fn step(attractor: AttractorType, state: Vec3, dt: f32) -> (Vec3, Vec3) {
211 let next = rk4_step(attractor, state, dt);
212 (next, next - state)
213}
214
215pub fn warmup(attractor: AttractorType, mut state: Vec3, steps: usize) -> Vec3 {
218 let dt = attractor.recommended_dt();
219 for _ in 0..steps {
220 state = rk4_step(attractor, state, dt);
221 }
222 state
223}
224
225pub fn initial_state(attractor: AttractorType) -> Vec3 {
229 match attractor {
230 AttractorType::Lorenz => Vec3::new(0.1, 0.0, 0.0),
231 AttractorType::Rossler => Vec3::new(0.1, 0.0, 0.0),
232 AttractorType::Chen => Vec3::new(0.1, 0.0, 0.0),
233 AttractorType::Halvorsen => Vec3::new(0.1, 0.0, 0.0),
234 AttractorType::Aizawa => Vec3::new(0.1, 0.0, 0.0),
235 AttractorType::Thomas => Vec3::new(0.1, 0.0, 0.0),
236 AttractorType::Dadras => Vec3::new(1.1, 2.1, -2.0),
239 AttractorType::Sprott => Vec3::new(1.0, 0.0, 0.5),
240 AttractorType::Rabinovich => Vec3::new(-1.0, 0.0, 0.5),
243 AttractorType::Burke => Vec3::new(0.6, -0.4, 0.4),
244 }
245}
246
247pub fn initial_state_warmed(attractor: AttractorType) -> Vec3 {
249 warmup(attractor, initial_state(attractor), 5000)
250}
251
252#[derive(Debug, Clone)]
257pub struct AttractorSampler {
258 pub attractor: AttractorType,
259 pub state: Vec3,
260 pub dt: f32,
261 pub time: f32,
262 pub scale: f32,
264 pub center: Vec3,
266 pub substeps: usize,
268}
269
270impl AttractorSampler {
271 pub fn new(attractor: AttractorType) -> Self {
272 let state = initial_state_warmed(attractor);
273 Self {
274 attractor,
275 state,
276 dt: attractor.recommended_dt(),
277 time: 0.0,
278 scale: attractor.normalization_scale(),
279 center: Vec3::ZERO,
280 substeps: 1,
281 }
282 }
283
284 pub fn next(&mut self) -> Vec3 {
286 for _ in 0..self.substeps {
287 self.state = rk4_step(self.attractor, self.state, self.dt);
288 self.time += self.dt;
289 }
290 self.state * self.scale + self.center
291 }
292
293 pub fn sample_trajectory(&mut self, n: usize) -> Vec<Vec3> {
295 (0..n).map(|_| self.next()).collect()
296 }
297
298 pub fn reset(&mut self) {
300 self.state = initial_state_warmed(self.attractor);
301 self.time = 0.0;
302 }
303
304 pub fn set_attractor(&mut self, attractor: AttractorType) {
306 self.attractor = attractor;
307 self.dt = attractor.recommended_dt();
308 self.scale = attractor.normalization_scale();
309 self.reset();
310 }
311
312 pub fn raw_state(&self) -> Vec3 {
314 self.state
315 }
316
317 pub fn position(&self) -> Vec3 {
319 self.state * self.scale + self.center
320 }
321
322 pub fn velocity(&self) -> Vec3 {
324 let d = derivatives(self.attractor, self.state);
325 d * (self.scale * self.dt)
326 }
327}
328
329pub fn largest_lyapunov_exponent(
340 attractor: AttractorType,
341 initial: Vec3,
342 steps: usize,
343) -> f32 {
344 let dt = attractor.recommended_dt();
345 let eps = 1e-8_f32;
346 let mut s1 = warmup(attractor, initial, 2000);
347 let mut s2 = s1 + Vec3::splat(eps / 3.0_f32.sqrt());
349
350 let mut lyapunov_sum = 0.0_f32;
351
352 for _ in 0..steps {
353 s1 = rk4_step(attractor, s1, dt);
354 s2 = rk4_step(attractor, s2, dt);
355
356 let sep = s2 - s1;
357 let d = sep.length();
358 if d < 1e-15 { continue; }
359
360 lyapunov_sum += (d / eps).ln();
361
362 s2 = s1 + sep * (eps / d);
364 }
365
366 lyapunov_sum / (steps as f32 * dt)
367}
368
369pub fn lyapunov_spectrum(
372 attractor: AttractorType,
373 initial: Vec3,
374 steps: usize,
375) -> [f32; 3] {
376 let dt = attractor.recommended_dt();
377 let eps = 1e-6_f32;
378
379 let mut state = warmup(attractor, initial, 2000);
381 let mut v1 = Vec3::new(eps, 0.0, 0.0);
382 let mut v2 = Vec3::new(0.0, eps, 0.0);
383 let mut v3 = Vec3::new(0.0, 0.0, eps);
384
385 let mut sums = [0.0_f32; 3];
386
387 for _ in 0..steps {
388 let s0 = state;
389 state = rk4_step(attractor, s0, dt);
390
391 let evolve = |v: Vec3| -> Vec3 {
393 rk4_step(attractor, s0 + v, dt) - state
394 };
395
396 v1 = evolve(v1);
397 v2 = evolve(v2);
398 v3 = evolve(v3);
399
400 let n1 = v1.length().max(1e-30);
402 sums[0] += n1.ln();
403 v1 = v1 / n1;
404
405 v2 = v2 - v1 * v2.dot(v1);
406 let n2 = v2.length().max(1e-30);
407 sums[1] += n2.ln();
408 v2 = v2 / n2;
409
410 v3 = v3 - v1 * v3.dot(v1) - v2 * v3.dot(v2);
411 let n3 = v3.length().max(1e-30);
412 sums[2] += n3.ln();
413 v3 = v3 / n3;
414
415 v1 *= eps;
417 v2 *= eps;
418 v3 *= eps;
419 }
420
421 let t = steps as f32 * dt;
422 [sums[0] / t, sums[1] / t, sums[2] / t]
423}
424
425pub fn kaplan_yorke_dimension(spectrum: [f32; 3]) -> f32 {
427 let mut sorted = spectrum;
428 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
429
430 let mut cumsum = 0.0_f32;
431 let mut j = 0usize;
432 for (i, &l) in sorted.iter().enumerate() {
433 if cumsum + l < 0.0 { break; }
434 cumsum += l;
435 j = i + 1;
436 }
437 if j >= 3 { return 3.0; }
438 j as f32 + cumsum / sorted[j].abs().max(1e-12)
439}
440
441#[derive(Debug, Clone)]
445pub struct AttractorStats {
446 pub attractor: AttractorType,
447 pub bbox_min: Vec3,
448 pub bbox_max: Vec3,
449 pub centroid: Vec3,
450 pub variance: Vec3,
451 pub sample_count: usize,
452 pub lyapunov_max: f32,
454}
455
456impl AttractorStats {
457 pub fn compute(attractor: AttractorType, n: usize) -> Self {
459 let mut sampler = AttractorSampler::new(attractor);
460 sampler.scale = 1.0; let mut min = Vec3::splat(f32::MAX);
463 let mut max = Vec3::splat(f32::MIN);
464 let mut sum = Vec3::ZERO;
465
466 let pts: Vec<Vec3> = (0..n).map(|_| {
467 let p = sampler.next();
468 min = min.min(p);
469 max = max.max(p);
470 sum += p;
471 p
472 }).collect();
473
474 let centroid = sum / n as f32;
475 let variance = pts.iter().fold(Vec3::ZERO, |acc, &p| {
476 let d = p - centroid;
477 acc + d * d
478 }) / n as f32;
479
480 let lyapunov_max = largest_lyapunov_exponent(
481 attractor, initial_state(attractor), 5000,
482 );
483
484 Self { attractor, bbox_min: min, bbox_max: max, centroid, variance, sample_count: n, lyapunov_max }
485 }
486
487 pub fn bbox_size(&self) -> Vec3 {
489 self.bbox_max - self.bbox_min
490 }
491
492 pub fn normalizing_scale(&self) -> f32 {
494 let s = self.bbox_size();
495 let m = s.x.max(s.y).max(s.z);
496 if m < 1e-10 { 1.0 } else { 2.0 / m }
497 }
498}
499
500pub fn lorenz_parametric(s: Vec3, sigma: f32, rho: f32, beta: f32) -> Vec3 {
504 let (x, y, z) = (s.x, s.y, s.z);
505 Vec3::new(sigma * (y - x), x * (rho - z) - y, x * y - beta * z)
506}
507
508pub fn rossler_parametric(s: Vec3, a: f32, b: f32, c: f32) -> Vec3 {
510 let (x, y, z) = (s.x, s.y, s.z);
511 Vec3::new(-y - z, x + a * y, b + z * (x - c))
512}
513
514pub fn lorenz_bifurcation(
516 rho_min: f32,
517 rho_max: f32,
518 rho_steps: usize,
519 warmup_n: usize,
520 sample_n: usize,
521) -> Vec<(f32, Vec<f32>)> {
522 let sigma = 10.0_f32;
523 let beta = 8.0_f32 / 3.0_f32;
524 let dt = 0.005_f32;
525
526 (0..rho_steps).map(|i| {
527 let rho = rho_min + (rho_max - rho_min) * i as f32 / (rho_steps - 1) as f32;
528 let mut state = Vec3::new(0.1, 0.0, 0.0);
529
530 for _ in 0..warmup_n {
532 let k1 = lorenz_parametric(state, sigma, rho, beta);
533 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
534 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
535 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
536 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
537 }
538
539 let zs: Vec<f32> = (0..sample_n).map(|_| {
541 let k1 = lorenz_parametric(state, sigma, rho, beta);
542 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
543 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
544 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
545 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
546 state.z
547 }).collect();
548
549 (rho, zs)
550 }).collect()
551}
552
553pub struct AttractorPool {
558 samplers: Vec<AttractorSampler>,
559 round_robin: usize,
560}
561
562impl AttractorPool {
563 pub fn new(attractor: AttractorType, n: usize) -> Self {
566 let base = initial_state_warmed(attractor);
567 let dt = attractor.recommended_dt();
568 let samplers = (0..n).map(|i| {
569 let offset_state = {
571 let mut s = base;
572 for _ in 0..(i * 200) {
573 s = rk4_step(attractor, s, dt);
574 }
575 s
576 };
577 AttractorSampler {
578 attractor,
579 state: offset_state,
580 dt,
581 time: i as f32 * 200.0 * dt,
582 scale: attractor.normalization_scale(),
583 center: Vec3::ZERO,
584 substeps: 1,
585 }
586 }).collect();
587 Self { samplers, round_robin: 0 }
588 }
589
590 pub fn next(&mut self) -> Vec3 {
592 if self.samplers.is_empty() {
593 return Vec3::ZERO;
594 }
595 let idx = self.round_robin % self.samplers.len();
596 self.round_robin = idx + 1;
597 self.samplers[idx].next()
598 }
599
600 pub fn tick_all(&mut self) {
602 for s in &mut self.samplers {
603 s.next();
604 }
605 }
606
607 pub fn positions(&self) -> Vec<Vec3> {
609 self.samplers.iter().map(|s| s.position()).collect()
610 }
611
612 pub fn set_scale(&mut self, scale: f32) {
614 for s in &mut self.samplers {
615 s.scale = scale;
616 }
617 }
618
619 pub fn set_center(&mut self, center: Vec3) {
621 for s in &mut self.samplers {
622 s.center = center;
623 }
624 }
625
626 pub fn len(&self) -> usize { self.samplers.len() }
627 pub fn is_empty(&self) -> bool { self.samplers.is_empty() }
628}
629
630pub fn poincare_section(
635 attractor: AttractorType,
636 z_level: f32,
637 n_crossings: usize,
638) -> Vec<(f32, f32)> {
639 let dt = attractor.recommended_dt();
640 let mut s = initial_state_warmed(attractor);
641 let mut crossings = Vec::with_capacity(n_crossings);
642 let mut prev_z = s.z;
643 let mut iterations = 0usize;
644 let max_iter = n_crossings * 100_000;
645
646 while crossings.len() < n_crossings && iterations < max_iter {
647 s = rk4_step(attractor, s, dt);
648 if prev_z < z_level && s.z >= z_level {
650 let t = (z_level - prev_z) / (s.z - prev_z);
652 let sx = prev_z + t * (s.x - prev_z); crossings.push((s.x, s.y));
654 let _ = sx;
655 }
656 prev_z = s.z;
657 iterations += 1;
658 }
659 crossings
660}
661
662pub fn recurrence_plot(
668 attractor: AttractorType,
669 n: usize,
670 threshold: f32,
671) -> Vec<bool> {
672 let trajectory = AttractorSampler::new(attractor)
673 .sample_trajectory(n);
674
675 let mut matrix = vec![false; n * n];
676 for i in 0..n {
677 for j in 0..n {
678 let d = (trajectory[i] - trajectory[j]).length();
679 matrix[i * n + j] = d < threshold;
680 }
681 }
682 matrix
683}
684
685pub fn velocity_color(velocity: Vec3, palette: AttractorPalette) -> glam::Vec4 {
690 let speed = velocity.length();
691 let t = (speed * 5.0).clamp(0.0, 1.0); match palette {
693 AttractorPalette::Plasma => {
694 let r = (0.5 + 0.5 * (t * std::f32::consts::TAU).sin()).clamp(0.0, 1.0);
696 let g = t.sqrt();
697 let b = (1.0 - t).powi(2);
698 glam::Vec4::new(r, g, b, 1.0)
699 }
700 AttractorPalette::Fire => {
701 let r = (t * 2.0).clamp(0.0, 1.0);
702 let g = ((t * 2.0) - 1.0).clamp(0.0, 1.0);
703 let b = 0.0;
704 glam::Vec4::new(r, g, b, 1.0)
705 }
706 AttractorPalette::Ice => {
707 let r = t * 0.2;
708 let g = t * 0.6;
709 let b = t;
710 glam::Vec4::new(r, g, b, 1.0)
711 }
712 AttractorPalette::Neon => {
713 let r = (1.0 - t) * 0.8;
714 let g = (1.0 - (t - 0.5).abs() * 2.0).max(0.0);
715 let b = t;
716 glam::Vec4::new(r, g, b, 1.0)
717 }
718 AttractorPalette::Greyscale => {
719 glam::Vec4::new(t, t, t, 1.0)
720 }
721 }
722}
723
724#[derive(Debug, Clone, Copy, PartialEq, Eq)]
726pub enum AttractorPalette {
727 Plasma,
728 Fire,
729 Ice,
730 Neon,
731 Greyscale,
732}
733
734#[cfg(test)]
735mod initial_state_tests {
736 use super::*;
737
738 #[test]
742 fn warmed_states_are_finite_and_off_the_origin() {
743 for &a in AttractorType::all() {
744 let s = initial_state_warmed(a);
745 assert!(s.is_finite(), "{} diverged: {s:?}", a.name());
746 assert!(s.length() > 0.05, "{} collapsed to the origin: {s:?}", a.name());
747 }
748 }
749}