1#![warn(missing_docs)]
11
12use glam::Vec3;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18pub enum AttractorType {
19 Lorenz,
21 Rossler,
23 Chen,
25 Halvorsen,
27 Aizawa,
29 Thomas,
31 Dadras,
33 Sprott,
35 Rabinovich,
37 Burke,
39}
40
41impl AttractorType {
42 pub fn name(self) -> &'static str {
44 match self {
45 AttractorType::Lorenz => "Lorenz",
46 AttractorType::Rossler => "Rössler",
47 AttractorType::Chen => "Chen",
48 AttractorType::Halvorsen => "Halvorsen",
49 AttractorType::Aizawa => "Aizawa",
50 AttractorType::Thomas => "Thomas",
51 AttractorType::Dadras => "Dadras",
52 AttractorType::Sprott => "Sprott B",
53 AttractorType::Rabinovich => "Rabinovich–Fabrikant",
54 AttractorType::Burke => "Burke–Shaw",
55 }
56 }
57
58 pub fn all() -> &'static [AttractorType] {
60 &[
61 AttractorType::Lorenz,
62 AttractorType::Rossler,
63 AttractorType::Chen,
64 AttractorType::Halvorsen,
65 AttractorType::Aizawa,
66 AttractorType::Thomas,
67 AttractorType::Dadras,
68 AttractorType::Sprott,
69 AttractorType::Rabinovich,
70 AttractorType::Burke,
71 ]
72 }
73
74 pub fn normalization_scale(self) -> f32 {
76 match self {
77 AttractorType::Lorenz => 1.0 / 30.0,
78 AttractorType::Rossler => 1.0 / 12.0,
79 AttractorType::Chen => 1.0 / 30.0,
80 AttractorType::Halvorsen => 1.0 / 8.0,
81 AttractorType::Aizawa => 1.0 / 1.5,
82 AttractorType::Thomas => 1.0 / 3.5,
83 AttractorType::Dadras => 1.0 / 10.0,
84 AttractorType::Sprott => 1.0 / 2.0,
85 AttractorType::Rabinovich => 1.0 / 3.0,
86 AttractorType::Burke => 1.0 / 3.5,
87 }
88 }
89
90 pub fn recommended_dt(self) -> f32 {
92 match self {
93 AttractorType::Lorenz => 0.005,
94 AttractorType::Rossler => 0.01,
95 AttractorType::Chen => 0.005,
96 AttractorType::Halvorsen => 0.01,
97 AttractorType::Aizawa => 0.01,
98 AttractorType::Thomas => 0.05,
99 AttractorType::Dadras => 0.005,
100 AttractorType::Sprott => 0.01,
101 AttractorType::Rabinovich => 0.005,
102 AttractorType::Burke => 0.005,
103 }
104 }
105
106 pub fn lyapunov_estimate(self) -> f32 {
108 match self {
109 AttractorType::Lorenz => 0.9056,
110 AttractorType::Rossler => 0.0714,
111 AttractorType::Chen => 2.0272,
112 AttractorType::Halvorsen => 0.8042,
113 AttractorType::Aizawa => 0.0721,
114 AttractorType::Thomas => 0.0550,
115 AttractorType::Dadras => 0.5100,
116 AttractorType::Sprott => 0.3600,
117 AttractorType::Rabinovich => 0.1600,
118 AttractorType::Burke => 0.3300,
119 }
120 }
121}
122
123pub fn derivatives(attractor: AttractorType, s: Vec3) -> Vec3 {
128 let (x, y, z) = (s.x, s.y, s.z);
129 let (dx, dy, dz) = match attractor {
130 AttractorType::Lorenz => {
131 const SIGMA: f32 = 10.0;
132 const RHO: f32 = 28.0;
133 const BETA: f32 = 8.0 / 3.0;
134 (SIGMA * (y - x), x * (RHO - z) - y, x * y - BETA * z)
135 }
136 AttractorType::Rossler => {
137 const A: f32 = 0.2;
138 const B: f32 = 0.2;
139 const C: f32 = 5.7;
140 (-y - z, x + A * y, B + z * (x - C))
141 }
142 AttractorType::Chen => {
143 const A: f32 = 35.0;
144 const B: f32 = 3.0;
145 const C: f32 = 28.0;
146 (A * (y - x), (C - A) * x - x * z + C * y, x * y - B * z)
147 }
148 AttractorType::Halvorsen => {
149 const A: f32 = 1.4;
150 (
151 -A * x - 4.0 * y - 4.0 * z - y * y,
152 -A * y - 4.0 * z - 4.0 * x - z * z,
153 -A * z - 4.0 * x - 4.0 * y - x * x,
154 )
155 }
156 AttractorType::Aizawa => {
157 const A: f32 = 0.95;
158 const B: f32 = 0.7;
159 const C: f32 = 0.6;
160 const D: f32 = 3.5;
161 const E: f32 = 0.25;
162 const F: f32 = 0.1;
163 (
164 (z - B) * x - D * y,
165 D * x + (z - B) * y,
166 C + A * z - z.powi(3) / 3.0
167 - (x * x + y * y) * (1.0 + E * z)
168 + F * z * x.powi(3),
169 )
170 }
171 AttractorType::Thomas => {
172 const B: f32 = 0.208_186;
173 (y.sin() - B * x, z.sin() - B * y, x.sin() - B * z)
174 }
175 AttractorType::Dadras => {
176 const P: f32 = 3.0;
177 const Q: f32 = 2.7;
178 const R: f32 = 1.7;
179 const S: f32 = 2.0;
180 const H: f32 = 9.0;
181 (y - P * x + Q * y * z, R * y - x * z + z, S * x * y - H * z)
182 }
183 AttractorType::Sprott => {
184 (y * z, x - y, 1.0 - x * y)
186 }
187 AttractorType::Rabinovich => {
188 const GAMMA: f32 = 0.87;
190 const ALPHA: f32 = 1.1;
191 (
192 y * (z - 1.0 + x * x) + GAMMA * x,
193 x * (3.0 * z + 1.0 - x * x) + GAMMA * y,
194 -2.0 * z * (ALPHA + x * y),
195 )
196 }
197 AttractorType::Burke => {
198 const S: f32 = 10.0;
200 const V: f32 = 4.272;
201 (-S * (x + y), -y - S * x * z, S * x * y + V)
202 }
203 };
204 Vec3::new(dx, dy, dz)
205}
206
207#[inline]
212pub fn rk4_step(attractor: AttractorType, state: Vec3, dt: f32) -> Vec3 {
213 let k1 = derivatives(attractor, state);
214 let k2 = derivatives(attractor, state + k1 * (dt * 0.5));
215 let k3 = derivatives(attractor, state + k2 * (dt * 0.5));
216 let k4 = derivatives(attractor, state + k3 * dt);
217 state + (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0)
218}
219
220pub fn rk4_step_all(attractor: AttractorType, points: &mut [Vec3], dt: f32) {
236 #[cfg(feature = "parallel")]
237 {
238 use rayon::prelude::*;
239 if points.len() >= 4096 {
241 points
242 .par_chunks_mut(1024)
243 .for_each(|chunk| chunk.iter_mut().for_each(|p| *p = rk4_step(attractor, *p, dt)));
244 return;
245 }
246 }
247 for p in points.iter_mut() {
248 *p = rk4_step(attractor, *p, dt);
249 }
250}
251
252pub fn step(attractor: AttractorType, state: Vec3, dt: f32) -> (Vec3, Vec3) {
255 let next = rk4_step(attractor, state, dt);
256 (next, next - state)
257}
258
259pub fn warmup(attractor: AttractorType, mut state: Vec3, steps: usize) -> Vec3 {
262 let dt = attractor.recommended_dt();
263 for _ in 0..steps {
264 state = rk4_step(attractor, state, dt);
265 }
266 state
267}
268
269pub fn initial_state(attractor: AttractorType) -> Vec3 {
273 match attractor {
274 AttractorType::Lorenz => Vec3::new(0.1, 0.0, 0.0),
275 AttractorType::Rossler => Vec3::new(0.1, 0.0, 0.0),
276 AttractorType::Chen => Vec3::new(0.1, 0.0, 0.0),
277 AttractorType::Halvorsen => Vec3::new(0.1, 0.0, 0.0),
278 AttractorType::Aizawa => Vec3::new(0.1, 0.0, 0.0),
279 AttractorType::Thomas => Vec3::new(0.1, 0.0, 0.0),
280 AttractorType::Dadras => Vec3::new(1.1, 2.1, -2.0),
283 AttractorType::Sprott => Vec3::new(1.0, 0.0, 0.5),
284 AttractorType::Rabinovich => Vec3::new(-1.0, 0.0, 0.5),
287 AttractorType::Burke => Vec3::new(0.6, -0.4, 0.4),
288 }
289}
290
291pub fn initial_state_warmed(attractor: AttractorType) -> Vec3 {
293 warmup(attractor, initial_state(attractor), 5000)
294}
295
296#[derive(Debug, Clone)]
301pub struct AttractorSampler {
302 pub attractor: AttractorType,
304 pub state: Vec3,
306 pub dt: f32,
308 pub time: f32,
310 pub scale: f32,
312 pub center: Vec3,
314 pub substeps: usize,
316}
317
318impl AttractorSampler {
319 pub fn new(attractor: AttractorType) -> Self {
322 let state = initial_state_warmed(attractor);
323 Self {
324 attractor,
325 state,
326 dt: attractor.recommended_dt(),
327 time: 0.0,
328 scale: attractor.normalization_scale(),
329 center: Vec3::ZERO,
330 substeps: 1,
331 }
332 }
333
334 pub fn next(&mut self) -> Vec3 {
336 for _ in 0..self.substeps {
337 self.state = rk4_step(self.attractor, self.state, self.dt);
338 self.time += self.dt;
339 }
340 self.state * self.scale + self.center
341 }
342
343 pub fn sample_trajectory(&mut self, n: usize) -> Vec<Vec3> {
345 (0..n).map(|_| self.next()).collect()
346 }
347
348 pub fn reset(&mut self) {
350 self.state = initial_state_warmed(self.attractor);
351 self.time = 0.0;
352 }
353
354 pub fn set_attractor(&mut self, attractor: AttractorType) {
356 self.attractor = attractor;
357 self.dt = attractor.recommended_dt();
358 self.scale = attractor.normalization_scale();
359 self.reset();
360 }
361
362 pub fn raw_state(&self) -> Vec3 {
364 self.state
365 }
366
367 pub fn position(&self) -> Vec3 {
369 self.state * self.scale + self.center
370 }
371
372 pub fn velocity(&self) -> Vec3 {
374 let d = derivatives(self.attractor, self.state);
375 d * (self.scale * self.dt)
376 }
377}
378
379pub fn largest_lyapunov_exponent(
390 attractor: AttractorType,
391 initial: Vec3,
392 steps: usize,
393) -> f32 {
394 let dt = attractor.recommended_dt();
395 let eps = 1e-8_f32;
396 let mut s1 = warmup(attractor, initial, 2000);
397 let mut s2 = s1 + Vec3::splat(eps / 3.0_f32.sqrt());
399
400 let mut lyapunov_sum = 0.0_f32;
401
402 for _ in 0..steps {
403 s1 = rk4_step(attractor, s1, dt);
404 s2 = rk4_step(attractor, s2, dt);
405
406 let sep = s2 - s1;
407 let d = sep.length();
408 if d < 1e-15 { continue; }
409
410 lyapunov_sum += (d / eps).ln();
411
412 s2 = s1 + sep * (eps / d);
414 }
415
416 lyapunov_sum / (steps as f32 * dt)
417}
418
419pub fn lyapunov_spectrum(
422 attractor: AttractorType,
423 initial: Vec3,
424 steps: usize,
425) -> [f32; 3] {
426 let dt = attractor.recommended_dt();
427 let eps = 1e-6_f32;
428
429 let mut state = warmup(attractor, initial, 2000);
431 let mut v1 = Vec3::new(eps, 0.0, 0.0);
432 let mut v2 = Vec3::new(0.0, eps, 0.0);
433 let mut v3 = Vec3::new(0.0, 0.0, eps);
434
435 let mut sums = [0.0_f32; 3];
436
437 for _ in 0..steps {
438 let s0 = state;
439 state = rk4_step(attractor, s0, dt);
440
441 let evolve = |v: Vec3| -> Vec3 {
443 rk4_step(attractor, s0 + v, dt) - state
444 };
445
446 v1 = evolve(v1);
447 v2 = evolve(v2);
448 v3 = evolve(v3);
449
450 let n1 = v1.length().max(1e-30);
452 sums[0] += n1.ln();
453 v1 = v1 / n1;
454
455 v2 = v2 - v1 * v2.dot(v1);
456 let n2 = v2.length().max(1e-30);
457 sums[1] += n2.ln();
458 v2 = v2 / n2;
459
460 v3 = v3 - v1 * v3.dot(v1) - v2 * v3.dot(v2);
461 let n3 = v3.length().max(1e-30);
462 sums[2] += n3.ln();
463 v3 = v3 / n3;
464
465 v1 *= eps;
467 v2 *= eps;
468 v3 *= eps;
469 }
470
471 let t = steps as f32 * dt;
472 [sums[0] / t, sums[1] / t, sums[2] / t]
473}
474
475pub fn kaplan_yorke_dimension(spectrum: [f32; 3]) -> f32 {
477 let mut sorted = spectrum;
478 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
479
480 let mut cumsum = 0.0_f32;
481 let mut j = 0usize;
482 for (i, &l) in sorted.iter().enumerate() {
483 if cumsum + l < 0.0 { break; }
484 cumsum += l;
485 j = i + 1;
486 }
487 if j >= 3 { return 3.0; }
488 j as f32 + cumsum / sorted[j].abs().max(1e-12)
489}
490
491#[derive(Debug, Clone)]
495pub struct AttractorStats {
496 pub attractor: AttractorType,
498 pub bbox_min: Vec3,
500 pub bbox_max: Vec3,
502 pub centroid: Vec3,
504 pub variance: Vec3,
506 pub sample_count: usize,
508 pub lyapunov_max: f32,
510}
511
512impl AttractorStats {
513 pub fn compute(attractor: AttractorType, n: usize) -> Self {
515 let mut sampler = AttractorSampler::new(attractor);
516 sampler.scale = 1.0; let mut min = Vec3::splat(f32::MAX);
519 let mut max = Vec3::splat(f32::MIN);
520 let mut sum = Vec3::ZERO;
521
522 let pts: Vec<Vec3> = (0..n).map(|_| {
523 let p = sampler.next();
524 min = min.min(p);
525 max = max.max(p);
526 sum += p;
527 p
528 }).collect();
529
530 let centroid = sum / n as f32;
531 let variance = pts.iter().fold(Vec3::ZERO, |acc, &p| {
532 let d = p - centroid;
533 acc + d * d
534 }) / n as f32;
535
536 let lyapunov_max = largest_lyapunov_exponent(
537 attractor, initial_state(attractor), 5000,
538 );
539
540 Self { attractor, bbox_min: min, bbox_max: max, centroid, variance, sample_count: n, lyapunov_max }
541 }
542
543 pub fn bbox_size(&self) -> Vec3 {
545 self.bbox_max - self.bbox_min
546 }
547
548 pub fn normalizing_scale(&self) -> f32 {
550 let s = self.bbox_size();
551 let m = s.x.max(s.y).max(s.z);
552 if m < 1e-10 { 1.0 } else { 2.0 / m }
553 }
554}
555
556pub fn lorenz_parametric(s: Vec3, sigma: f32, rho: f32, beta: f32) -> Vec3 {
560 let (x, y, z) = (s.x, s.y, s.z);
561 Vec3::new(sigma * (y - x), x * (rho - z) - y, x * y - beta * z)
562}
563
564pub fn rossler_parametric(s: Vec3, a: f32, b: f32, c: f32) -> Vec3 {
566 let (x, y, z) = (s.x, s.y, s.z);
567 Vec3::new(-y - z, x + a * y, b + z * (x - c))
568}
569
570pub fn lorenz_bifurcation(
572 rho_min: f32,
573 rho_max: f32,
574 rho_steps: usize,
575 warmup_n: usize,
576 sample_n: usize,
577) -> Vec<(f32, Vec<f32>)> {
578 let sigma = 10.0_f32;
579 let beta = 8.0_f32 / 3.0_f32;
580 let dt = 0.005_f32;
581
582 (0..rho_steps).map(|i| {
583 let rho = rho_min + (rho_max - rho_min) * i as f32 / (rho_steps - 1) as f32;
584 let mut state = Vec3::new(0.1, 0.0, 0.0);
585
586 for _ in 0..warmup_n {
588 let k1 = lorenz_parametric(state, sigma, rho, beta);
589 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
590 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
591 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
592 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
593 }
594
595 let zs: Vec<f32> = (0..sample_n).map(|_| {
597 let k1 = lorenz_parametric(state, sigma, rho, beta);
598 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
599 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
600 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
601 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
602 state.z
603 }).collect();
604
605 (rho, zs)
606 }).collect()
607}
608
609pub struct AttractorPool {
614 samplers: Vec<AttractorSampler>,
615 round_robin: usize,
616}
617
618impl AttractorPool {
619 pub fn new(attractor: AttractorType, n: usize) -> Self {
622 let base = initial_state_warmed(attractor);
623 let dt = attractor.recommended_dt();
624 let samplers = (0..n).map(|i| {
625 let offset_state = {
627 let mut s = base;
628 for _ in 0..(i * 200) {
629 s = rk4_step(attractor, s, dt);
630 }
631 s
632 };
633 AttractorSampler {
634 attractor,
635 state: offset_state,
636 dt,
637 time: i as f32 * 200.0 * dt,
638 scale: attractor.normalization_scale(),
639 center: Vec3::ZERO,
640 substeps: 1,
641 }
642 }).collect();
643 Self { samplers, round_robin: 0 }
644 }
645
646 pub fn next(&mut self) -> Vec3 {
648 if self.samplers.is_empty() {
649 return Vec3::ZERO;
650 }
651 let idx = self.round_robin % self.samplers.len();
652 self.round_robin = idx + 1;
653 self.samplers[idx].next()
654 }
655
656 pub fn tick_all(&mut self) {
658 for s in &mut self.samplers {
659 s.next();
660 }
661 }
662
663 pub fn positions(&self) -> Vec<Vec3> {
665 self.samplers.iter().map(|s| s.position()).collect()
666 }
667
668 pub fn set_scale(&mut self, scale: f32) {
670 for s in &mut self.samplers {
671 s.scale = scale;
672 }
673 }
674
675 pub fn set_center(&mut self, center: Vec3) {
677 for s in &mut self.samplers {
678 s.center = center;
679 }
680 }
681
682 pub fn len(&self) -> usize { self.samplers.len() }
684 pub fn is_empty(&self) -> bool { self.samplers.is_empty() }
686}
687
688pub fn poincare_section(
693 attractor: AttractorType,
694 z_level: f32,
695 n_crossings: usize,
696) -> Vec<(f32, f32)> {
697 let dt = attractor.recommended_dt();
698 let mut s = initial_state_warmed(attractor);
699 let mut crossings = Vec::with_capacity(n_crossings);
700 let mut prev_z = s.z;
701 let mut iterations = 0usize;
702 let max_iter = n_crossings * 100_000;
703
704 while crossings.len() < n_crossings && iterations < max_iter {
705 s = rk4_step(attractor, s, dt);
706 if prev_z < z_level && s.z >= z_level {
708 let t = (z_level - prev_z) / (s.z - prev_z);
710 let sx = prev_z + t * (s.x - prev_z); crossings.push((s.x, s.y));
712 let _ = sx;
713 }
714 prev_z = s.z;
715 iterations += 1;
716 }
717 crossings
718}
719
720pub fn recurrence_plot(
726 attractor: AttractorType,
727 n: usize,
728 threshold: f32,
729) -> Vec<bool> {
730 let trajectory = AttractorSampler::new(attractor)
731 .sample_trajectory(n);
732
733 let mut matrix = vec![false; n * n];
734 for i in 0..n {
735 for j in 0..n {
736 let d = (trajectory[i] - trajectory[j]).length();
737 matrix[i * n + j] = d < threshold;
738 }
739 }
740 matrix
741}
742
743pub fn velocity_color(velocity: Vec3, palette: AttractorPalette) -> glam::Vec4 {
748 let speed = velocity.length();
749 let t = (speed * 5.0).clamp(0.0, 1.0); match palette {
751 AttractorPalette::Plasma => {
752 let r = (0.5 + 0.5 * (t * std::f32::consts::TAU).sin()).clamp(0.0, 1.0);
754 let g = t.sqrt();
755 let b = (1.0 - t).powi(2);
756 glam::Vec4::new(r, g, b, 1.0)
757 }
758 AttractorPalette::Fire => {
759 let r = (t * 2.0).clamp(0.0, 1.0);
760 let g = ((t * 2.0) - 1.0).clamp(0.0, 1.0);
761 let b = 0.0;
762 glam::Vec4::new(r, g, b, 1.0)
763 }
764 AttractorPalette::Ice => {
765 let r = t * 0.2;
766 let g = t * 0.6;
767 let b = t;
768 glam::Vec4::new(r, g, b, 1.0)
769 }
770 AttractorPalette::Neon => {
771 let r = (1.0 - t) * 0.8;
772 let g = (1.0 - (t - 0.5).abs() * 2.0).max(0.0);
773 let b = t;
774 glam::Vec4::new(r, g, b, 1.0)
775 }
776 AttractorPalette::Greyscale => {
777 glam::Vec4::new(t, t, t, 1.0)
778 }
779 }
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq)]
784pub enum AttractorPalette {
785 Plasma,
787 Fire,
789 Ice,
791 Neon,
793 Greyscale,
795}
796
797#[cfg(test)]
798mod initial_state_tests {
799 use super::*;
800
801 #[test]
805 fn warmed_states_are_finite_and_off_the_origin() {
806 for &a in AttractorType::all() {
807 let s = initial_state_warmed(a);
808 assert!(s.is_finite(), "{} diverged: {s:?}", a.name());
809 assert!(s.length() > 0.05, "{} collapsed to the origin: {s:?}", a.name());
810 }
811 }
812}
813
814#[cfg(test)]
815mod step_all_tests {
816 use super::*;
817
818 #[test]
819 fn rk4_step_all_matches_single_steps() {
820 let start: Vec<Vec3> = (0..10_000).map(|i| Vec3::new(1.0 + i as f32 * 1e-3, 1.0, 1.0)).collect();
822 let mut all = start.clone();
823 for _ in 0..5 {
824 rk4_step_all(AttractorType::Lorenz, &mut all, 0.005);
825 }
826 for (i, p0) in start.iter().enumerate() {
827 let mut p = *p0;
828 for _ in 0..5 {
829 p = rk4_step(AttractorType::Lorenz, p, 0.005);
830 }
831 assert_eq!(all[i], p, "point {i}");
832 }
833 }
834}