1#![allow(dead_code)]
11
12pub trait DiscreteMapIterate {
18 fn dim(&self) -> usize;
20 fn step(&self, state: &[f64]) -> Vec<f64>;
22
23 fn orbit(&self, init: &[f64], n: usize) -> Vec<Vec<f64>> {
25 let mut out = Vec::with_capacity(n + 1);
26 out.push(init.to_vec());
27 for _ in 0..n {
28 let next = self.step(out.last().expect("orbit is non-empty"));
29 out.push(next);
30 }
31 out
32 }
33}
34
35#[derive(Debug, Clone)]
41pub struct LogisticMap {
42 pub r: f64,
44}
45
46impl LogisticMap {
47 pub fn new(r: f64) -> Self {
49 Self { r }
50 }
51
52 pub fn apply(&self, x: f64) -> f64 {
54 self.r * x * (1.0 - x)
55 }
56
57 pub fn orbit_1d(&self, x0: f64, n: usize) -> Vec<f64> {
59 let mut orbit = Vec::with_capacity(n + 1);
60 orbit.push(x0);
61 for _ in 0..n {
62 let last = *orbit.last().expect("orbit is non-empty");
63 orbit.push(self.apply(last));
64 }
65 orbit
66 }
67
68 pub fn bifurcation_diagram(
74 r_min: f64,
75 r_max: f64,
76 n_r: usize,
77 transient: usize,
78 keep: usize,
79 ) -> Vec<(f64, f64)> {
80 let mut result = Vec::with_capacity(n_r * keep);
81 for k in 0..n_r {
82 let r = r_min + (r_max - r_min) * k as f64 / n_r.saturating_sub(1).max(1) as f64;
83 let map = LogisticMap::new(r);
84 let mut x = 0.5;
85 for _ in 0..transient {
86 x = map.apply(x);
87 }
88 for _ in 0..keep {
89 x = map.apply(x);
90 result.push((r, x));
91 }
92 }
93 result
94 }
95
96 pub fn lyapunov_exponent(&self, x0: f64, n: usize) -> Option<f64> {
100 let mut x = x0;
101 let mut sum = 0.0f64;
102 for _ in 0..n {
103 let deriv = (self.r * (1.0 - 2.0 * x)).abs();
104 if deriv < 1e-300 {
105 return None;
106 }
107 sum += deriv.ln();
108 x = self.apply(x);
109 }
110 Some(sum / n as f64)
111 }
112}
113
114impl DiscreteMapIterate for LogisticMap {
115 fn dim(&self) -> usize {
116 1
117 }
118 fn step(&self, state: &[f64]) -> Vec<f64> {
119 vec![self.apply(state[0])]
120 }
121}
122
123#[derive(Debug, Clone)]
134pub struct HenonMap {
135 pub a: f64,
137 pub b: f64,
139}
140
141impl HenonMap {
142 pub fn new(a: f64, b: f64) -> Self {
144 Self { a, b }
145 }
146
147 pub fn apply(&self, x: f64, y: f64) -> (f64, f64) {
149 (1.0 - self.a * x * x + y, self.b * x)
150 }
151
152 pub fn orbit_2d(&self, x0: f64, y0: f64, n: usize) -> Vec<(f64, f64)> {
154 let mut out = Vec::with_capacity(n + 1);
155 out.push((x0, y0));
156 for _ in 0..n {
157 let &(x, y) = out.last().expect("orbit is non-empty");
158 out.push(self.apply(x, y));
159 }
160 out
161 }
162
163 pub fn jacobian_det(&self) -> f64 {
165 -self.b
166 }
167
168 pub fn jacobian(&self, x: f64, _y: f64) -> [[f64; 2]; 2] {
174 [[-2.0 * self.a * x, 1.0], [self.b, 0.0]]
175 }
176}
177
178impl DiscreteMapIterate for HenonMap {
179 fn dim(&self) -> usize {
180 2
181 }
182 fn step(&self, state: &[f64]) -> Vec<f64> {
183 let (xn, yn) = self.apply(state[0], state[1]);
184 vec![xn, yn]
185 }
186}
187
188#[derive(Debug, Clone)]
199pub struct ChirikovStandardMap {
200 pub k: f64,
202}
203
204impl ChirikovStandardMap {
205 pub fn new(k: f64) -> Self {
207 Self { k }
208 }
209
210 pub fn apply(&self, theta: f64, p: f64) -> (f64, f64) {
213 let p_new = (p + self.k * theta.sin()).rem_euclid(std::f64::consts::TAU);
214 let theta_new = (theta + p_new).rem_euclid(std::f64::consts::TAU);
215 (theta_new, p_new)
216 }
217
218 pub fn orbit_2d(&self, theta0: f64, p0: f64, n: usize) -> Vec<(f64, f64)> {
220 let mut out = Vec::with_capacity(n + 1);
221 out.push((theta0, p0));
222 for _ in 0..n {
223 let &(th, p) = out.last().expect("orbit is non-empty");
224 out.push(self.apply(th, p));
225 }
226 out
227 }
228
229 pub fn lyapunov_exponent(&self, theta0: f64, p0: f64, n: usize) -> f64 {
231 let mut theta = theta0;
232 let mut p = p0;
233 let mut dt = 1.0f64;
235 let mut dp = 0.0f64;
236 let mut sum = 0.0f64;
237
238 for _ in 0..n {
239 let cos_th = theta.cos();
240 let dp_new = dp + self.k * cos_th * dt;
242 let dt_new = dt + dp_new;
243 let norm = (dt_new * dt_new + dp_new * dp_new).sqrt().max(1e-300);
244 sum += norm.ln();
245 dt = dt_new / norm;
246 dp = dp_new / norm;
247 let (th_new, p_new) = self.apply(theta, p);
248 theta = th_new;
249 p = p_new;
250 }
251 sum / n as f64
252 }
253}
254
255impl DiscreteMapIterate for ChirikovStandardMap {
256 fn dim(&self) -> usize {
257 2
258 }
259 fn step(&self, state: &[f64]) -> Vec<f64> {
260 let (th, p) = self.apply(state[0], state[1]);
261 vec![th, p]
262 }
263}
264
265#[derive(Debug, Clone)]
271pub struct AttractorAnalysis {
272 pub data: Vec<Vec<f64>>,
274}
275
276impl AttractorAnalysis {
277 pub fn new(data: Vec<Vec<f64>>) -> Self {
279 Self { data }
280 }
281
282 pub fn find_fixed_points<F>(&self, f: F, tol: f64) -> Vec<Vec<f64>>
286 where
287 F: Fn(&[f64]) -> Vec<f64>,
288 {
289 self.data
290 .iter()
291 .filter(|x| {
292 let fx = f(x.as_slice());
293 let dist: f64 = x
294 .iter()
295 .zip(fx.iter())
296 .map(|(a, b)| (a - b).powi(2))
297 .sum::<f64>()
298 .sqrt();
299 dist < tol
300 })
301 .cloned()
302 .collect()
303 }
304
305 pub fn find_periodic_orbits<F>(&self, f: &F, period: usize, tol: f64) -> Vec<Vec<f64>>
307 where
308 F: Fn(&[f64]) -> Vec<f64>,
309 {
310 self.data
311 .iter()
312 .filter(|x| {
313 let mut state = x.to_vec();
314 for _ in 0..period {
315 state = f(&state);
316 }
317 let dist: f64 = x
318 .iter()
319 .zip(state.iter())
320 .map(|(a, b)| (a - b).powi(2))
321 .sum::<f64>()
322 .sqrt();
323 dist < tol
324 })
325 .cloned()
326 .collect()
327 }
328
329 pub fn correlation_dimension(&self, n_radii: usize) -> Vec<(f64, f64)> {
334 let n = self.data.len();
335 if n < 2 {
336 return vec![];
337 }
338
339 let mut dists = Vec::new();
341 for i in 0..n {
342 for j in (i + 1)..n {
343 let d: f64 = self.data[i]
344 .iter()
345 .zip(self.data[j].iter())
346 .map(|(a, b)| (a - b).powi(2))
347 .sum::<f64>()
348 .sqrt();
349 dists.push(d);
350 }
351 }
352 dists.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
353
354 if dists.is_empty() {
355 return vec![];
356 }
357
358 let r_min = *dists.first().expect("dists is non-empty") * 1.1;
359 let r_max = *dists.last().expect("dists is non-empty") * 0.9;
360 if r_min >= r_max {
361 return vec![];
362 }
363
364 let total_pairs = dists.len() as f64;
365 let mut result = Vec::with_capacity(n_radii);
366
367 for k in 0..n_radii {
368 let r =
369 r_min * (r_max / r_min).powf(k as f64 / n_radii.saturating_sub(1).max(1) as f64);
370 let count = dists.partition_point(|&d| d <= r);
371 let c_r = count as f64 / total_pairs;
372 if c_r > 0.0 {
373 result.push((r.ln(), c_r.ln()));
374 }
375 }
376 result
377 }
378
379 pub fn rosenstein_lyapunov(&self, horizon: usize) -> Vec<f64> {
384 let n = self.data.len();
385 if n < 2 || horizon == 0 {
386 return vec![];
387 }
388 let steps = horizon.min(n / 2);
389 let mut sum_log = vec![0.0f64; steps];
390 let mut counts = vec![0usize; steps];
391
392 for i in 0..n {
393 let min_sep = 1usize;
395 let mut nn_idx = n;
396 let mut nn_dist = f64::INFINITY;
397 for j in 0..n {
398 if (i as isize - j as isize).unsigned_abs() < min_sep {
399 continue;
400 }
401 let d: f64 = self.data[i]
402 .iter()
403 .zip(self.data[j].iter())
404 .map(|(a, b)| (a - b).powi(2))
405 .sum::<f64>()
406 .sqrt();
407 if d < nn_dist {
408 nn_dist = d;
409 nn_idx = j;
410 }
411 }
412 if nn_idx == n {
413 continue;
414 }
415
416 for t in 0..steps {
418 let ii = i + t;
419 let jj = nn_idx + t;
420 if ii >= n || jj >= n {
421 break;
422 }
423 let d: f64 = self.data[ii]
424 .iter()
425 .zip(self.data[jj].iter())
426 .map(|(a, b)| (a - b).powi(2))
427 .sum::<f64>()
428 .sqrt();
429 if d > 1e-300 {
430 sum_log[t] += d.ln();
431 counts[t] += 1;
432 }
433 }
434 }
435
436 sum_log
437 .iter()
438 .zip(counts.iter())
439 .map(|(&s, &c)| if c > 0 { s / c as f64 } else { 0.0 })
440 .collect()
441 }
442
443 pub fn is_strange_attractor(&self) -> bool {
446 let pairs = self.correlation_dimension(20);
447 if pairs.len() < 4 {
448 return false;
449 }
450 let slope = linear_slope(&pairs);
452 slope > 1.5 && slope < 4.0
453 }
454}
455
456fn linear_slope(pairs: &[(f64, f64)]) -> f64 {
458 let n = pairs.len() as f64;
459 let sx: f64 = pairs.iter().map(|(x, _)| x).sum();
460 let sy: f64 = pairs.iter().map(|(_, y)| y).sum();
461 let sxy: f64 = pairs.iter().map(|(x, y)| x * y).sum();
462 let sxx: f64 = pairs.iter().map(|(x, _)| x * x).sum();
463 let denom = n * sxx - sx * sx;
464 if denom.abs() < 1e-12 {
465 return 0.0;
466 }
467 (n * sxy - sx * sy) / denom
468}
469
470#[derive(Debug, Clone, Copy, PartialEq)]
476pub enum BifurcationType {
477 SaddleNode,
479 Transcritical,
481 PitchforkSuper,
483 PitchforkSub,
485 HopfSuper,
487 HopfSub,
489}
490
491#[derive(Debug, Clone)]
493pub struct DynamicalNormalForm {
494 pub bif_type: BifurcationType,
496 pub mu: f64,
498}
499
500impl DynamicalNormalForm {
501 pub fn new(bif_type: BifurcationType, mu: f64) -> Self {
503 Self { bif_type, mu }
504 }
505
506 pub fn rhs(&self, state: &[f64]) -> Vec<f64> {
510 match self.bif_type {
511 BifurcationType::SaddleNode => {
512 let x = state[0];
513 vec![self.mu + x * x]
514 }
515 BifurcationType::Transcritical => {
516 let x = state[0];
517 vec![self.mu * x - x * x]
518 }
519 BifurcationType::PitchforkSuper => {
520 let x = state[0];
521 vec![self.mu * x - x * x * x]
522 }
523 BifurcationType::PitchforkSub => {
524 let x = state[0];
525 vec![self.mu * x + x * x * x]
526 }
527 BifurcationType::HopfSuper => {
528 let r = state[0];
529 let theta = if state.len() > 1 { state[1] } else { 0.0 };
530 vec![self.mu * r - r * r * r, theta + 1.0]
531 }
532 BifurcationType::HopfSub => {
533 let r = state[0];
534 let theta = if state.len() > 1 { state[1] } else { 0.0 };
535 vec![self.mu * r + r * r * r, theta + 1.0]
536 }
537 }
538 }
539
540 pub fn fixed_points_1d(&self) -> Vec<f64> {
545 match self.bif_type {
546 BifurcationType::SaddleNode => {
547 if self.mu <= 0.0 {
549 let s = (-self.mu).sqrt();
550 if s < 1e-12 { vec![0.0] } else { vec![-s, s] }
551 } else {
552 vec![]
553 }
554 }
555 BifurcationType::Transcritical => {
556 vec![0.0, self.mu]
558 }
559 BifurcationType::PitchforkSuper | BifurcationType::PitchforkSub => {
560 let mut fps = vec![0.0];
562 if self.bif_type == BifurcationType::PitchforkSuper && self.mu > 0.0 {
563 let s = self.mu.sqrt();
564 fps.push(-s);
565 fps.push(s);
566 } else if self.bif_type == BifurcationType::PitchforkSub && self.mu < 0.0 {
567 let s = (-self.mu).sqrt();
568 fps.push(-s);
569 fps.push(s);
570 }
571 fps
572 }
573 BifurcationType::HopfSuper | BifurcationType::HopfSub => {
574 let mut fps = vec![0.0];
576 if self.bif_type == BifurcationType::HopfSuper && self.mu > 0.0 {
577 fps.push(self.mu.sqrt());
578 } else if self.bif_type == BifurcationType::HopfSub && self.mu < 0.0 {
579 fps.push((-self.mu).sqrt());
580 }
581 fps
582 }
583 }
584 }
585
586 pub fn integrate_euler(&self, init: &[f64], dt: f64, n: usize) -> Vec<Vec<f64>> {
588 let mut state = init.to_vec();
589 let mut traj = Vec::with_capacity(n + 1);
590 traj.push(state.clone());
591 for _ in 0..n {
592 let rhs = self.rhs(&state);
593 for (s, r) in state.iter_mut().zip(rhs.iter()) {
594 *s += dt * r;
595 }
596 traj.push(state.clone());
597 }
598 traj
599 }
600
601 pub fn stability_1d(&self, x_star: f64) -> Stability {
604 let eps = 1e-7;
605 let rhs_plus = self.rhs(&[x_star + eps])[0];
606 let rhs_minus = self.rhs(&[x_star - eps])[0];
607 let deriv = (rhs_plus - rhs_minus) / (2.0 * eps);
608 if deriv < -1e-10 {
609 Stability::Stable
610 } else if deriv > 1e-10 {
611 Stability::Unstable
612 } else {
613 Stability::Marginal
614 }
615 }
616}
617
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub enum Stability {
621 Stable,
623 Unstable,
625 Marginal,
627}
628
629#[derive(Debug, Clone)]
638pub struct PoincareSection {
639 pub normal: Vec<f64>,
641 pub origin: Vec<f64>,
643 pub intersections: Vec<Vec<f64>>,
645}
646
647impl PoincareSection {
648 pub fn new(normal: Vec<f64>, origin: Vec<f64>) -> Self {
650 Self {
651 normal,
652 origin,
653 intersections: Vec::new(),
654 }
655 }
656
657 pub fn signed_distance(&self, p: &[f64]) -> f64 {
659 p.iter()
660 .zip(self.normal.iter())
661 .zip(self.origin.iter())
662 .map(|((pi, ni), oi)| (pi - oi) * ni)
663 .sum()
664 }
665
666 pub fn process_trajectory(&mut self, traj: &[Vec<f64>]) {
669 for w in traj.windows(2) {
670 let d0 = self.signed_distance(&w[0]);
671 let d1 = self.signed_distance(&w[1]);
672 if d0 < 0.0 && d1 >= 0.0 {
674 let t = d0.abs() / (d0.abs() + d1.abs() + 1e-300);
675 let interp: Vec<f64> = w[0]
676 .iter()
677 .zip(w[1].iter())
678 .map(|(a, b)| a + t * (b - a))
679 .collect();
680 self.intersections.push(interp);
681 }
682 }
683 }
684
685 pub fn return_map_1d(&self, idx: usize) -> Vec<(f64, f64)> {
688 let coords: Vec<f64> = self
689 .intersections
690 .iter()
691 .filter_map(|p| p.get(idx).copied())
692 .collect();
693 coords.windows(2).map(|w| (w[0], w[1])).collect()
694 }
695
696 pub fn rotation_number(&self, angle_idx_x: usize, angle_idx_y: usize) -> f64 {
701 if self.intersections.len() < 2 {
702 return 0.0;
703 }
704 let angles: Vec<f64> = self
705 .intersections
706 .iter()
707 .filter_map(|p| {
708 let x = p.get(angle_idx_x).copied()? - self.origin.get(angle_idx_x).copied()?;
709 let y = p.get(angle_idx_y).copied()? - self.origin.get(angle_idx_y).copied()?;
710 Some(y.atan2(x))
711 })
712 .collect();
713
714 let mut total = 0.0f64;
715 for w in angles.windows(2) {
716 let mut diff = w[1] - w[0];
717 while diff > std::f64::consts::PI {
719 diff -= std::f64::consts::TAU;
720 }
721 while diff < -std::f64::consts::PI {
722 diff += std::f64::consts::TAU;
723 }
724 total += diff;
725 }
726 total / (std::f64::consts::TAU * (angles.len() - 1) as f64)
727 }
728
729 pub fn count(&self) -> usize {
731 self.intersections.len()
732 }
733
734 pub fn clear(&mut self) {
736 self.intersections.clear();
737 }
738}
739
740#[derive(Debug, Clone)]
748pub struct FlowMap {
749 pub dim: usize,
751 pub dt: f64,
753}
754
755impl FlowMap {
756 pub fn new(dim: usize, dt: f64) -> Self {
758 Self { dim, dt }
759 }
760
761 pub fn advance<F>(&self, init: &[f64], n_steps: usize, f: &F) -> Vec<f64>
763 where
764 F: Fn(&[f64]) -> Vec<f64>,
765 {
766 let mut state = init.to_vec();
767 for _ in 0..n_steps {
768 state = self.rk4_step(&state, f);
769 }
770 state
771 }
772
773 pub fn trajectory<F>(&self, init: &[f64], n_steps: usize, f: &F) -> Vec<Vec<f64>>
775 where
776 F: Fn(&[f64]) -> Vec<f64>,
777 {
778 let mut traj = Vec::with_capacity(n_steps + 1);
779 traj.push(init.to_vec());
780 for _ in 0..n_steps {
781 let next = self.rk4_step(traj.last().expect("trajectory is non-empty"), f);
782 traj.push(next);
783 }
784 traj
785 }
786
787 pub fn stroboscopic_map<F>(
789 &self,
790 init: &[f64],
791 n_periods: usize,
792 steps_per_period: usize,
793 f: &F,
794 ) -> Vec<Vec<f64>>
795 where
796 F: Fn(&[f64]) -> Vec<f64>,
797 {
798 let mut state = init.to_vec();
799 let mut samples = Vec::with_capacity(n_periods + 1);
800 samples.push(state.clone());
801 for _ in 0..n_periods {
802 state = self.advance(&state, steps_per_period, f);
803 samples.push(state.clone());
804 }
805 samples
806 }
807
808 pub fn fundamental_solution_matrix<F>(
814 &self,
815 init: &[f64],
816 n_steps: usize,
817 f: &F,
818 ) -> Vec<Vec<f64>>
819 where
820 F: Fn(&[f64]) -> Vec<f64>,
821 {
822 let d = self.dim;
823 let mut pert: Vec<Vec<f64>> = (0..d)
825 .map(|i| {
826 let mut v = vec![0.0; d];
827 v[i] = 1.0;
828 v
829 })
830 .collect();
831
832 let mut state = init.to_vec();
833 let eps = 1e-6;
834
835 for _ in 0..n_steps {
836 let next_state = self.rk4_step(&state, f);
838 let mut next_pert = vec![vec![0.0f64; d]; d];
840 for (col, pv) in pert.iter().enumerate() {
841 let state_plus: Vec<f64> = state
842 .iter()
843 .zip(pv.iter())
844 .map(|(s, p)| s + eps * p)
845 .collect();
846 let f_plus = f(&state_plus);
847 let f_ref = f(&state);
848 for row in 0..d {
849 next_pert[row][col] = pv[row] + self.dt * (f_plus[row] - f_ref[row]) / eps;
850 }
851 }
852 state = next_state;
853 pert = next_pert;
854 }
855 pert
856 }
857
858 fn rk4_step<F>(&self, state: &[f64], f: &F) -> Vec<f64>
859 where
860 F: Fn(&[f64]) -> Vec<f64>,
861 {
862 let k1 = f(state);
863 let s2: Vec<f64> = state
864 .iter()
865 .zip(k1.iter())
866 .map(|(s, k)| s + 0.5 * self.dt * k)
867 .collect();
868 let k2 = f(&s2);
869 let s3: Vec<f64> = state
870 .iter()
871 .zip(k2.iter())
872 .map(|(s, k)| s + 0.5 * self.dt * k)
873 .collect();
874 let k3 = f(&s3);
875 let s4: Vec<f64> = state
876 .iter()
877 .zip(k3.iter())
878 .map(|(s, k)| s + self.dt * k)
879 .collect();
880 let k4 = f(&s4);
881 state
882 .iter()
883 .enumerate()
884 .map(|(i, s)| s + self.dt / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
885 .collect()
886 }
887}
888
889#[derive(Debug, Clone)]
895pub struct SynchronizationAnalysis;
896
897impl SynchronizationAnalysis {
898 pub fn pecora_carroll_sync(
908 drive_orbit: &[Vec<f64>],
909 response_orbit: &[Vec<f64>],
910 sync_idx: usize,
911 tol: f64,
912 ) -> bool {
913 if drive_orbit.is_empty() || response_orbit.is_empty() {
914 return false;
915 }
916 let last_drive = drive_orbit.last().expect("drive orbit is non-empty");
918 let last_response = response_orbit.last().expect("response orbit is non-empty");
919 let err: f64 = last_drive
920 .iter()
921 .zip(last_response.iter())
922 .enumerate()
923 .filter(|(i, _)| *i != sync_idx)
924 .map(|(_, (d, r))| (d - r).powi(2))
925 .sum::<f64>()
926 .sqrt();
927 err < tol
928 }
929
930 pub fn phase_synchronization_index(phi1: &[f64], phi2: &[f64]) -> f64 {
938 let n = phi1.len().min(phi2.len());
939 if n == 0 {
940 return 0.0;
941 }
942 let sum_cos: f64 = phi1
943 .iter()
944 .zip(phi2.iter())
945 .map(|(a, b)| (a - b).cos())
946 .sum();
947 let sum_sin: f64 = phi1
948 .iter()
949 .zip(phi2.iter())
950 .map(|(a, b)| (a - b).sin())
951 .sum();
952 let n_f = n as f64;
953 ((sum_cos / n_f).powi(2) + (sum_sin / n_f).powi(2)).sqrt()
954 }
955
956 pub fn generalised_sync_check(resp1: &[Vec<f64>], resp2: &[Vec<f64>], tol: f64) -> bool {
962 let last1 = match resp1.last() {
963 Some(v) => v,
964 None => return false,
965 };
966 let last2 = match resp2.last() {
967 Some(v) => v,
968 None => return false,
969 };
970 let dist: f64 = last1
971 .iter()
972 .zip(last2.iter())
973 .map(|(a, b)| (a - b).powi(2))
974 .sum::<f64>()
975 .sqrt();
976 dist < tol
977 }
978
979 pub fn mutual_information(x: &[f64], y: &[f64], n_bins: usize) -> f64 {
982 let n = x.len().min(y.len());
983 if n == 0 || n_bins == 0 {
984 return 0.0;
985 }
986 let x_min = x[..n].iter().cloned().fold(f64::INFINITY, f64::min);
987 let x_max = x[..n].iter().cloned().fold(f64::NEG_INFINITY, f64::max);
988 let y_min = y[..n].iter().cloned().fold(f64::INFINITY, f64::min);
989 let y_max = y[..n].iter().cloned().fold(f64::NEG_INFINITY, f64::max);
990
991 let x_range = (x_max - x_min).max(1e-15);
992 let y_range = (y_max - y_min).max(1e-15);
993
994 let mut joint = vec![vec![0u64; n_bins]; n_bins];
995 let mut hist_x = vec![0u64; n_bins];
996 let mut hist_y = vec![0u64; n_bins];
997
998 for i in 0..n {
999 let ix = ((x[i] - x_min) / x_range * n_bins as f64)
1000 .floor()
1001 .clamp(0.0, (n_bins - 1) as f64) as usize;
1002 let iy = ((y[i] - y_min) / y_range * n_bins as f64)
1003 .floor()
1004 .clamp(0.0, (n_bins - 1) as f64) as usize;
1005 joint[ix][iy] += 1;
1006 hist_x[ix] += 1;
1007 hist_y[iy] += 1;
1008 }
1009
1010 let n_f = n as f64;
1011 let mut mi = 0.0f64;
1012 for ix in 0..n_bins {
1013 for iy in 0..n_bins {
1014 let p_xy = joint[ix][iy] as f64 / n_f;
1015 let p_x = hist_x[ix] as f64 / n_f;
1016 let p_y = hist_y[iy] as f64 / n_f;
1017 if p_xy > 0.0 && p_x > 0.0 && p_y > 0.0 {
1018 mi += p_xy * (p_xy / (p_x * p_y)).ln();
1019 }
1020 }
1021 }
1022 mi
1023 }
1024
1025 pub fn instantaneous_phase(signal: &[f64]) -> Vec<f64> {
1031 let n = signal.len();
1032 if n < 2 {
1033 return vec![0.0; n];
1034 }
1035 let mean = signal.iter().sum::<f64>() / n as f64;
1038 let centered: Vec<f64> = signal.iter().map(|s| s - mean).collect();
1039
1040 let mut quad = vec![0.0f64; n];
1042 for i in 1..n {
1043 quad[i] = quad[i - 1] + centered[i - 1];
1044 }
1045 let q_max = quad
1047 .iter()
1048 .cloned()
1049 .fold(0.0f64, |a, b| a.max(b.abs()))
1050 .max(1e-15);
1051 let c_max = centered
1052 .iter()
1053 .cloned()
1054 .fold(0.0f64, |a, b| a.max(b.abs()))
1055 .max(1e-15);
1056 let scale = c_max / q_max;
1057
1058 let mut phase = Vec::with_capacity(n);
1059 for i in 0..n {
1060 phase.push(quad[i] * scale * centered[i].signum().max(0.0));
1061 }
1062
1063 let mut phases = Vec::with_capacity(n);
1065 for i in 0..n {
1066 phases.push((quad[i] * scale).atan2(centered[i]));
1067 }
1068 phases
1069 }
1070
1071 pub fn cross_correlation_zero_lag(x: &[f64], y: &[f64]) -> f64 {
1073 let n = x.len().min(y.len());
1074 if n == 0 {
1075 return 0.0;
1076 }
1077 let mx = x.iter().sum::<f64>() / n as f64;
1078 let my = y.iter().sum::<f64>() / n as f64;
1079 let num: f64 = x[..n]
1080 .iter()
1081 .zip(y[..n].iter())
1082 .map(|(a, b)| (a - mx) * (b - my))
1083 .sum();
1084 let sx: f64 = x[..n].iter().map(|a| (a - mx).powi(2)).sum::<f64>().sqrt();
1085 let sy: f64 = y[..n].iter().map(|b| (b - my).powi(2)).sum::<f64>().sqrt();
1086 if sx < 1e-15 || sy < 1e-15 {
1087 return 0.0;
1088 }
1089 num / (sx * sy)
1090 }
1091}
1092
1093#[derive(Debug, Clone)]
1099pub struct LorenzSystem {
1100 pub sigma: f64,
1102 pub rho: f64,
1104 pub beta: f64,
1106}
1107
1108impl LorenzSystem {
1109 pub fn standard() -> Self {
1111 Self {
1112 sigma: 10.0,
1113 rho: 28.0,
1114 beta: 8.0 / 3.0,
1115 }
1116 }
1117
1118 pub fn rhs(&self, state: &[f64]) -> Vec<f64> {
1120 let (x, y, z) = (state[0], state[1], state[2]);
1121 vec![
1122 self.sigma * (y - x),
1123 x * (self.rho - z) - y,
1124 x * y - self.beta * z,
1125 ]
1126 }
1127}
1128
1129#[cfg(test)]
1134mod tests {
1135 use super::*;
1136
1137 #[test]
1140 fn test_logistic_fixed_point_stability() {
1141 let map = LogisticMap::new(2.0);
1143 let x_star = (map.r - 1.0) / map.r;
1144 let mut x = 0.4;
1145 for _ in 0..200 {
1146 x = map.apply(x);
1147 }
1148 assert!(
1149 (x - x_star).abs() < 1e-6,
1150 "should converge to {x_star}, got {x}"
1151 );
1152 }
1153
1154 #[test]
1155 fn test_logistic_orbit_length() {
1156 let map = LogisticMap::new(3.5);
1157 let orbit = map.orbit_1d(0.3, 50);
1158 assert_eq!(orbit.len(), 51);
1159 }
1160
1161 #[test]
1162 fn test_logistic_orbit_bounded() {
1163 let map = LogisticMap::new(3.9);
1164 let orbit = map.orbit_1d(0.5, 1000);
1165 for &x in &orbit {
1166 assert!((0.0..=1.0).contains(&x), "orbit escaped [0,1]: {x}");
1167 }
1168 }
1169
1170 #[test]
1171 fn test_logistic_lyapunov_chaotic() {
1172 let map = LogisticMap::new(4.0);
1174 let le = map.lyapunov_exponent(0.3, 5000).unwrap();
1175 assert!(le > 0.4, "should be positive (chaotic), got {le}");
1176 }
1177
1178 #[test]
1179 fn test_logistic_lyapunov_stable() {
1180 let map = LogisticMap::new(2.0);
1183 let mut x = 0.3f64;
1186 for _ in 0..500 {
1187 x = map.apply(x);
1188 }
1189 assert!((x - 0.5).abs() < 1e-6, "should converge to x*=0.5, got {x}");
1191 if let Some(le) = map.lyapunov_exponent(0.3, 200) {
1193 assert!(
1194 le < 0.1,
1195 "stable fixed point should have non-positive LE, got {le}"
1196 );
1197 }
1198 }
1200
1201 #[test]
1202 fn test_logistic_bifurcation_diagram_size() {
1203 let diag = LogisticMap::bifurcation_diagram(2.5, 4.0, 10, 200, 50);
1204 assert_eq!(diag.len(), 10 * 50);
1205 }
1206
1207 #[test]
1208 fn test_logistic_trait_orbit() {
1209 let map = LogisticMap::new(3.2);
1210 let orbit = map.orbit(&[0.5], 20);
1211 assert_eq!(orbit.len(), 21);
1212 assert_eq!(orbit[0], vec![0.5]);
1213 }
1214
1215 #[test]
1218 fn test_henon_orbit_first_step() {
1219 let map = HenonMap::new(1.4, 0.3);
1220 let (xn, yn) = map.apply(0.0, 0.0);
1221 assert!((xn - 1.0).abs() < 1e-12);
1222 assert!(yn.abs() < 1e-12);
1223 }
1224
1225 #[test]
1226 fn test_henon_jacobian_det_constant() {
1227 let map = HenonMap::new(1.4, 0.3);
1228 assert!((map.jacobian_det() - (-0.3)).abs() < 1e-12);
1229 }
1230
1231 #[test]
1232 fn test_henon_orbit_length() {
1233 let map = HenonMap::new(1.4, 0.3);
1234 let orbit = map.orbit_2d(0.0, 0.0, 100);
1235 assert_eq!(orbit.len(), 101);
1236 }
1237
1238 #[test]
1239 fn test_henon_jacobian_structure() {
1240 let map = HenonMap::new(1.4, 0.3);
1241 let j = map.jacobian(1.0, 0.5);
1242 assert!((j[0][0] - (-2.0 * 1.4)).abs() < 1e-12);
1243 assert!((j[0][1] - 1.0).abs() < 1e-12);
1244 assert!((j[1][0] - 0.3).abs() < 1e-12);
1245 assert!(j[1][1].abs() < 1e-12);
1246 }
1247
1248 #[test]
1249 fn test_henon_trait_step() {
1250 let map = HenonMap::new(1.4, 0.3);
1251 let state = vec![0.0, 0.0];
1252 let next = map.step(&state);
1253 assert_eq!(next.len(), 2);
1254 }
1255
1256 #[test]
1259 fn test_chirikov_orbit_stays_in_torus() {
1260 let map = ChirikovStandardMap::new(0.5);
1261 let orbit = map.orbit_2d(0.1, 0.2, 1000);
1262 let tau = std::f64::consts::TAU;
1263 for (th, p) in orbit {
1264 assert!(th >= 0.0 && th < tau, "theta out of [0,2π): {th}");
1265 assert!(p >= 0.0 && p < tau, "p out of [0,2π): {p}");
1266 }
1267 }
1268
1269 #[test]
1270 fn test_chirikov_zero_k_is_twist_map() {
1271 let map = ChirikovStandardMap::new(0.0);
1273 let (th, p) = map.apply(1.0, 0.5);
1274 let tau = std::f64::consts::TAU;
1275 assert!((p - 0.5).abs() < 1e-12, "p should not change: {p}");
1276 assert!(
1277 (th - (1.5_f64).rem_euclid(tau)).abs() < 1e-12,
1278 "theta = 1+p mod 2π: {th}"
1279 );
1280 }
1281
1282 #[test]
1283 fn test_chirikov_lyapunov_chaotic() {
1284 let map = ChirikovStandardMap::new(5.0);
1286 let le = map.lyapunov_exponent(1.0, 1.5, 5000);
1287 assert!(le > 0.5, "K=5 should be chaotic, got LE={le}");
1288 }
1289
1290 #[test]
1291 fn test_chirikov_trait_orbit() {
1292 let map = ChirikovStandardMap::new(1.0);
1293 let orbit = map.orbit(&[0.5, 0.5], 10);
1294 assert_eq!(orbit.len(), 11);
1295 }
1296
1297 #[test]
1300 fn test_attractor_find_fixed_points_identity() {
1301 let data = vec![vec![0.0], vec![0.5], vec![1.0], vec![2.0]];
1303 let analysis = AttractorAnalysis::new(data);
1304 let fps = analysis.find_fixed_points(|x| x.to_vec(), 1e-9);
1305 assert!(
1306 !fps.is_empty(),
1307 "identity map has every point as fixed point"
1308 );
1309 }
1310
1311 #[test]
1312 fn test_attractor_correlation_dimension_returns_pairs() {
1313 let data: Vec<Vec<f64>> = (0..50).map(|i| vec![i as f64 * 0.1]).collect();
1314 let analysis = AttractorAnalysis::new(data);
1315 let pairs = analysis.correlation_dimension(10);
1316 assert!(!pairs.is_empty(), "should produce log-log pairs");
1317 }
1318
1319 #[test]
1320 fn test_attractor_rosenstein_length() {
1321 let data: Vec<Vec<f64>> = (0..100)
1322 .map(|i| {
1323 let t = i as f64 * 0.1;
1324 vec![t.sin(), t.cos()]
1325 })
1326 .collect();
1327 let analysis = AttractorAnalysis::new(data);
1328 let div = analysis.rosenstein_lyapunov(10);
1329 assert_eq!(div.len(), 10);
1330 }
1331
1332 #[test]
1333 fn test_attractor_find_periodic_orbit() {
1334 let map = LogisticMap::new(3.2);
1336 let mut x = 0.5f64;
1338 for _ in 0..1000 {
1339 x = map.apply(x);
1340 }
1341 let p1 = x;
1342 let p2 = map.apply(p1);
1343 let data = vec![vec![p1], vec![p2]];
1344 let analysis = AttractorAnalysis::new(data);
1345 let po = analysis.find_periodic_orbits(&|s: &[f64]| vec![map.apply(s[0])], 2, 1e-6);
1346 assert!(!po.is_empty(), "should find period-2 orbit");
1347 }
1348
1349 #[test]
1352 fn test_normal_form_saddle_node_fixed_points() {
1353 let nf = DynamicalNormalForm::new(BifurcationType::SaddleNode, -1.0);
1354 let fps = nf.fixed_points_1d();
1355 assert_eq!(fps.len(), 2, "two fps for μ<0");
1356 for &x in &fps {
1357 let rhs_val = nf.rhs(&[x])[0];
1358 assert!(rhs_val.abs() < 1e-10, "not a fixed point: rhs={rhs_val}");
1359 }
1360 }
1361
1362 #[test]
1363 fn test_normal_form_saddle_node_no_fps_positive_mu() {
1364 let nf = DynamicalNormalForm::new(BifurcationType::SaddleNode, 1.0);
1365 let fps = nf.fixed_points_1d();
1366 assert!(fps.is_empty(), "no fps for μ>0 in saddle-node");
1367 }
1368
1369 #[test]
1370 fn test_normal_form_transcritical_fps() {
1371 let nf = DynamicalNormalForm::new(BifurcationType::Transcritical, 2.0);
1372 let fps = nf.fixed_points_1d();
1373 assert_eq!(fps.len(), 2);
1374 for &x in &fps {
1375 let rhs_val = nf.rhs(&[x])[0];
1376 assert!(
1377 rhs_val.abs() < 1e-10,
1378 "not a fixed point: x={x}, rhs={rhs_val}"
1379 );
1380 }
1381 }
1382
1383 #[test]
1384 fn test_normal_form_pitchfork_super_three_fps() {
1385 let nf = DynamicalNormalForm::new(BifurcationType::PitchforkSuper, 1.0);
1386 let fps = nf.fixed_points_1d();
1387 assert_eq!(fps.len(), 3, "super-critical pitchfork μ>0 has 3 fps");
1388 }
1389
1390 #[test]
1391 fn test_normal_form_stability_classification() {
1392 let nf = DynamicalNormalForm::new(BifurcationType::PitchforkSuper, 1.0);
1394 assert_eq!(nf.stability_1d(0.0), Stability::Unstable);
1395 assert_eq!(nf.stability_1d(1.0), Stability::Stable);
1396 assert_eq!(nf.stability_1d(-1.0), Stability::Stable);
1397 }
1398
1399 #[test]
1400 fn test_normal_form_euler_integration_converges() {
1401 let nf = DynamicalNormalForm::new(BifurcationType::SaddleNode, -1.0);
1403 let traj = nf.integrate_euler(&[-0.9], 0.01, 2000);
1404 let last = traj.last().unwrap()[0];
1405 assert!(
1406 (last - (-1.0)).abs() < 0.05,
1407 "should converge near -1, got {last}"
1408 );
1409 }
1410
1411 #[test]
1412 fn test_normal_form_hopf_super_limit_cycle() {
1413 let nf = DynamicalNormalForm::new(BifurcationType::HopfSuper, 1.0);
1415 let traj = nf.integrate_euler(&[0.1, 0.0], 0.01, 5000);
1416 let last_r = traj.last().unwrap()[0];
1417 assert!((last_r - 1.0).abs() < 0.05, "limit cycle r≈1, got {last_r}");
1418 }
1419
1420 #[test]
1423 fn test_poincare_section_circle() {
1424 let n = 1000usize;
1426 let traj: Vec<Vec<f64>> = (0..=n)
1427 .map(|i| {
1428 let t = i as f64 * std::f64::consts::TAU / n as f64 * 5.0;
1429 vec![t.cos(), t.sin()]
1430 })
1431 .collect();
1432 let mut section = PoincareSection::new(vec![0.0, 1.0], vec![0.0, 0.0]);
1433 section.process_trajectory(&traj);
1434 assert!(
1436 section.count() >= 4,
1437 "expected ~5 crossings, got {}",
1438 section.count()
1439 );
1440 }
1441
1442 #[test]
1443 fn test_poincare_section_rotation_number() {
1444 let n = 2000usize;
1445 let traj: Vec<Vec<f64>> = (0..=n)
1446 .map(|i| {
1447 let t = i as f64 * std::f64::consts::TAU / n as f64 * 3.0;
1448 vec![t.cos(), t.sin()]
1449 })
1450 .collect();
1451 let mut section = PoincareSection::new(vec![0.0, 1.0], vec![0.0, 0.0]);
1452 section.process_trajectory(&traj);
1453 let rn = section.rotation_number(0, 1);
1454 assert!(rn.abs() < 2.0, "rotation number should be finite, got {rn}");
1456 }
1457
1458 #[test]
1459 fn test_poincare_clear() {
1460 let traj: Vec<Vec<f64>> = (0..=100)
1461 .map(|i| {
1462 let t = i as f64 * 0.1;
1463 vec![t.cos(), t.sin()]
1464 })
1465 .collect();
1466 let mut section = PoincareSection::new(vec![0.0, 1.0], vec![0.0, 0.0]);
1467 section.process_trajectory(&traj);
1468 section.clear();
1469 assert_eq!(section.count(), 0);
1470 }
1471
1472 #[test]
1473 fn test_poincare_return_map() {
1474 let n = 2000usize;
1475 let traj: Vec<Vec<f64>> = (0..=n)
1476 .map(|i| {
1477 let t = i as f64 * std::f64::consts::TAU / n as f64 * 5.0;
1478 vec![t.cos(), t.sin()]
1479 })
1480 .collect();
1481 let mut section = PoincareSection::new(vec![0.0, 1.0], vec![0.0, 0.0]);
1482 section.process_trajectory(&traj);
1483 let rm = section.return_map_1d(0);
1484 assert_eq!(rm.len(), section.count().saturating_sub(1));
1486 }
1487
1488 #[test]
1491 fn test_flow_simple_harmonic_oscillator() {
1492 let f = |s: &[f64]| vec![s[1], -s[0]];
1494 let flow = FlowMap::new(2, 0.01);
1495 let final_state = flow.advance(&[1.0, 0.0], 628, &f);
1497 assert!(
1499 (final_state[0] - 1.0).abs() < 0.1,
1500 "x≈1, got {}",
1501 final_state[0]
1502 );
1503 assert!(final_state[1].abs() < 0.1, "y≈0, got {}", final_state[1]);
1504 }
1505
1506 #[test]
1507 fn test_flow_trajectory_length() {
1508 let f = |s: &[f64]| vec![s[1], -s[0]];
1509 let flow = FlowMap::new(2, 0.01);
1510 let traj = flow.trajectory(&[1.0, 0.0], 100, &f);
1511 assert_eq!(traj.len(), 101);
1512 }
1513
1514 #[test]
1515 fn test_flow_stroboscopic_map() {
1516 let f = |s: &[f64]| vec![s[1], -s[0]];
1517 let flow = FlowMap::new(2, 0.01);
1518 let samples = flow.stroboscopic_map(&[1.0, 0.0], 5, 10, &f);
1519 assert_eq!(samples.len(), 6);
1520 }
1521
1522 #[test]
1523 fn test_flow_energy_conservation_sho() {
1524 let f = |s: &[f64]| vec![s[1], -s[0]];
1526 let flow = FlowMap::new(2, 0.001);
1527 let init = vec![1.0, 0.0];
1528 let energy_init: f64 = init[0] * init[0] + init[1] * init[1];
1529 let final_state = flow.advance(&init, 1000, &f);
1530 let energy_final: f64 = final_state[0] * final_state[0] + final_state[1] * final_state[1];
1531 assert!(
1532 (energy_final - energy_init).abs() < 0.01,
1533 "energy not conserved: {energy_final}"
1534 );
1535 }
1536
1537 #[test]
1538 fn test_flow_fundamental_solution_matrix() {
1539 let f = |s: &[f64]| vec![s[1], -s[0]];
1540 let flow = FlowMap::new(2, 0.01);
1541 let fsm = flow.fundamental_solution_matrix(&[1.0, 0.0], 10, &f);
1542 assert_eq!(fsm.len(), 2);
1543 assert_eq!(fsm[0].len(), 2);
1544 }
1545
1546 #[test]
1549 fn test_phase_sync_index_identical() {
1550 let phi: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
1552 let r = SynchronizationAnalysis::phase_synchronization_index(&phi, &phi);
1553 assert!((r - 1.0).abs() < 1e-6, "identical phases → R=1, got {r}");
1554 }
1555
1556 #[test]
1557 fn test_phase_sync_index_random_low() {
1558 let phi1: Vec<f64> = (0..100).map(|i| i as f64 * 0.1).collect();
1560 let phi2: Vec<f64> = (0..100).map(|i| i as f64 * 0.3 + 0.7).collect();
1561 let r = SynchronizationAnalysis::phase_synchronization_index(&phi1, &phi2);
1562 assert!(r < 0.8, "asynchronous phases → low R, got {r}");
1563 }
1564
1565 #[test]
1566 fn test_generalised_sync_converged() {
1567 let traj1: Vec<Vec<f64>> = (0..10).map(|_| vec![1.0, 2.0]).collect();
1568 let traj2: Vec<Vec<f64>> = (0..10).map(|_| vec![1.0, 2.0]).collect();
1569 assert!(SynchronizationAnalysis::generalised_sync_check(
1570 &traj1, &traj2, 1e-9
1571 ));
1572 }
1573
1574 #[test]
1575 fn test_generalised_sync_diverged() {
1576 let traj1: Vec<Vec<f64>> = (0..10).map(|_| vec![1.0]).collect();
1577 let traj2: Vec<Vec<f64>> = (0..10).map(|_| vec![100.0]).collect();
1578 assert!(!SynchronizationAnalysis::generalised_sync_check(
1579 &traj1, &traj2, 1.0
1580 ));
1581 }
1582
1583 #[test]
1584 fn test_mutual_information_identical_signals() {
1585 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1586 let mi = SynchronizationAnalysis::mutual_information(&x, &x, 10);
1587 assert!(
1588 mi > 0.0,
1589 "MI should be positive for identical signals, got {mi}"
1590 );
1591 }
1592
1593 #[test]
1594 fn test_mutual_information_independent_signals() {
1595 let x: Vec<f64> = (0..100).map(|i| i as f64).collect();
1596 let y: Vec<f64> = (0..100).map(|i| (i * 7 + 3) as f64 % 100.0).collect();
1597 let mi = SynchronizationAnalysis::mutual_information(&x, &y, 10);
1598 assert!(mi >= 0.0, "MI should be non-negative, got {mi}");
1600 }
1601
1602 #[test]
1603 fn test_cross_correlation_identical() {
1604 let x: Vec<f64> = (0..50).map(|i| (i as f64 * 0.3).sin()).collect();
1605 let cc = SynchronizationAnalysis::cross_correlation_zero_lag(&x, &x);
1606 assert!(
1607 (cc - 1.0).abs() < 1e-6,
1608 "self-correlation should be 1, got {cc}"
1609 );
1610 }
1611
1612 #[test]
1613 fn test_cross_correlation_anticorrelated() {
1614 let x: Vec<f64> = (0..50).map(|i| i as f64).collect();
1615 let y: Vec<f64> = x.iter().map(|v| -v).collect();
1616 let cc = SynchronizationAnalysis::cross_correlation_zero_lag(&x, &y);
1617 assert!(
1618 (cc + 1.0).abs() < 1e-6,
1619 "anti-correlated: should be -1, got {cc}"
1620 );
1621 }
1622
1623 #[test]
1624 fn test_instantaneous_phase_length() {
1625 let sig: Vec<f64> = (0..100).map(|i| (i as f64 * 0.2).sin()).collect();
1626 let phase = SynchronizationAnalysis::instantaneous_phase(&sig);
1627 assert_eq!(phase.len(), sig.len());
1628 }
1629
1630 #[test]
1631 fn test_pecora_carroll_identical_sync() {
1632 let traj: Vec<Vec<f64>> = (0..10).map(|i| vec![i as f64, (i as f64).sin()]).collect();
1633 assert!(SynchronizationAnalysis::pecora_carroll_sync(
1634 &traj, &traj, 0, 1e-9
1635 ));
1636 }
1637
1638 #[test]
1641 fn test_lorenz_trajectory() {
1642 let lorenz = LorenzSystem::standard();
1643 let flow = FlowMap::new(3, 0.01);
1644 let init = vec![1.0, 0.0, 0.0];
1645 let traj = flow.trajectory(&init, 100, &|s| lorenz.rhs(s));
1646 assert_eq!(traj.len(), 101);
1647 }
1648
1649 #[test]
1650 fn test_lorenz_sensitivity() {
1651 let lorenz = LorenzSystem::standard();
1654 let flow = FlowMap::new(3, 0.01);
1655 let init1 = vec![1.0, 1.0, 1.0];
1656 let init2 = vec![1.0 + 1e-5, 1.0, 1.0];
1657 let s1 = flow.advance(&init1, 5000, &|s| lorenz.rhs(s));
1658 let s2 = flow.advance(&init2, 5000, &|s| lorenz.rhs(s));
1659 let dist: f64 = s1
1660 .iter()
1661 .zip(s2.iter())
1662 .map(|(a, b)| (a - b) * (a - b))
1663 .sum::<f64>()
1664 .sqrt();
1665 assert!(dist > 1e-3, "Lorenz should show sensitivity, dist={dist}");
1666 }
1667
1668 #[test]
1669 fn test_lorenz_poincare_section() {
1670 let lorenz = LorenzSystem::standard();
1671 let flow = FlowMap::new(3, 0.01);
1672 let mut state = vec![1.0, 1.0, 1.0];
1673 state = flow.advance(&state, 1000, &|s| lorenz.rhs(s));
1675 let traj = flow.trajectory(&state, 5000, &|s| lorenz.rhs(s));
1676 let mut section = PoincareSection::new(vec![0.0, 0.0, 1.0], vec![0.0, 0.0, 27.0]);
1678 section.process_trajectory(&traj);
1679 assert!(section.count() > 5, "Lorenz should cross z=27 many times");
1680 }
1681
1682 #[test]
1683 fn test_attractor_analysis_lorenz_is_strange() {
1684 let lorenz = LorenzSystem::standard();
1686 let flow = FlowMap::new(3, 0.02);
1687 let mut state = vec![1.0, 1.0, 1.0];
1688 state = flow.advance(&state, 500, &|s| lorenz.rhs(s));
1689 let traj = flow.trajectory(&state, 400, &|s| lorenz.rhs(s));
1690 let data: Vec<Vec<f64>> = traj.to_vec();
1691 let analysis = AttractorAnalysis::new(data);
1692 let pairs = analysis.correlation_dimension(15);
1693 assert!(!pairs.is_empty(), "should compute correlation dimension");
1695 }
1696
1697 #[test]
1700 fn test_linear_slope_perfect() {
1701 let pairs: Vec<(f64, f64)> = (0..5).map(|i| (i as f64, 2.0 * i as f64 + 1.0)).collect();
1703 let s = linear_slope(&pairs);
1704 assert!((s - 2.0).abs() < 1e-6, "slope should be 2, got {s}");
1705 }
1706
1707 #[test]
1708 fn test_linear_slope_flat() {
1709 let pairs: Vec<(f64, f64)> = (0..5).map(|i| (i as f64, 3.0)).collect();
1710 let s = linear_slope(&pairs);
1711 assert!(s.abs() < 1e-6, "flat line has slope 0, got {s}");
1712 }
1713}