1#![allow(clippy::needless_range_loop)]
2#![allow(dead_code)]
13
14#[inline]
20pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
21 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
22}
23
24#[inline]
26pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
27 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
28}
29
30#[inline]
32pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
33 [a[0] * s, a[1] * s, a[2] * s]
34}
35
36#[inline]
38pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
39 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
40}
41
42#[inline]
44pub fn norm3(a: [f64; 3]) -> f64 {
45 dot3(a, a).sqrt()
46}
47
48#[inline]
50pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
51 [
52 a[1] * b[2] - a[2] * b[1],
53 a[2] * b[0] - a[0] * b[2],
54 a[0] * b[1] - a[1] * b[0],
55 ]
56}
57
58pub trait OdeSystem {
64 fn dim(&self) -> usize;
66
67 fn rhs(&self, t: f64, x: &[f64]) -> Vec<f64>;
69}
70
71pub fn rk4_step(sys: &dyn OdeSystem, t: f64, x: &[f64], h: f64) -> Vec<f64> {
80 let n = x.len();
81 let k1 = sys.rhs(t, x);
82 let x2: Vec<f64> = (0..n).map(|i| x[i] + 0.5 * h * k1[i]).collect();
83 let k2 = sys.rhs(t + 0.5 * h, &x2);
84 let x3: Vec<f64> = (0..n).map(|i| x[i] + 0.5 * h * k2[i]).collect();
85 let k3 = sys.rhs(t + 0.5 * h, &x3);
86 let x4: Vec<f64> = (0..n).map(|i| x[i] + h * k3[i]).collect();
87 let k4 = sys.rhs(t + h, &x4);
88 (0..n)
89 .map(|i| x[i] + h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
90 .collect()
91}
92
93pub fn integrate_rk4(
97 sys: &dyn OdeSystem,
98 x0: &[f64],
99 t0: f64,
100 t_end: f64,
101 h: f64,
102) -> Vec<(f64, Vec<f64>)> {
103 let mut result = Vec::new();
104 let mut t = t0;
105 let mut x = x0.to_vec();
106 result.push((t, x.clone()));
107 while t + h <= t_end + 1e-12 {
108 x = rk4_step(sys, t, &x, h);
109 t += h;
110 result.push((t, x.clone()));
111 }
112 result
113}
114
115pub fn numerical_jacobian(sys: &dyn OdeSystem, t: f64, x: &[f64], eps: f64) -> Vec<f64> {
124 let n = x.len();
125 let mut jac = vec![0.0f64; n * n];
126 for j in 0..n {
127 let mut xp = x.to_vec();
128 let mut xm = x.to_vec();
129 xp[j] += eps;
130 xm[j] -= eps;
131 let fp = sys.rhs(t, &xp);
132 let fm = sys.rhs(t, &xm);
133 for i in 0..n {
134 jac[i * n + j] = (fp[i] - fm[i]) / (2.0 * eps);
135 }
136 }
137 jac
138}
139
140#[derive(Debug, Clone)]
146pub struct FixedPointResult {
147 pub point: Vec<f64>,
149 pub residual: f64,
151 pub converged: bool,
153 pub iterations: usize,
155}
156
157#[allow(clippy::too_many_arguments)]
163pub fn find_fixed_point(
164 sys: &dyn OdeSystem,
165 x0: &[f64],
166 tol: f64,
167 max_iter: usize,
168 eps_jac: f64,
169) -> FixedPointResult {
170 let n = x0.len();
171 let mut x = x0.to_vec();
172 for iter in 0..max_iter {
173 let f = sys.rhs(0.0, &x);
174 let res: f64 = f.iter().map(|v| v * v).sum::<f64>().sqrt();
175 if res < tol {
176 return FixedPointResult {
177 point: x,
178 residual: res,
179 converged: true,
180 iterations: iter,
181 };
182 }
183 let jac = numerical_jacobian(sys, 0.0, &x, eps_jac);
184 let delta = gauss_solve(&jac, n, &f.iter().map(|v| -v).collect::<Vec<_>>());
186 match delta {
187 Some(d) => {
188 for i in 0..n {
189 x[i] += d[i];
190 }
191 }
192 None => break,
193 }
194 }
195 let f = sys.rhs(0.0, &x);
196 let res: f64 = f.iter().map(|v| v * v).sum::<f64>().sqrt();
197 FixedPointResult {
198 point: x,
199 residual: res,
200 converged: false,
201 iterations: max_iter,
202 }
203}
204
205fn gauss_solve(a_flat: &[f64], n: usize, b: &[f64]) -> Option<Vec<f64>> {
209 let mut mat: Vec<f64> = a_flat.to_vec();
210 let mut rhs: Vec<f64> = b.to_vec();
211 for col in 0..n {
212 let mut max_row = col;
214 let mut max_val = mat[col * n + col].abs();
215 for row in (col + 1)..n {
216 let v = mat[row * n + col].abs();
217 if v > max_val {
218 max_val = v;
219 max_row = row;
220 }
221 }
222 if max_val < 1e-14 {
223 return None;
224 }
225 if max_row != col {
226 for k in 0..n {
227 mat.swap(col * n + k, max_row * n + k);
228 }
229 rhs.swap(col, max_row);
230 }
231 let pivot = mat[col * n + col];
232 for row in (col + 1)..n {
233 let factor = mat[row * n + col] / pivot;
234 for k in col..n {
235 let val = mat[col * n + k];
236 mat[row * n + k] -= factor * val;
237 }
238 rhs[row] -= factor * rhs[col];
239 }
240 }
241 let mut x = vec![0.0f64; n];
243 for i in (0..n).rev() {
244 let mut sum = rhs[i];
245 for j in (i + 1)..n {
246 sum -= mat[i * n + j] * x[j];
247 }
248 x[i] = sum / mat[i * n + i];
249 }
250 Some(x)
251}
252
253pub fn eigen2(a: f64, b: f64, c: f64, d: f64) -> ((f64, f64), (f64, f64)) {
262 let tr = a + d;
263 let det = a * d - b * c;
264 let disc = tr * tr - 4.0 * det;
265 if disc >= 0.0 {
266 let sq = disc.sqrt();
267 (((tr + sq) / 2.0, 0.0), ((tr - sq) / 2.0, 0.0))
268 } else {
269 let sq = (-disc).sqrt();
270 ((tr / 2.0, sq / 2.0), (tr / 2.0, -sq / 2.0))
271 }
272}
273
274pub fn stability_2d(a: f64, b: f64, c: f64, d: f64) -> &'static str {
279 let tr = a + d;
280 let det = a * d - b * c;
281 let disc = tr * tr - 4.0 * det;
282 if det < 0.0 {
283 "saddle"
284 } else if disc >= 0.0 {
285 if tr < -1e-10 {
286 "stable_node"
287 } else if tr > 1e-10 {
288 "unstable_node"
289 } else {
290 "center"
291 }
292 } else if tr < -1e-10 {
293 "stable_spiral"
294 } else if tr > 1e-10 {
295 "unstable_spiral"
296 } else {
297 "center"
298 }
299}
300
301pub fn lyapunov_exponents_qr(sys: &dyn OdeSystem, x0: &[f64], h: f64, n_steps: usize) -> Vec<f64> {
314 let n = sys.dim();
315 let mut x = x0.to_vec();
316 let mut q: Vec<Vec<f64>> = (0..n)
318 .map(|i| {
319 let mut v = vec![0.0f64; n];
320 v[i] = 1.0;
321 v
322 })
323 .collect();
324 let mut sums = vec![0.0f64; n];
325 let total_time = h * n_steps as f64;
326
327 for _step in 0..n_steps {
328 let t = _step as f64 * h;
329 x = rk4_step(sys, t, &x, h);
331 let jac = numerical_jacobian(sys, t, &x, 1e-6);
333 let mut new_q: Vec<Vec<f64>> = Vec::with_capacity(n);
334 for col in 0..n {
335 let mut w = vec![0.0f64; n];
337 for i in 0..n {
338 for j in 0..n {
339 w[i] += jac[i * n + j] * q[col][j];
340 }
341 }
342 let evolved: Vec<f64> = (0..n).map(|i| q[col][i] + h * w[i]).collect();
344 new_q.push(evolved);
345 }
346 let mut r_diag = vec![0.0f64; n];
348 for col in 0..n {
349 let mut v = new_q[col].clone();
350 for prev in 0..col {
351 let proj: f64 = (0..n).map(|i| q[prev][i] * v[i]).sum();
352 for i in 0..n {
353 v[i] -= proj * q[prev][i];
354 }
355 }
356 let norm: f64 = v.iter().map(|vi| vi * vi).sum::<f64>().sqrt();
357 r_diag[col] = norm;
358 if norm > 1e-14 {
359 q[col] = v.iter().map(|vi| vi / norm).collect();
360 }
361 sums[col] += if norm > 1e-14 { norm.ln() } else { -100.0 };
362 }
363 let _ = r_diag;
364 }
365 let mut exponents: Vec<f64> = sums.iter().map(|s| s / total_time).collect();
366 exponents.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
367 exponents
368}
369
370#[derive(Debug, Clone, PartialEq)]
376pub enum BifurcationType {
377 SaddleNode,
379 Transcritical,
381 Pitchfork,
383 Hopf,
385 None,
387}
388
389#[derive(Debug, Clone)]
391pub struct BifurcationEvent {
392 pub param_value: f64,
394 pub bif_type: BifurcationType,
396 pub fixed_point: Vec<f64>,
398}
399
400#[derive(Debug, Clone)]
414pub struct LorenzSystem {
415 pub sigma: f64,
417 pub rho: f64,
419 pub beta: f64,
421}
422
423impl LorenzSystem {
424 pub fn new(sigma: f64, rho: f64, beta: f64) -> Self {
426 Self { sigma, rho, beta }
427 }
428
429 pub fn classic() -> Self {
431 Self::new(10.0, 28.0, 8.0 / 3.0)
432 }
433
434 pub fn fixed_points(&self) -> Vec<[f64; 3]> {
438 let mut fps = vec![[0.0, 0.0, 0.0]];
439 if self.rho > 1.0 {
440 let c = (self.beta * (self.rho - 1.0)).sqrt();
441 fps.push([c, c, self.rho - 1.0]);
442 fps.push([-c, -c, self.rho - 1.0]);
443 }
444 fps
445 }
446}
447
448impl OdeSystem for LorenzSystem {
449 fn dim(&self) -> usize {
450 3
451 }
452 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
453 vec![
454 self.sigma * (x[1] - x[0]),
455 x[0] * (self.rho - x[2]) - x[1],
456 x[0] * x[1] - self.beta * x[2],
457 ]
458 }
459}
460
461#[derive(Debug, Clone)]
475pub struct RosslerSystem {
476 pub a: f64,
478 pub b: f64,
480 pub c: f64,
482}
483
484impl RosslerSystem {
485 pub fn new(a: f64, b: f64, c: f64) -> Self {
487 Self { a, b, c }
488 }
489
490 pub fn classic() -> Self {
492 Self::new(0.2, 0.2, 5.7)
493 }
494}
495
496impl OdeSystem for RosslerSystem {
497 fn dim(&self) -> usize {
498 3
499 }
500 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
501 vec![
502 -x[1] - x[2],
503 x[0] + self.a * x[1],
504 self.b + x[2] * (x[0] - self.c),
505 ]
506 }
507}
508
509#[derive(Debug, Clone)]
518pub struct PoincareSectionConfig {
519 pub axis: usize,
521 pub level: f64,
523 pub positive_direction: bool,
525}
526
527#[derive(Debug, Clone)]
529pub struct PoincareCrossing {
530 pub time: f64,
532 pub state: Vec<f64>,
534}
535
536pub fn poincare_section(
540 sys: &dyn OdeSystem,
541 x0: &[f64],
542 h: f64,
543 n_steps: usize,
544 cfg: &PoincareSectionConfig,
545) -> Vec<PoincareCrossing> {
546 let mut crossings = Vec::new();
547 let mut t = 0.0f64;
548 let mut x_prev = x0.to_vec();
549 let mut x = rk4_step(sys, t, &x_prev, h);
550 for _i in 1..n_steps {
551 let v_prev = x_prev[cfg.axis] - cfg.level;
552 let v_cur = x[cfg.axis] - cfg.level;
553 let crossing = if cfg.positive_direction {
554 v_prev < 0.0 && v_cur >= 0.0
555 } else {
556 v_prev > 0.0 && v_cur <= 0.0
557 };
558 if crossing {
559 let frac = v_prev.abs() / (v_prev.abs() + v_cur.abs() + 1e-30);
561 let t_cross = t + frac * h;
562 let state: Vec<f64> = x_prev
563 .iter()
564 .zip(x.iter())
565 .map(|(a, b)| a + frac * (b - a))
566 .collect();
567 crossings.push(PoincareCrossing {
568 time: t_cross,
569 state,
570 });
571 }
572 x_prev = x.clone();
573 x = rk4_step(sys, t, &x, h);
574 t += h;
575 }
576 crossings
577}
578
579#[derive(Debug, Clone)]
585pub struct LimitCycleEstimate {
586 pub period: f64,
588 pub n_cycles: usize,
590 pub found: bool,
592}
593
594pub fn estimate_limit_cycle_period(
598 sys: &dyn OdeSystem,
599 x0: &[f64],
600 h: f64,
601 n_steps: usize,
602 poincare_axis: usize,
603 level: f64,
604) -> Option<LimitCycleEstimate> {
605 let cfg = PoincareSectionConfig {
606 axis: poincare_axis,
607 level,
608 positive_direction: true,
609 };
610 let crossings = poincare_section(sys, x0, h, n_steps, &cfg);
611 if crossings.len() < 2 {
612 return None;
613 }
614 let n = crossings.len();
615 let total_time = crossings[n - 1].time - crossings[0].time;
616 let period = total_time / (n - 1) as f64;
617 Some(LimitCycleEstimate {
618 period,
619 n_cycles: n - 1,
620 found: true,
621 })
622}
623
624#[derive(Debug, Clone)]
630pub struct RqaParams {
631 pub embed_dim: usize,
633 pub tau: usize,
635 pub epsilon: f64,
637 pub min_line: usize,
639}
640
641#[derive(Debug, Clone)]
643pub struct RqaResult {
644 pub rr: f64,
646 pub det: f64,
648 pub avg_line: f64,
650 pub l_max: usize,
652 pub entropy: f64,
654 pub lam: f64,
656 pub tt: f64,
658}
659
660pub fn delay_embed(series: &[f64], embed_dim: usize, tau: usize) -> Vec<Vec<f64>> {
664 let n = series.len();
665 let max_start = (embed_dim - 1) * tau;
666 if n <= max_start {
667 return Vec::new();
668 }
669 (0..=(n - 1 - max_start))
670 .map(|i| (0..embed_dim).map(|d| series[i + d * tau]).collect())
671 .collect()
672}
673
674pub fn recurrence_matrix(points: &[Vec<f64>], epsilon: f64) -> Vec<bool> {
679 let n = points.len();
680 let mut mat = vec![false; n * n];
681 for i in 0..n {
682 for j in 0..n {
683 let dist: f64 = points[i]
684 .iter()
685 .zip(points[j].iter())
686 .map(|(a, b)| (a - b).powi(2))
687 .sum::<f64>()
688 .sqrt();
689 mat[i * n + j] = dist < epsilon;
690 }
691 }
692 mat
693}
694
695pub fn rqa(series: &[f64], params: &RqaParams) -> RqaResult {
699 let points = delay_embed(series, params.embed_dim, params.tau);
700 let n = points.len();
701 if n == 0 {
702 return RqaResult {
703 rr: 0.0,
704 det: 0.0,
705 avg_line: 0.0,
706 l_max: 0,
707 entropy: 0.0,
708 lam: 0.0,
709 tt: 0.0,
710 };
711 }
712 let mat = recurrence_matrix(&points, params.epsilon);
714
715 let total_off = (n * n - n) as f64;
717 let recurrent_pts: usize = (0..n)
718 .flat_map(|i| (0..n).map(move |j| (i, j)))
719 .filter(|&(i, j)| i != j && mat[i * n + j])
720 .count();
721 let rr = if total_off > 0.0 {
722 recurrent_pts as f64 / total_off
723 } else {
724 0.0
725 };
726
727 let mut diag_lines: Vec<usize> = Vec::new();
729 for diag in (-(n as isize - 1))..(n as isize) {
730 if diag == 0 {
731 continue;
732 }
733 let mut run = 0usize;
734 let i_start = if diag < 0 { (-diag) as usize } else { 0 };
735 let j_start = if diag > 0 { diag as usize } else { 0 };
736 let len = n - i_start.max(j_start);
737 for k in 0..len {
738 let i = i_start + k;
739 let j = j_start + k;
740 if mat[i * n + j] {
741 run += 1;
742 } else {
743 if run >= params.min_line {
744 diag_lines.push(run);
745 }
746 run = 0;
747 }
748 }
749 if run >= params.min_line {
750 diag_lines.push(run);
751 }
752 }
753 let diag_total: usize = diag_lines.iter().sum();
754 let det = if recurrent_pts > 0 {
755 diag_total as f64 / recurrent_pts as f64
756 } else {
757 0.0
758 };
759 let l_max = diag_lines.iter().copied().max().unwrap_or(0);
760 let avg_line = if !diag_lines.is_empty() {
761 diag_total as f64 / diag_lines.len() as f64
762 } else {
763 0.0
764 };
765 let entropy = if !diag_lines.is_empty() {
767 let total = diag_total as f64;
768 let mut ent = 0.0f64;
769 let mut counts: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
770 for &l in &diag_lines {
771 *counts.entry(l).or_insert(0) += 1;
772 }
773 for &cnt in counts.values() {
774 let p = cnt as f64 / total;
775 if p > 0.0 {
776 ent -= p * p.ln();
777 }
778 }
779 ent
780 } else {
781 0.0
782 };
783
784 let mut vert_lines: Vec<usize> = Vec::new();
786 for col in 0..n {
787 let mut run = 0usize;
788 for row in 0..n {
789 if row != col && mat[row * n + col] {
790 run += 1;
791 } else {
792 if run >= params.min_line {
793 vert_lines.push(run);
794 }
795 run = 0;
796 }
797 }
798 if run >= params.min_line {
799 vert_lines.push(run);
800 }
801 }
802 let vert_total: usize = vert_lines.iter().sum();
803 let lam = if recurrent_pts > 0 {
804 vert_total as f64 / recurrent_pts as f64
805 } else {
806 0.0
807 };
808 let tt = if !vert_lines.is_empty() {
809 vert_total as f64 / vert_lines.len() as f64
810 } else {
811 0.0
812 };
813
814 RqaResult {
815 rr,
816 det,
817 avg_line,
818 l_max,
819 entropy,
820 lam,
821 tt,
822 }
823}
824
825pub trait HamiltonianSystem {
834 fn ndof(&self) -> usize;
836
837 fn hamiltonian(&self, q: &[f64], p: &[f64]) -> f64;
839
840 fn dh_dq(&self, q: &[f64], p: &[f64]) -> Vec<f64>;
842
843 fn dh_dp(&self, q: &[f64], p: &[f64]) -> Vec<f64>;
845}
846
847pub struct HamiltonianOde<'a> {
851 pub ham: &'a dyn HamiltonianSystem,
853}
854
855impl<'a> OdeSystem for HamiltonianOde<'a> {
856 fn dim(&self) -> usize {
857 2 * self.ham.ndof()
858 }
859 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
860 let n = self.ham.ndof();
861 let q = &x[..n];
862 let p = &x[n..];
863 let qdot = self.ham.dh_dp(q, p);
864 let dhdq = self.ham.dh_dq(q, p);
865 let pdot: Vec<f64> = dhdq.iter().map(|v| -v).collect();
866 [qdot, pdot].concat()
867 }
868}
869
870pub fn stormer_verlet_step(
880 q: &[f64],
881 p: &[f64],
882 h: f64,
883 grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
884) -> (Vec<f64>, Vec<f64>) {
885 let n = q.len();
886 let f = grad_v(q);
887 let p_half: Vec<f64> = (0..n).map(|i| p[i] - 0.5 * h * f[i]).collect();
889 let q_new: Vec<f64> = (0..n).map(|i| q[i] + h * p_half[i]).collect();
891 let f2 = grad_v(&q_new);
893 let p_new: Vec<f64> = (0..n).map(|i| p_half[i] - 0.5 * h * f2[i]).collect();
895 (q_new, p_new)
896}
897
898pub fn forest_ruth_step(
902 q: &[f64],
903 p: &[f64],
904 h: f64,
905 grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
906) -> (Vec<f64>, Vec<f64>) {
907 let theta: f64 = 1.0 / (2.0 - 2.0f64.powf(1.0 / 3.0));
908 let xi = 1.0 - 2.0 * theta;
909 let n = q.len();
910 let mut qq = q.to_vec();
911 let mut pp = p.to_vec();
912 for &c in &[theta, xi, theta] {
913 let f = grad_v(&qq);
915 for i in 0..n {
916 pp[i] -= 0.5 * c * h * f[i];
917 }
918 for i in 0..n {
919 qq[i] += c * h * pp[i];
920 }
921 let f2 = grad_v(&qq);
922 for i in 0..n {
923 pp[i] -= 0.5 * c * h * f2[i];
924 }
925 }
926 (qq, pp)
927}
928
929pub fn integrate_symplectic(
933 q0: &[f64],
934 p0: &[f64],
935 h: f64,
936 n_steps: usize,
937 grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
938) -> Vec<(Vec<f64>, Vec<f64>)> {
939 let mut traj = Vec::with_capacity(n_steps + 1);
940 let mut q = q0.to_vec();
941 let mut p = p0.to_vec();
942 traj.push((q.clone(), p.clone()));
943 for _ in 0..n_steps {
944 let (qn, pn) = stormer_verlet_step(&q, &p, h, grad_v);
945 q = qn;
946 p = pn;
947 traj.push((q.clone(), p.clone()));
948 }
949 traj
950}
951
952pub fn estimate_winding_number(q_traj: &[(Vec<f64>, Vec<f64>)], _period: f64) -> Option<f64> {
962 if q_traj.len() < 4 {
964 return None;
965 }
966 let mut crossings_0: usize = 0;
967 let mut crossings_1: usize = 0;
968 for w in q_traj.windows(2) {
969 if w[0].0[0] * w[1].0[0] < 0.0 {
970 crossings_0 += 1;
971 }
972 if w[0].0.len() > 1 && w[1].0.len() > 1 && w[0].0[1] * w[1].0[1] < 0.0 {
973 crossings_1 += 1;
974 }
975 }
976 if crossings_1 == 0 {
977 return None;
978 }
979 Some(crossings_0 as f64 / crossings_1 as f64)
980}
981
982pub fn phase_plane_grid(
991 sys: &dyn OdeSystem,
992 x_range: (f64, f64, usize),
993 y_range: (f64, f64, usize),
994) -> Vec<(f64, f64, f64, f64)> {
995 let (x_min, x_max, nx) = x_range;
996 let (y_min, y_max, ny) = y_range;
997 let mut out = Vec::with_capacity(nx * ny);
998 for i in 0..nx {
999 let xv = x_min + (x_max - x_min) * i as f64 / (nx.saturating_sub(1).max(1)) as f64;
1000 for j in 0..ny {
1001 let yv = y_min + (y_max - y_min) * j as f64 / (ny.saturating_sub(1).max(1)) as f64;
1002 let f = sys.rhs(0.0, &[xv, yv]);
1003 out.push((xv, yv, f[0], f[1]));
1004 }
1005 }
1006 out
1007}
1008
1009#[derive(Debug, Clone)]
1022pub struct VanDerPol {
1023 pub mu: f64,
1025}
1026
1027impl VanDerPol {
1028 pub fn new(mu: f64) -> Self {
1030 Self { mu }
1031 }
1032}
1033
1034impl OdeSystem for VanDerPol {
1035 fn dim(&self) -> usize {
1036 2
1037 }
1038 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
1039 vec![x[1], self.mu * (1.0 - x[0] * x[0]) * x[1] - x[0]]
1040 }
1041}
1042
1043#[derive(Debug, Clone)]
1051pub struct DoublePendulum {
1052 pub g: f64,
1054}
1055
1056impl DoublePendulum {
1057 pub fn new(g: f64) -> Self {
1059 Self { g }
1060 }
1061
1062 pub fn integrate(&self, state0: [f64; 4], h: f64, n: usize) -> Vec<[f64; 4]> {
1066 let mut traj = Vec::with_capacity(n + 1);
1067 let mut s = state0;
1068 traj.push(s);
1069 for _ in 0..n {
1070 let rhs = self.rhs_arr(s);
1071 let k1 = rhs;
1072 let s2 = [
1073 s[0] + 0.5 * h * k1[0],
1074 s[1] + 0.5 * h * k1[1],
1075 s[2] + 0.5 * h * k1[2],
1076 s[3] + 0.5 * h * k1[3],
1077 ];
1078 let k2 = self.rhs_arr(s2);
1079 let s3 = [
1080 s[0] + 0.5 * h * k2[0],
1081 s[1] + 0.5 * h * k2[1],
1082 s[2] + 0.5 * h * k2[2],
1083 s[3] + 0.5 * h * k2[3],
1084 ];
1085 let k3 = self.rhs_arr(s3);
1086 let s4 = [
1087 s[0] + h * k3[0],
1088 s[1] + h * k3[1],
1089 s[2] + h * k3[2],
1090 s[3] + h * k3[3],
1091 ];
1092 let k4 = self.rhs_arr(s4);
1093 for i in 0..4 {
1094 s[i] += h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
1095 }
1096 traj.push(s);
1097 }
1098 traj
1099 }
1100
1101 fn rhs_arr(&self, s: [f64; 4]) -> [f64; 4] {
1103 let (t1, t2, p1, p2) = (s[0], s[1], s[2], s[3]);
1104 let dt = t1 - t2;
1105 let denom = 16.0 - 9.0 * dt.cos() * dt.cos();
1106 let t1dot = (6.0 * p1 - 3.0 * dt.cos() * p2) / denom;
1107 let t2dot = (8.0 * p2 - 6.0 * dt.cos() * p1) / denom;
1108 let h = t1dot * t2dot * dt.sin();
1109 let p1dot = -3.0 * self.g * t1.sin() - h;
1110 let p2dot = self.g * t2.sin() + h;
1111 [t1dot, t2dot, p1dot, p2dot]
1112 }
1113
1114 pub fn energy(&self, s: [f64; 4]) -> f64 {
1116 let (t1, t2, p1, p2) = (s[0], s[1], s[2], s[3]);
1117 let dt = t1 - t2;
1118 let denom = 16.0 - 9.0 * dt.cos() * dt.cos();
1119 let t1dot = (6.0 * p1 - 3.0 * dt.cos() * p2) / denom;
1120 let t2dot = (8.0 * p2 - 6.0 * dt.cos() * p1) / denom;
1121 let ke = 0.5 * t1dot * t1dot + 0.5 * t2dot * t2dot + t1dot * t2dot * dt.cos();
1122 let pe = -self.g * (2.0 * t1.cos() + t2.cos());
1123 ke + pe
1124 }
1125}
1126
1127#[derive(Debug, Clone)]
1138pub struct DuffingOscillator {
1139 pub delta: f64,
1141 pub alpha: f64,
1143 pub beta: f64,
1145 pub gamma: f64,
1147 pub omega: f64,
1149}
1150
1151impl DuffingOscillator {
1152 #[allow(clippy::too_many_arguments)]
1154 pub fn new(delta: f64, alpha: f64, beta: f64, gamma: f64, omega: f64) -> Self {
1155 Self {
1156 delta,
1157 alpha,
1158 beta,
1159 gamma,
1160 omega,
1161 }
1162 }
1163}
1164
1165impl OdeSystem for DuffingOscillator {
1166 fn dim(&self) -> usize {
1167 2
1168 }
1169 fn rhs(&self, t: f64, x: &[f64]) -> Vec<f64> {
1170 vec![
1171 x[1],
1172 -self.delta * x[1] - self.alpha * x[0] - self.beta * x[0].powi(3)
1173 + self.gamma * (self.omega * t).cos(),
1174 ]
1175 }
1176}
1177
1178pub fn saddle_node_fixed_points(mu: f64) -> Vec<f64> {
1186 if mu > 0.0 {
1187 Vec::new()
1188 } else if mu == 0.0 {
1189 vec![0.0]
1190 } else {
1191 let sq = (-mu).sqrt();
1192 vec![-sq, sq]
1193 }
1194}
1195
1196pub fn pitchfork_fixed_points(mu: f64) -> Vec<f64> {
1200 if mu <= 0.0 {
1201 vec![0.0]
1202 } else {
1203 let sq = mu.sqrt();
1204 vec![-sq, 0.0, sq]
1205 }
1206}
1207
1208pub fn is_hopf_bifurcation(tr: f64, det: f64, tol: f64) -> bool {
1216 det > tol && tr.abs() < tol
1217}
1218
1219pub fn maximal_lyapunov_exponent(
1235 sys: &dyn OdeSystem,
1236 x0: &[f64],
1237 h: f64,
1238 n_steps: usize,
1239 renorm: usize,
1240) -> f64 {
1241 let n = sys.dim();
1242 let mut x = x0.to_vec();
1243 let mut dv: Vec<f64> = (0..n).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
1245 let mut sum_log = 0.0f64;
1246 let mut t = 0.0f64;
1247 let mut count = 0usize;
1248 for step in 0..n_steps {
1249 let x_new = rk4_step(sys, t, &x, h);
1251 let jac = numerical_jacobian(sys, t, &x, 1e-7);
1253 let jdv: Vec<f64> = (0..n)
1254 .map(|i| (0..n).map(|j| jac[i * n + j] * dv[j]).sum::<f64>())
1255 .collect();
1256 let dv_new: Vec<f64> = (0..n).map(|i| dv[i] + h * jdv[i]).collect();
1257 x = x_new;
1258 dv = dv_new;
1259 t += h;
1260 if (step + 1) % renorm == 0 {
1262 let norm: f64 = dv.iter().map(|v| v * v).sum::<f64>().sqrt();
1263 if norm > 1e-14 {
1264 sum_log += norm.ln();
1265 for vi in &mut dv {
1266 *vi /= norm;
1267 }
1268 count += 1;
1269 }
1270 }
1271 }
1272 if count == 0 {
1273 return 0.0;
1274 }
1275 sum_log / (count * renorm) as f64 / h
1276}
1277
1278pub fn energy_drift(ham: &dyn HamiltonianSystem, traj: &[(Vec<f64>, Vec<f64>)]) -> f64 {
1286 if traj.is_empty() {
1287 return 0.0;
1288 }
1289 let n = ham.ndof();
1290 let (q0, p0) = &traj[0];
1291 let h0 = ham.hamiltonian(q0, p0);
1292 if h0.abs() < 1e-30 {
1293 return 0.0;
1294 }
1295 traj.iter()
1296 .skip(1)
1297 .map(|(q, p)| {
1298 let _ = n;
1299 ((ham.hamiltonian(q, p) - h0) / h0).abs()
1300 })
1301 .fold(0.0f64, f64::max)
1302}
1303
1304#[derive(Debug, Clone)]
1310pub struct HarmonicOscillatorHam {
1311 pub omega: f64,
1313}
1314
1315impl HarmonicOscillatorHam {
1316 pub fn new(omega: f64) -> Self {
1318 Self { omega }
1319 }
1320}
1321
1322impl HamiltonianSystem for HarmonicOscillatorHam {
1323 fn ndof(&self) -> usize {
1324 1
1325 }
1326 fn hamiltonian(&self, q: &[f64], p: &[f64]) -> f64 {
1327 0.5 * p[0] * p[0] + 0.5 * self.omega * self.omega * q[0] * q[0]
1328 }
1329 fn dh_dq(&self, q: &[f64], _p: &[f64]) -> Vec<f64> {
1330 vec![self.omega * self.omega * q[0]]
1331 }
1332 fn dh_dp(&self, _q: &[f64], p: &[f64]) -> Vec<f64> {
1333 vec![p[0]]
1334 }
1335}
1336
1337#[allow(clippy::too_many_arguments)]
1349pub fn detect_heteroclinic(
1350 sys: &dyn OdeSystem,
1351 fp_a: &[f64],
1352 fp_b: &[f64],
1353 unstable_dir: &[f64],
1354 eps_perturb: f64,
1355 h: f64,
1356 n_steps: usize,
1357 tol: f64,
1358) -> Option<f64> {
1359 let n = fp_a.len();
1360 let mut x: Vec<f64> = (0..n)
1361 .map(|i| fp_a[i] + eps_perturb * unstable_dir[i])
1362 .collect();
1363 let mut t = 0.0f64;
1364 for _ in 0..n_steps {
1365 let dist: f64 = (0..n).map(|i| (x[i] - fp_b[i]).powi(2)).sum::<f64>().sqrt();
1366 if dist < tol {
1367 return Some(t);
1368 }
1369 x = rk4_step(sys, t, &x, h);
1370 t += h;
1371 }
1372 None
1373}
1374
1375pub fn kaplan_yorke_dim(exponents: &[f64]) -> f64 {
1385 let n = exponents.len();
1386 let mut sum = 0.0f64;
1387 let mut j = 0usize;
1388 for (i, &le) in exponents.iter().enumerate() {
1389 sum += le;
1390 if sum < 0.0 {
1391 j = i;
1392 sum -= le;
1393 break;
1394 }
1395 if i == n - 1 {
1396 return n as f64;
1397 }
1398 j = i + 1;
1399 }
1400 if j == 0 && exponents[0] < 0.0 {
1401 return 0.0;
1402 }
1403 let denom = exponents[j].abs();
1404 if denom < 1e-14 {
1405 return j as f64;
1406 }
1407 j as f64 + sum / denom
1408}
1409
1410#[derive(Debug, Clone)]
1420pub struct KuramotoModel {
1421 pub omegas: Vec<f64>,
1423 pub k: f64,
1425}
1426
1427impl KuramotoModel {
1428 pub fn new(omegas: Vec<f64>, k: f64) -> Self {
1430 Self { omegas, k }
1431 }
1432
1433 pub fn order_parameter(theta: &[f64]) -> (f64, f64) {
1437 let n = theta.len() as f64;
1438 let re: f64 = theta.iter().map(|&t| t.cos()).sum::<f64>() / n;
1439 let im: f64 = theta.iter().map(|&t| t.sin()).sum::<f64>() / n;
1440 let r = (re * re + im * im).sqrt();
1441 let psi = im.atan2(re);
1442 (r, psi)
1443 }
1444}
1445
1446impl OdeSystem for KuramotoModel {
1447 fn dim(&self) -> usize {
1448 self.omegas.len()
1449 }
1450 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
1451 let n = x.len();
1452 let k_over_n = self.k / n as f64;
1453 (0..n)
1454 .map(|i| {
1455 let coupling: f64 = (0..n).map(|j| (x[j] - x[i]).sin()).sum();
1456 self.omegas[i] + k_over_n * coupling
1457 })
1458 .collect()
1459 }
1460}
1461
1462#[derive(Debug, Clone)]
1475pub struct Brusselator {
1476 pub a: f64,
1478 pub b: f64,
1480}
1481
1482impl Brusselator {
1483 pub fn new(a: f64, b: f64) -> Self {
1485 Self { a, b }
1486 }
1487
1488 pub fn fixed_point(&self) -> (f64, f64) {
1490 (self.a, self.b / self.a)
1491 }
1492
1493 pub fn hopf_b(&self) -> f64 {
1495 1.0 + self.a * self.a
1496 }
1497}
1498
1499impl OdeSystem for Brusselator {
1500 fn dim(&self) -> usize {
1501 2
1502 }
1503 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
1504 vec![
1505 self.a - (self.b + 1.0) * x[0] + x[0] * x[0] * x[1],
1506 self.b * x[0] - x[0] * x[0] * x[1],
1507 ]
1508 }
1509}
1510
1511#[cfg(test)]
1516mod tests {
1517 use super::*;
1518
1519 struct ShoOde {
1521 omega: f64,
1522 }
1523 impl OdeSystem for ShoOde {
1524 fn dim(&self) -> usize {
1525 2
1526 }
1527 fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
1528 vec![x[1], -self.omega * self.omega * x[0]]
1529 }
1530 }
1531
1532 #[test]
1534 fn test_rk4_sho_energy_conservation() {
1535 let sys = ShoOde { omega: 1.0 };
1536 let x0 = vec![1.0, 0.0];
1537 let energy0 = 0.5 * (x0[0] * x0[0] + x0[1] * x0[1]);
1538 let h = 0.01;
1539 let mut x = x0.clone();
1540 for i in 0..628 {
1541 x = rk4_step(&sys, i as f64 * h, &x, h);
1542 }
1543 let energy1 = 0.5 * (x[0] * x[0] + x[1] * x[1]);
1544 assert!((energy1 - energy0).abs() < 1e-4);
1545 }
1546
1547 #[test]
1549 fn test_integrate_rk4_step_count() {
1550 let sys = ShoOde { omega: 1.0 };
1551 let traj = integrate_rk4(&sys, &[1.0, 0.0], 0.0, 1.0, 0.1);
1552 assert_eq!(traj.len(), 11);
1553 }
1554
1555 #[test]
1557 fn test_numerical_jacobian_sho() {
1558 let sys = ShoOde { omega: 2.0 };
1559 let jac = numerical_jacobian(&sys, 0.0, &[0.0, 0.0], 1e-5);
1560 assert!((jac[0] - 0.0).abs() < 1e-6);
1562 assert!((jac[1] - 1.0).abs() < 1e-6);
1563 assert!((jac[2] - (-4.0)).abs() < 1e-6);
1564 assert!((jac[3] - 0.0).abs() < 1e-6);
1565 }
1566
1567 #[test]
1569 fn test_find_fixed_point_sho_origin() {
1570 let sys = ShoOde { omega: 1.0 };
1571 let res = find_fixed_point(&sys, &[0.1, 0.1], 1e-10, 50, 1e-7);
1572 assert!(res.converged);
1573 assert!(res.point[0].abs() < 1e-6);
1574 assert!(res.point[1].abs() < 1e-6);
1575 }
1576
1577 #[test]
1579 fn test_gauss_solve_2x2() {
1580 let a = vec![2.0, 1.0, 1.0, 3.0];
1582 let b = vec![5.0, 10.0];
1583 let x = gauss_solve(&a, 2, &b).unwrap();
1584 assert!((x[0] - 1.0).abs() < 1e-10);
1585 assert!((x[1] - 3.0).abs() < 1e-10);
1586 }
1587
1588 #[test]
1590 fn test_eigen2_real() {
1591 let ((r1, i1), (r2, i2)) = eigen2(3.0, 0.0, 0.0, 2.0);
1593 assert!((r1 - 3.0).abs() < 1e-10 || (r1 - 2.0).abs() < 1e-10);
1594 assert!(i1.abs() < 1e-10);
1595 assert!(i2.abs() < 1e-10);
1596 let _ = r2;
1597 }
1598
1599 #[test]
1601 fn test_eigen2_complex() {
1602 let ((r1, i1), (_r2, _i2)) = eigen2(0.0, 1.0, -1.0, 0.0);
1604 assert!(r1.abs() < 1e-10);
1605 assert!((i1.abs() - 1.0).abs() < 1e-10);
1606 }
1607
1608 #[test]
1610 fn test_stability_saddle() {
1611 let s = stability_2d(1.0, 0.0, 0.0, -1.0);
1613 assert_eq!(s, "saddle");
1614 }
1615
1616 #[test]
1618 fn test_stability_stable_spiral() {
1619 let s = stability_2d(-0.1, 1.0, -1.0, -0.1);
1621 assert_eq!(s, "stable_spiral");
1622 }
1623
1624 #[test]
1626 fn test_lorenz_fixed_points_classic() {
1627 let lor = LorenzSystem::classic();
1628 let fps = lor.fixed_points();
1629 assert_eq!(fps.len(), 3);
1630 let c1 = fps[1];
1632 assert!((c1[0] - c1[1]).abs() < 1e-10);
1633 }
1634
1635 #[test]
1637 fn test_lorenz_rhs_origin() {
1638 let lor = LorenzSystem::classic();
1639 let f = lor.rhs(0.0, &[0.0, 0.0, 0.0]);
1640 assert!(f.iter().all(|v| v.abs() < 1e-12));
1641 }
1642
1643 #[test]
1645 fn test_rossler_dim() {
1646 let ros = RosslerSystem::classic();
1647 assert_eq!(ros.dim(), 3);
1648 }
1649
1650 #[test]
1652 fn test_poincare_sho_crossings() {
1653 let sys = ShoOde { omega: 1.0 };
1654 let cfg = PoincareSectionConfig {
1655 axis: 0,
1656 level: 0.0,
1657 positive_direction: true,
1658 };
1659 let h = 0.01;
1661 let n_steps = (10.0 * 2.0 * std::f64::consts::PI / h) as usize;
1662 let crossings = poincare_section(&sys, &[1.0, 0.0], h, n_steps, &cfg);
1663 assert!(
1665 crossings.len() >= 8 && crossings.len() <= 12,
1666 "expected ~10 crossings, got {}",
1667 crossings.len()
1668 );
1669 }
1670
1671 #[test]
1673 fn test_limit_cycle_period_sho() {
1674 let sys = ShoOde { omega: 1.0 };
1675 let h = 0.005;
1676 let n_steps = (20.0 * 2.0 * std::f64::consts::PI / h) as usize;
1677 let est = estimate_limit_cycle_period(&sys, &[1.0, 0.0], h, n_steps, 0, 0.0);
1678 let est = est.unwrap();
1679 let expected = 2.0 * std::f64::consts::PI;
1680 assert!(
1681 (est.period - expected).abs() < 0.05,
1682 "period={} expected≈{}",
1683 est.period,
1684 expected
1685 );
1686 }
1687
1688 #[test]
1690 fn test_delay_embed_shape() {
1691 let series: Vec<f64> = (0..20).map(|i| i as f64).collect();
1692 let pts = delay_embed(&series, 3, 2);
1693 assert_eq!(pts.len(), 16);
1695 assert_eq!(pts[0].len(), 3);
1696 assert_eq!(pts[0], vec![0.0, 2.0, 4.0]);
1698 }
1699
1700 #[test]
1702 fn test_recurrence_matrix_diagonal() {
1703 let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![2.0, 0.0]];
1704 let mat = recurrence_matrix(&pts, 0.5);
1705 assert!(mat[0]);
1707 assert!(mat[3 + 1]);
1708 assert!(mat[2 * 3 + 2]);
1709 }
1710
1711 #[test]
1713 fn test_rqa_periodic() {
1714 let series: Vec<f64> = (0..200)
1715 .map(|i| (2.0 * std::f64::consts::PI * i as f64 / 20.0).sin())
1716 .collect();
1717 let params = RqaParams {
1718 embed_dim: 2,
1719 tau: 5,
1720 epsilon: 0.2,
1721 min_line: 2,
1722 };
1723 let res = rqa(&series, ¶ms);
1724 assert!(res.rr > 0.0);
1725 assert!(res.det >= 0.0);
1726 }
1727
1728 #[test]
1730 fn test_stormer_verlet_energy_sho() {
1731 let omega = 1.0f64;
1732 let grad_v = |q: &[f64]| vec![omega * omega * q[0]];
1733 let traj = integrate_symplectic(&[1.0], &[0.0], 0.01, 1000, &grad_v);
1734 let energy0 = 0.5 * 0.0_f64.powi(2) + 0.5 * omega * omega * 1.0_f64.powi(2);
1735 let (q_end, p_end) = &traj[traj.len() - 1];
1736 let energy_end = 0.5 * p_end[0].powi(2) + 0.5 * omega * omega * q_end[0].powi(2);
1737 assert!(
1738 (energy_end - energy0).abs() < 1e-4,
1739 "energy drift = {}",
1740 (energy_end - energy0).abs()
1741 );
1742 }
1743
1744 #[test]
1746 fn test_forest_ruth_returns_same_dim() {
1747 let grad_v = |q: &[f64]| vec![q[0]];
1748 let (q_new, p_new) = forest_ruth_step(&[1.0], &[0.5], 0.1, &grad_v);
1749 assert_eq!(q_new.len(), 1);
1750 assert_eq!(p_new.len(), 1);
1751 }
1752
1753 #[test]
1755 fn test_hamiltonian_sho_energy_drift() {
1756 let ham = HarmonicOscillatorHam::new(1.0);
1757 let grad_v = |q: &[f64]| ham.dh_dq(q, &[0.0]);
1758 let traj = integrate_symplectic(&[1.0], &[0.0], 0.01, 500, &grad_v);
1759 let pairs: Vec<(Vec<f64>, Vec<f64>)> = traj;
1760 let drift = energy_drift(&ham, &pairs);
1761 assert!(drift < 1e-3, "energy drift = {}", drift);
1762 }
1763
1764 #[test]
1766 fn test_saddle_node_fps() {
1767 let fps_neg = saddle_node_fixed_points(-4.0);
1768 assert_eq!(fps_neg.len(), 2);
1769 assert!((fps_neg[0].abs() - 2.0).abs() < 1e-10);
1771 let fps_pos = saddle_node_fixed_points(1.0);
1772 assert!(fps_pos.is_empty());
1773 let fps_zero = saddle_node_fixed_points(0.0);
1774 assert_eq!(fps_zero.len(), 1);
1775 }
1776
1777 #[test]
1779 fn test_pitchfork_fps() {
1780 let fps = pitchfork_fixed_points(4.0);
1781 assert_eq!(fps.len(), 3);
1782 let fps_neg = pitchfork_fixed_points(-1.0);
1783 assert_eq!(fps_neg.len(), 1);
1784 }
1785
1786 #[test]
1788 fn test_is_hopf() {
1789 assert!(is_hopf_bifurcation(0.0, 1.0, 1e-6));
1790 assert!(!is_hopf_bifurcation(0.5, 1.0, 1e-6));
1791 assert!(!is_hopf_bifurcation(0.0, -1.0, 1e-6));
1792 }
1793
1794 #[test]
1796 fn test_brusselator_fixed_point() {
1797 let br = Brusselator::new(2.0, 3.0);
1798 let (xfp, yfp) = br.fixed_point();
1799 let f = br.rhs(0.0, &[xfp, yfp]);
1800 assert!(f[0].abs() < 1e-10);
1801 assert!(f[1].abs() < 1e-10);
1802 }
1803
1804 #[test]
1806 fn test_brusselator_hopf_b() {
1807 let br = Brusselator::new(2.0, 5.0);
1808 let hopf = br.hopf_b(); assert!((hopf - 5.0).abs() < 1e-10);
1810 }
1811
1812 #[test]
1814 fn test_vdp_origin_unstable() {
1815 let vdp = VanDerPol::new(1.0);
1816 let jac = numerical_jacobian(&vdp, 0.0, &[0.0, 0.0], 1e-5);
1817 let tr = jac[0] + jac[3];
1819 assert!(tr > 0.0);
1820 }
1821
1822 #[test]
1824 fn test_kuramoto_fully_synced() {
1825 let theta = vec![1.0; 5];
1827 let (r, _psi) = KuramotoModel::order_parameter(&theta);
1828 assert!((r - 1.0).abs() < 1e-10);
1829 }
1830
1831 #[test]
1833 fn test_kuramoto_incoherent() {
1834 use std::f64::consts::PI;
1835 let n = 8;
1837 let theta: Vec<f64> = (0..n).map(|i| 2.0 * PI * i as f64 / n as f64).collect();
1838 let (r, _) = KuramotoModel::order_parameter(&theta);
1839 assert!(r < 0.05);
1840 }
1841
1842 #[test]
1844 fn test_kaplan_yorke_dim() {
1845 let les = vec![0.9, 0.0, -14.6];
1847 let d = kaplan_yorke_dim(&les);
1848 assert!(d > 2.0 && d < 2.2, "KY dim = {}", d);
1850 }
1851
1852 #[test]
1854 fn test_mle_lorenz_positive() {
1855 let lor = LorenzSystem::classic();
1856 let x0 = vec![0.1, 0.0, 0.0];
1857 let mle = maximal_lyapunov_exponent(&lor, &x0, 0.01, 1000, 10);
1859 assert!(mle > -1.0, "MLE = {}", mle);
1861 }
1862
1863 #[test]
1865 fn test_double_pendulum_energy() {
1866 let dp = DoublePendulum::new(9.81);
1867 let s0 = [0.1, 0.05, 0.0, 0.0];
1868 let e0 = dp.energy(s0);
1869 let traj = dp.integrate(s0, 0.001, 100);
1871 let e_end = dp.energy(*traj.last().unwrap());
1872 assert!(
1873 (e_end - e0).abs() < 0.02,
1874 "energy drift = {}",
1875 (e_end - e0).abs()
1876 );
1877 }
1878
1879 #[test]
1881 fn test_duffing_dim() {
1882 let duf = DuffingOscillator::new(0.1, -1.0, 1.0, 0.3, 1.2);
1883 assert_eq!(duf.dim(), 2);
1884 let f = duf.rhs(0.0, &[0.0, 0.0]);
1885 assert_eq!(f.len(), 2);
1886 }
1887
1888 #[test]
1890 fn test_phase_plane_grid_count() {
1891 let sys = ShoOde { omega: 1.0 };
1892 let grid = phase_plane_grid(&sys, (-2.0, 2.0, 5), (-2.0, 2.0, 5));
1893 assert_eq!(grid.len(), 25);
1894 }
1895
1896 #[test]
1898 fn test_vec3_ops() {
1899 let a = [1.0, 2.0, 3.0];
1900 let b = [4.0, 5.0, 6.0];
1901 let s = add3(a, b);
1902 assert_eq!(s, [5.0, 7.0, 9.0]);
1903 let d = sub3(b, a);
1904 assert_eq!(d, [3.0, 3.0, 3.0]);
1905 let sc = scale3(a, 2.0);
1906 assert_eq!(sc, [2.0, 4.0, 6.0]);
1907 assert!((dot3(a, b) - 32.0).abs() < 1e-10);
1908 assert!((norm3([3.0, 4.0, 0.0]) - 5.0).abs() < 1e-10);
1909 }
1910
1911 #[test]
1913 fn test_cross3() {
1914 let a = [1.0, 0.0, 0.0];
1915 let b = [0.0, 1.0, 0.0];
1916 let c = cross3(a, b);
1917 assert!((c[0] - 0.0).abs() < 1e-10);
1918 assert!((c[1] - 0.0).abs() < 1e-10);
1919 assert!((c[2] - 1.0).abs() < 1e-10);
1920 }
1921
1922 #[test]
1924 fn test_rossler_rhs_values() {
1925 let ros = RosslerSystem::new(0.2, 0.2, 5.7);
1926 let f = ros.rhs(0.0, &[1.0, 0.0, 0.0]);
1927 assert!((f[0] - 0.0).abs() < 1e-10);
1929 assert!((f[1] - 1.0).abs() < 1e-10);
1930 assert!((f[2] - 0.2).abs() < 1e-10);
1931 }
1932
1933 #[test]
1935 fn test_ham_sho_value() {
1936 let ham = HarmonicOscillatorHam::new(2.0);
1937 let h = ham.hamiltonian(&[1.0], &[0.0]);
1938 assert!((h - 2.0).abs() < 1e-10);
1940 }
1941
1942 #[test]
1944 fn test_hamiltonian_ode_rhs() {
1945 let ham = HarmonicOscillatorHam::new(1.0);
1946 let ode = HamiltonianOde { ham: &ham };
1947 let f = ode.rhs(0.0, &[0.0, 1.0]);
1949 assert!((f[0] - 1.0).abs() < 1e-10);
1950 assert!((f[1] - 0.0).abs() < 1e-10);
1951 }
1952
1953 #[test]
1955 fn test_lyapunov_qr_sho_near_zero() {
1956 let sys = ShoOde { omega: 1.0 };
1957 let les = lyapunov_exponents_qr(&sys, &[1.0, 0.0], 0.1, 200);
1958 assert_eq!(les.len(), 2);
1960 let sum: f64 = les.iter().sum();
1961 assert!(sum.abs() < 1.0, "sum of LEs = {}", sum);
1963 }
1964
1965 #[test]
1967 fn test_detect_heteroclinic_none() {
1968 let sys = ShoOde { omega: 1.0 };
1969 let result = detect_heteroclinic(
1971 &sys,
1972 &[0.0, 0.0],
1973 &[5.0, 0.0],
1974 &[1.0, 0.0],
1975 0.01,
1976 0.01,
1977 100,
1978 0.1,
1979 );
1980 let _ = result;
1983 }
1984}