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(0.1, 0.0, 0.0),
237 AttractorType::Sprott => Vec3::new(1.0, 0.0, 0.5),
238 AttractorType::Rabinovich => Vec3::new(0.05, 0.05, 0.5),
239 AttractorType::Burke => Vec3::new(0.6, -0.4, 0.4),
240 }
241}
242
243pub fn initial_state_warmed(attractor: AttractorType) -> Vec3 {
245 warmup(attractor, initial_state(attractor), 5000)
246}
247
248#[derive(Debug, Clone)]
253pub struct AttractorSampler {
254 pub attractor: AttractorType,
255 pub state: Vec3,
256 pub dt: f32,
257 pub time: f32,
258 pub scale: f32,
260 pub center: Vec3,
262 pub substeps: usize,
264}
265
266impl AttractorSampler {
267 pub fn new(attractor: AttractorType) -> Self {
268 let state = initial_state_warmed(attractor);
269 Self {
270 attractor,
271 state,
272 dt: attractor.recommended_dt(),
273 time: 0.0,
274 scale: attractor.normalization_scale(),
275 center: Vec3::ZERO,
276 substeps: 1,
277 }
278 }
279
280 pub fn next(&mut self) -> Vec3 {
282 for _ in 0..self.substeps {
283 self.state = rk4_step(self.attractor, self.state, self.dt);
284 self.time += self.dt;
285 }
286 self.state * self.scale + self.center
287 }
288
289 pub fn sample_trajectory(&mut self, n: usize) -> Vec<Vec3> {
291 (0..n).map(|_| self.next()).collect()
292 }
293
294 pub fn reset(&mut self) {
296 self.state = initial_state_warmed(self.attractor);
297 self.time = 0.0;
298 }
299
300 pub fn set_attractor(&mut self, attractor: AttractorType) {
302 self.attractor = attractor;
303 self.dt = attractor.recommended_dt();
304 self.scale = attractor.normalization_scale();
305 self.reset();
306 }
307
308 pub fn raw_state(&self) -> Vec3 {
310 self.state
311 }
312
313 pub fn position(&self) -> Vec3 {
315 self.state * self.scale + self.center
316 }
317
318 pub fn velocity(&self) -> Vec3 {
320 let d = derivatives(self.attractor, self.state);
321 d * (self.scale * self.dt)
322 }
323}
324
325pub fn largest_lyapunov_exponent(
336 attractor: AttractorType,
337 initial: Vec3,
338 steps: usize,
339) -> f32 {
340 let dt = attractor.recommended_dt();
341 let eps = 1e-8_f32;
342 let mut s1 = warmup(attractor, initial, 2000);
343 let mut s2 = s1 + Vec3::splat(eps / 3.0_f32.sqrt());
345
346 let mut lyapunov_sum = 0.0_f32;
347
348 for _ in 0..steps {
349 s1 = rk4_step(attractor, s1, dt);
350 s2 = rk4_step(attractor, s2, dt);
351
352 let sep = s2 - s1;
353 let d = sep.length();
354 if d < 1e-15 { continue; }
355
356 lyapunov_sum += (d / eps).ln();
357
358 s2 = s1 + sep * (eps / d);
360 }
361
362 lyapunov_sum / (steps as f32 * dt)
363}
364
365pub fn lyapunov_spectrum(
368 attractor: AttractorType,
369 initial: Vec3,
370 steps: usize,
371) -> [f32; 3] {
372 let dt = attractor.recommended_dt();
373 let eps = 1e-6_f32;
374
375 let mut state = warmup(attractor, initial, 2000);
377 let mut v1 = Vec3::new(eps, 0.0, 0.0);
378 let mut v2 = Vec3::new(0.0, eps, 0.0);
379 let mut v3 = Vec3::new(0.0, 0.0, eps);
380
381 let mut sums = [0.0_f32; 3];
382
383 for _ in 0..steps {
384 let s0 = state;
385 state = rk4_step(attractor, s0, dt);
386
387 let evolve = |v: Vec3| -> Vec3 {
389 rk4_step(attractor, s0 + v, dt) - state
390 };
391
392 v1 = evolve(v1);
393 v2 = evolve(v2);
394 v3 = evolve(v3);
395
396 let n1 = v1.length().max(1e-30);
398 sums[0] += n1.ln();
399 v1 = v1 / n1;
400
401 v2 = v2 - v1 * v2.dot(v1);
402 let n2 = v2.length().max(1e-30);
403 sums[1] += n2.ln();
404 v2 = v2 / n2;
405
406 v3 = v3 - v1 * v3.dot(v1) - v2 * v3.dot(v2);
407 let n3 = v3.length().max(1e-30);
408 sums[2] += n3.ln();
409 v3 = v3 / n3;
410
411 v1 *= eps;
413 v2 *= eps;
414 v3 *= eps;
415 }
416
417 let t = steps as f32 * dt;
418 [sums[0] / t, sums[1] / t, sums[2] / t]
419}
420
421pub fn kaplan_yorke_dimension(spectrum: [f32; 3]) -> f32 {
423 let mut sorted = spectrum;
424 sorted.sort_by(|a, b| b.partial_cmp(a).unwrap());
425
426 let mut cumsum = 0.0_f32;
427 let mut j = 0usize;
428 for (i, &l) in sorted.iter().enumerate() {
429 if cumsum + l < 0.0 { break; }
430 cumsum += l;
431 j = i + 1;
432 }
433 if j >= 3 { return 3.0; }
434 j as f32 + cumsum / sorted[j].abs().max(1e-12)
435}
436
437#[derive(Debug, Clone)]
441pub struct AttractorStats {
442 pub attractor: AttractorType,
443 pub bbox_min: Vec3,
444 pub bbox_max: Vec3,
445 pub centroid: Vec3,
446 pub variance: Vec3,
447 pub sample_count: usize,
448 pub lyapunov_max: f32,
450}
451
452impl AttractorStats {
453 pub fn compute(attractor: AttractorType, n: usize) -> Self {
455 let mut sampler = AttractorSampler::new(attractor);
456 sampler.scale = 1.0; let mut min = Vec3::splat(f32::MAX);
459 let mut max = Vec3::splat(f32::MIN);
460 let mut sum = Vec3::ZERO;
461
462 let pts: Vec<Vec3> = (0..n).map(|_| {
463 let p = sampler.next();
464 min = min.min(p);
465 max = max.max(p);
466 sum += p;
467 p
468 }).collect();
469
470 let centroid = sum / n as f32;
471 let variance = pts.iter().fold(Vec3::ZERO, |acc, &p| {
472 let d = p - centroid;
473 acc + d * d
474 }) / n as f32;
475
476 let lyapunov_max = largest_lyapunov_exponent(
477 attractor, initial_state(attractor), 5000,
478 );
479
480 Self { attractor, bbox_min: min, bbox_max: max, centroid, variance, sample_count: n, lyapunov_max }
481 }
482
483 pub fn bbox_size(&self) -> Vec3 {
485 self.bbox_max - self.bbox_min
486 }
487
488 pub fn normalizing_scale(&self) -> f32 {
490 let s = self.bbox_size();
491 let m = s.x.max(s.y).max(s.z);
492 if m < 1e-10 { 1.0 } else { 2.0 / m }
493 }
494}
495
496pub fn lorenz_parametric(s: Vec3, sigma: f32, rho: f32, beta: f32) -> Vec3 {
500 let (x, y, z) = (s.x, s.y, s.z);
501 Vec3::new(sigma * (y - x), x * (rho - z) - y, x * y - beta * z)
502}
503
504pub fn rossler_parametric(s: Vec3, a: f32, b: f32, c: f32) -> Vec3 {
506 let (x, y, z) = (s.x, s.y, s.z);
507 Vec3::new(-y - z, x + a * y, b + z * (x - c))
508}
509
510pub fn lorenz_bifurcation(
512 rho_min: f32,
513 rho_max: f32,
514 rho_steps: usize,
515 warmup_n: usize,
516 sample_n: usize,
517) -> Vec<(f32, Vec<f32>)> {
518 let sigma = 10.0_f32;
519 let beta = 8.0_f32 / 3.0_f32;
520 let dt = 0.005_f32;
521
522 (0..rho_steps).map(|i| {
523 let rho = rho_min + (rho_max - rho_min) * i as f32 / (rho_steps - 1) as f32;
524 let mut state = Vec3::new(0.1, 0.0, 0.0);
525
526 for _ in 0..warmup_n {
528 let k1 = lorenz_parametric(state, sigma, rho, beta);
529 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
530 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
531 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
532 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
533 }
534
535 let zs: Vec<f32> = (0..sample_n).map(|_| {
537 let k1 = lorenz_parametric(state, sigma, rho, beta);
538 let k2 = lorenz_parametric(state + k1 * (dt * 0.5), sigma, rho, beta);
539 let k3 = lorenz_parametric(state + k2 * (dt * 0.5), sigma, rho, beta);
540 let k4 = lorenz_parametric(state + k3 * dt, sigma, rho, beta);
541 state += (k1 + k2 * 2.0 + k3 * 2.0 + k4) * (dt / 6.0);
542 state.z
543 }).collect();
544
545 (rho, zs)
546 }).collect()
547}
548
549pub struct AttractorPool {
554 samplers: Vec<AttractorSampler>,
555 round_robin: usize,
556}
557
558impl AttractorPool {
559 pub fn new(attractor: AttractorType, n: usize) -> Self {
562 let base = initial_state_warmed(attractor);
563 let dt = attractor.recommended_dt();
564 let samplers = (0..n).map(|i| {
565 let offset_state = {
567 let mut s = base;
568 for _ in 0..(i * 200) {
569 s = rk4_step(attractor, s, dt);
570 }
571 s
572 };
573 AttractorSampler {
574 attractor,
575 state: offset_state,
576 dt,
577 time: i as f32 * 200.0 * dt,
578 scale: attractor.normalization_scale(),
579 center: Vec3::ZERO,
580 substeps: 1,
581 }
582 }).collect();
583 Self { samplers, round_robin: 0 }
584 }
585
586 pub fn next(&mut self) -> Vec3 {
588 if self.samplers.is_empty() {
589 return Vec3::ZERO;
590 }
591 let idx = self.round_robin % self.samplers.len();
592 self.round_robin = idx + 1;
593 self.samplers[idx].next()
594 }
595
596 pub fn tick_all(&mut self) {
598 for s in &mut self.samplers {
599 s.next();
600 }
601 }
602
603 pub fn positions(&self) -> Vec<Vec3> {
605 self.samplers.iter().map(|s| s.position()).collect()
606 }
607
608 pub fn set_scale(&mut self, scale: f32) {
610 for s in &mut self.samplers {
611 s.scale = scale;
612 }
613 }
614
615 pub fn set_center(&mut self, center: Vec3) {
617 for s in &mut self.samplers {
618 s.center = center;
619 }
620 }
621
622 pub fn len(&self) -> usize { self.samplers.len() }
623 pub fn is_empty(&self) -> bool { self.samplers.is_empty() }
624}
625
626pub fn poincare_section(
631 attractor: AttractorType,
632 z_level: f32,
633 n_crossings: usize,
634) -> Vec<(f32, f32)> {
635 let dt = attractor.recommended_dt();
636 let mut s = initial_state_warmed(attractor);
637 let mut crossings = Vec::with_capacity(n_crossings);
638 let mut prev_z = s.z;
639 let mut iterations = 0usize;
640 let max_iter = n_crossings * 100_000;
641
642 while crossings.len() < n_crossings && iterations < max_iter {
643 s = rk4_step(attractor, s, dt);
644 if prev_z < z_level && s.z >= z_level {
646 let t = (z_level - prev_z) / (s.z - prev_z);
648 let sx = prev_z + t * (s.x - prev_z); crossings.push((s.x, s.y));
650 let _ = sx;
651 }
652 prev_z = s.z;
653 iterations += 1;
654 }
655 crossings
656}
657
658pub fn recurrence_plot(
664 attractor: AttractorType,
665 n: usize,
666 threshold: f32,
667) -> Vec<bool> {
668 let trajectory = AttractorSampler::new(attractor)
669 .sample_trajectory(n);
670
671 let mut matrix = vec![false; n * n];
672 for i in 0..n {
673 for j in 0..n {
674 let d = (trajectory[i] - trajectory[j]).length();
675 matrix[i * n + j] = d < threshold;
676 }
677 }
678 matrix
679}
680
681pub fn velocity_color(velocity: Vec3, palette: AttractorPalette) -> glam::Vec4 {
686 let speed = velocity.length();
687 let t = (speed * 5.0).clamp(0.0, 1.0); match palette {
689 AttractorPalette::Plasma => {
690 let r = (0.5 + 0.5 * (t * std::f32::consts::TAU).sin()).clamp(0.0, 1.0);
692 let g = t.sqrt();
693 let b = (1.0 - t).powi(2);
694 glam::Vec4::new(r, g, b, 1.0)
695 }
696 AttractorPalette::Fire => {
697 let r = (t * 2.0).clamp(0.0, 1.0);
698 let g = ((t * 2.0) - 1.0).clamp(0.0, 1.0);
699 let b = 0.0;
700 glam::Vec4::new(r, g, b, 1.0)
701 }
702 AttractorPalette::Ice => {
703 let r = t * 0.2;
704 let g = t * 0.6;
705 let b = t;
706 glam::Vec4::new(r, g, b, 1.0)
707 }
708 AttractorPalette::Neon => {
709 let r = (1.0 - t) * 0.8;
710 let g = (1.0 - (t - 0.5).abs() * 2.0).max(0.0);
711 let b = t;
712 glam::Vec4::new(r, g, b, 1.0)
713 }
714 AttractorPalette::Greyscale => {
715 glam::Vec4::new(t, t, t, 1.0)
716 }
717 }
718}
719
720#[derive(Debug, Clone, Copy, PartialEq, Eq)]
722pub enum AttractorPalette {
723 Plasma,
724 Fire,
725 Ice,
726 Neon,
727 Greyscale,
728}