1use ndarray::{Array1, Array2};
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22
23#[derive(Error, Debug)]
24pub enum AutoError {
25 #[error("Convergence failed after {0} iterations")]
26 ConvergenceFailed(usize),
27 #[error("Singular Jacobian at parameter {0}")]
28 SingularJacobian(f64),
29 #[error("Step size too small: {0}")]
30 StepTooSmall(f64),
31 #[error("Maximum steps reached: {0}")]
32 MaxStepsReached(usize),
33 #[error("Invalid parameter: {0}")]
34 InvalidParameter(String),
35 #[error("Linear algebra error: {0}")]
36 LinearAlgebraError(String),
37}
38
39pub type Result<T> = std::result::Result<T, AutoError>;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContinuationParams {
48 pub parameter: String,
50 pub par_start: f64,
52 pub par_end: f64,
54 pub ds: f64,
56 pub ds_min: f64,
58 pub ds_max: f64,
60 pub max_steps: usize,
62 pub newton_tol: f64,
64 pub newton_max_iter: usize,
66 pub ntst: usize,
68 pub ncol: usize,
70 pub adapt_threshold: f64,
72 pub output_every: usize,
74 pub detect_bifurcations: bool,
76 pub branch_switch_tol: f64,
78}
79
80impl Default for ContinuationParams {
81 fn default() -> Self {
82 Self {
83 parameter: "lambda".into(),
84 par_start: 0.0,
85 par_end: 1.0,
86 ds: 0.01,
87 ds_min: 1e-6,
88 ds_max: 0.1,
89 max_steps: 1000,
90 newton_tol: 1e-8,
91 newton_max_iter: 20,
92 ntst: 20,
93 ncol: 4,
94 adapt_threshold: 0.5,
95 output_every: 1,
96 detect_bifurcations: true,
97 branch_switch_tol: 1e-4,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
108pub enum BifurcationType {
109 Regular,
111 SaddleNode,
113 Transcritical,
115 Pitchfork,
117 Hopf,
119 PeriodDoubling,
121 Torus,
123 BranchPoint,
125 LimitPointCycle,
127 UserZero,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BifurcationPoint {
134 pub bif_type: BifurcationType,
135 pub parameter: f64,
136 pub state: Array1<f64>,
137 pub critical_eigenvalues: Vec<(f64, f64)>, pub tangent: Option<Array1<f64>>,
141 pub period: Option<f64>,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct SolutionPoint {
152 pub parameter: f64,
154 pub state: Array1<f64>,
156 pub stable: bool,
158 pub eigenvalues: Vec<(f64, f64)>, pub period: Option<f64>,
162 pub floquet_multipliers: Option<Vec<(f64, f64)>>,
164 pub bifurcation: Option<BifurcationType>,
166 pub arclength: f64,
168 pub residual_norm: f64,
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ContinuationBranch {
179 pub name: String,
180 pub points: Vec<SolutionPoint>,
181 pub bifurcations: Vec<BifurcationPoint>,
182 pub is_periodic: bool,
184 pub stats: ComputationStats,
186}
187
188impl ContinuationBranch {
189 pub fn new(name: &str) -> Self {
190 Self {
191 name: name.to_string(),
192 points: vec![],
193 bifurcations: vec![],
194 is_periodic: false,
195 stats: ComputationStats::default(),
196 }
197 }
198
199 pub fn parameter_range(&self) -> (f64, f64) {
201 if self.points.is_empty() {
202 return (0.0, 0.0);
203 }
204 let pars: Vec<f64> = self.points.iter().map(|p| p.parameter).collect();
205 let min = pars.iter().cloned().fold(f64::INFINITY, f64::min);
206 let max = pars.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
207 (min, max)
208 }
209
210 pub fn stable_segments(&self) -> Vec<(usize, usize)> {
212 let mut segments = vec![];
213 let mut start = None;
214
215 for (i, pt) in self.points.iter().enumerate() {
216 if pt.stable {
217 if start.is_none() {
218 start = Some(i);
219 }
220 } else if let Some(s) = start {
221 segments.push((s, i - 1));
222 start = None;
223 }
224 }
225
226 if let Some(s) = start {
227 segments.push((s, self.points.len() - 1));
228 }
229
230 segments
231 }
232}
233
234#[derive(Debug, Clone, Default, Serialize, Deserialize)]
236pub struct ComputationStats {
237 pub total_steps: usize,
238 pub newton_iterations: usize,
239 pub jacobian_evaluations: usize,
240 pub step_size_reductions: usize,
241 pub bifurcations_detected: usize,
242 pub branch_switches: usize,
243 pub cpu_time_seconds: f64,
244}
245
246pub trait OdeSystem {
252 fn dim(&self) -> usize;
254
255 fn rhs(&self, x: &Array1<f64>, par: f64) -> Array1<f64>;
257
258 fn jacobian(&self, _x: &Array1<f64>, _par: f64) -> Option<Array2<f64>> {
260 None
261 }
262
263 fn par_derivative(&self, _x: &Array1<f64>, _par: f64) -> Option<Array1<f64>> {
265 None
266 }
267}
268
269pub fn newton_solve<F, J>(
275 f: F,
276 jacobian: J,
277 mut x: Array1<f64>,
278 tol: f64,
279 max_iter: usize,
280) -> Result<(Array1<f64>, usize)>
281where
282 F: Fn(&Array1<f64>) -> Array1<f64>,
283 J: Fn(&Array1<f64>) -> Array2<f64>,
284{
285 for iter in 0..max_iter {
286 let fx = f(&x);
287 let norm = fx.iter().map(|&v| v * v).sum::<f64>().sqrt();
288
289 if norm < tol {
290 return Ok((x, iter + 1));
291 }
292
293 let jac = jacobian(&x);
294 let dx = solve_linear_system(&jac, &fx)?;
295 x = x - dx;
296 }
297
298 Err(AutoError::ConvergenceFailed(max_iter))
299}
300
301fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>> {
303 let n = b.len();
304 if a.nrows() != n || a.ncols() != n {
305 return Err(AutoError::LinearAlgebraError(
306 "Matrix dimension mismatch".into()
307 ));
308 }
309
310 let mut aug = Array2::zeros((n, n + 1));
312 for i in 0..n {
313 for j in 0..n {
314 aug[[i, j]] = a[[i, j]];
315 }
316 aug[[i, n]] = b[i];
317 }
318
319 for k in 0..n {
321 let mut max_row = k;
323 let mut max_val = aug[[k, k]].abs();
324 for i in (k + 1)..n {
325 if aug[[i, k]].abs() > max_val {
326 max_val = aug[[i, k]].abs();
327 max_row = i;
328 }
329 }
330
331 if max_val < 1e-15 {
332 return Err(AutoError::SingularJacobian(0.0));
333 }
334
335 if max_row != k {
337 for j in 0..=n {
338 let tmp = aug[[k, j]];
339 aug[[k, j]] = aug[[max_row, j]];
340 aug[[max_row, j]] = tmp;
341 }
342 }
343
344 for i in (k + 1)..n {
346 let factor = aug[[i, k]] / aug[[k, k]];
347 for j in k..=n {
348 aug[[i, j]] -= factor * aug[[k, j]];
349 }
350 }
351 }
352
353 let mut x = Array1::zeros(n);
355 for i in (0..n).rev() {
356 let mut sum = aug[[i, n]];
357 for j in (i + 1)..n {
358 sum -= aug[[i, j]] * x[j];
359 }
360 x[i] = sum / aug[[i, i]];
361 }
362
363 Ok(x)
364}
365
366pub fn compute_eigenvalues(a: &Array2<f64>) -> Vec<(f64, f64)> {
372 let n = a.nrows();
373 if n == 0 {
374 return vec![];
375 }
376
377 if n == 1 {
379 return vec![(a[[0, 0]], 0.0)];
380 }
381
382 if n == 2 {
383 return eigenvalues_2x2(a);
384 }
385
386 qr_eigenvalues(a)
388}
389
390fn eigenvalues_2x2(a: &Array2<f64>) -> Vec<(f64, f64)> {
392 let trace = a[[0, 0]] + a[[1, 1]];
393 let det = a[[0, 0]] * a[[1, 1]] - a[[0, 1]] * a[[1, 0]];
394
395 let discriminant = trace * trace - 4.0 * det;
396
397 if discriminant >= 0.0 {
398 let sqrt_d = discriminant.sqrt();
399 vec![
400 ((trace + sqrt_d) / 2.0, 0.0),
401 ((trace - sqrt_d) / 2.0, 0.0),
402 ]
403 } else {
404 let real = trace / 2.0;
405 let imag = (-discriminant).sqrt() / 2.0;
406 vec![
407 (real, imag),
408 (real, -imag),
409 ]
410 }
411}
412
413fn qr_eigenvalues(a: &Array2<f64>) -> Vec<(f64, f64)> {
415 let n = a.nrows();
416 let mut h = a.clone();
417 let max_iter = 100 * n;
418
419 for iter in 0..max_iter {
421 let shift = h[[n - 1, n - 1]];
423
424 for i in 0..n {
425 h[[i, i]] -= shift;
426 }
427
428 let (q, r) = qr_decomposition(&h);
430
431 h = r.dot(&q);
433
434 for i in 0..n {
435 h[[i, i]] += shift;
436 }
437
438 let mut converged = true;
440 for i in 1..n {
441 if h[[i, i - 1]].abs() > 1e-10 {
442 converged = false;
443 break;
444 }
445 }
446
447 if converged || iter == max_iter - 1 {
448 break;
449 }
450 }
451
452 let mut eigenvalues = vec![];
454 let mut i = 0;
455 while i < n {
456 if i == n - 1 || h[[i + 1, i]].abs() < 1e-10 {
457 eigenvalues.push((h[[i, i]], 0.0));
459 i += 1;
460 } else {
461 let block = Array2::from_shape_vec((2, 2), vec![
463 h[[i, i]], h[[i, i + 1]],
464 h[[i + 1, i]], h[[i + 1, i + 1]],
465 ]).unwrap();
466
467 let eigs = eigenvalues_2x2(&block);
468 eigenvalues.extend(eigs);
469 i += 2;
470 }
471 }
472
473 eigenvalues
474}
475
476fn qr_decomposition(a: &Array2<f64>) -> (Array2<f64>, Array2<f64>) {
478 let n = a.nrows();
479 let mut q = Array2::zeros((n, n));
480 let mut r = Array2::zeros((n, n));
481
482 for j in 0..n {
483 let mut v = a.column(j).to_owned();
484
485 for i in 0..j {
486 r[[i, j]] = q.column(i).dot(&v);
487 for k in 0..n {
488 v[k] -= r[[i, j]] * q[[k, i]];
489 }
490 }
491
492 r[[j, j]] = v.iter().map(|&x| x * x).sum::<f64>().sqrt();
493
494 if r[[j, j]].abs() > 1e-15 {
495 for k in 0..n {
496 q[[k, j]] = v[k] / r[[j, j]];
497 }
498 }
499 }
500
501 (q, r)
502}
503
504pub fn natural_continuation<S: OdeSystem>(
511 system: &S,
512 initial_state: Array1<f64>,
513 params: &ContinuationParams,
514) -> Result<ContinuationBranch> {
515 let mut branch = ContinuationBranch::new("natural");
516 let mut state = initial_state;
517 let mut par = params.par_start;
518 let direction = if params.par_end > params.par_start { 1.0 } else { -1.0 };
519
520 let mut arclength = 0.0;
521
522 for step in 0..params.max_steps {
523 let f = |x: &Array1<f64>| system.rhs(x, par);
525
526 let jac = |x: &Array1<f64>| {
527 system.jacobian(x, par).unwrap_or_else(|| numerical_jacobian(system, x, par))
528 };
529
530 let (new_state, newton_iters) = newton_solve(f, jac, state.clone(), params.newton_tol, params.newton_max_iter)?;
531
532 branch.stats.newton_iterations += newton_iters;
533 branch.stats.jacobian_evaluations += newton_iters;
534
535 let jac_matrix = system.jacobian(&new_state, par)
537 .unwrap_or_else(|| numerical_jacobian(system, &new_state, par));
538 let eigenvalues = compute_eigenvalues(&jac_matrix);
539 let stable = eigenvalues.iter().all(|&(re, _)| re < 0.0);
540
541 let bifurcation = if params.detect_bifurcations && step > 0 {
543 detect_bifurcation(&branch.points.last().unwrap().eigenvalues, &eigenvalues)
544 } else {
545 None
546 };
547
548 if let Some(bif_type) = bifurcation {
549 branch.bifurcations.push(BifurcationPoint {
550 bif_type,
551 parameter: par,
552 state: new_state.clone(),
553 critical_eigenvalues: find_critical_eigenvalues(&eigenvalues),
554 tangent: None,
555 period: None,
556 });
557 branch.stats.bifurcations_detected += 1;
558 }
559
560 let residual = system.rhs(&new_state, par);
562 let residual_norm = residual.iter().map(|&x| x * x).sum::<f64>().sqrt();
563
564 branch.points.push(SolutionPoint {
565 parameter: par,
566 state: new_state.clone(),
567 stable,
568 eigenvalues,
569 period: None,
570 floquet_multipliers: None,
571 bifurcation,
572 arclength,
573 residual_norm,
574 });
575
576 state = new_state;
577 arclength += params.ds;
578
579 par += direction * params.ds;
581
582 if (direction > 0.0 && par > params.par_end) ||
584 (direction < 0.0 && par < params.par_end) {
585 break;
586 }
587
588 branch.stats.total_steps = step + 1;
589 }
590
591 Ok(branch)
592}
593
594fn numerical_jacobian<S: OdeSystem>(system: &S, x: &Array1<f64>, par: f64) -> Array2<f64> {
596 let n = x.len();
597 let eps = 1e-8;
598 let f0 = system.rhs(x, par);
599
600 let mut jac = Array2::zeros((n, n));
601
602 for j in 0..n {
603 let mut x_plus = x.clone();
604 x_plus[j] += eps;
605 let f_plus = system.rhs(&x_plus, par);
606
607 for i in 0..n {
608 jac[[i, j]] = (f_plus[i] - f0[i]) / eps;
609 }
610 }
611
612 jac
613}
614
615pub fn arclength_continuation<S: OdeSystem>(
622 system: &S,
623 initial_state: Array1<f64>,
624 params: &ContinuationParams,
625) -> Result<ContinuationBranch> {
626 let n = system.dim();
627 let mut branch = ContinuationBranch::new("arclength");
628
629 let mut x = initial_state.clone();
631 let mut par = params.par_start;
632 let mut ds = params.ds;
633
634 let mut tangent = compute_initial_tangent(system, &x, par, n, params.par_end > params.par_start);
636 let mut arclength = 0.0;
637
638 {
640 let jac = system.jacobian(&x, par)
641 .unwrap_or_else(|| numerical_jacobian(system, &x, par));
642 let eigenvalues = compute_eigenvalues(&jac);
643 let stable = eigenvalues.iter().all(|&(re, _)| re < 0.0);
644
645 branch.points.push(SolutionPoint {
646 parameter: par,
647 state: x.clone(),
648 stable,
649 eigenvalues,
650 period: None,
651 floquet_multipliers: None,
652 bifurcation: None,
653 arclength: 0.0,
654 residual_norm: 0.0,
655 });
656 }
657
658 for step in 0..params.max_steps {
659 let mut x_pred = x.clone();
661 for i in 0..n {
662 x_pred[i] += ds * tangent[i];
663 }
664 let par_pred = par + ds * tangent[n];
665
666 let result = newton_arclength(
668 system,
669 x_pred,
670 par_pred,
671 &x,
672 par,
673 &tangent,
674 ds,
675 params.newton_tol,
676 params.newton_max_iter,
677 );
678
679 match result {
680 Ok((new_x, new_par, iters)) => {
681 branch.stats.newton_iterations += iters;
682 branch.stats.jacobian_evaluations += iters;
683
684 arclength += ds;
685
686 let new_tangent = compute_tangent(system, &new_x, new_par, &tangent, n);
688
689 let jac = system.jacobian(&new_x, new_par)
691 .unwrap_or_else(|| numerical_jacobian(system, &new_x, new_par));
692 let eigenvalues = compute_eigenvalues(&jac);
693 let stable = eigenvalues.iter().all(|&(re, _)| re < 0.0);
694
695 let bifurcation = if params.detect_bifurcations && !branch.points.is_empty() {
697 detect_bifurcation(&branch.points.last().unwrap().eigenvalues, &eigenvalues)
698 } else {
699 None
700 };
701
702 if let Some(bif_type) = bifurcation {
703 branch.bifurcations.push(BifurcationPoint {
704 bif_type,
705 parameter: new_par,
706 state: new_x.clone(),
707 critical_eigenvalues: find_critical_eigenvalues(&eigenvalues),
708 tangent: Some(new_tangent.clone()),
709 period: None,
710 });
711 branch.stats.bifurcations_detected += 1;
712 }
713
714 let residual = system.rhs(&new_x, new_par);
716 let residual_norm = residual.iter().map(|&v| v * v).sum::<f64>().sqrt();
717
718 branch.points.push(SolutionPoint {
719 parameter: new_par,
720 state: new_x.clone(),
721 stable,
722 eigenvalues,
723 period: None,
724 floquet_multipliers: None,
725 bifurcation,
726 arclength,
727 residual_norm,
728 });
729
730 x = new_x;
731 par = new_par;
732 tangent = new_tangent;
733
734 if iters < 3 {
736 ds = (ds * 1.5).min(params.ds_max);
737 }
738
739 if (params.par_end > params.par_start && par > params.par_end) ||
741 (params.par_end < params.par_start && par < params.par_end) {
742 break;
743 }
744 }
745
746 Err(_) => {
747 ds = ds / 2.0;
749 branch.stats.step_size_reductions += 1;
750
751 if ds < params.ds_min {
752 return Err(AutoError::StepTooSmall(ds));
753 }
754 }
755 }
756
757 branch.stats.total_steps = step + 1;
758 }
759
760 Ok(branch)
761}
762
763fn compute_initial_tangent<S: OdeSystem>(
765 system: &S,
766 x: &Array1<f64>,
767 par: f64,
768 n: usize,
769 forward: bool,
770) -> Array1<f64> {
771 let jac = system.jacobian(x, par)
772 .unwrap_or_else(|| numerical_jacobian(system, x, par));
773
774 let eps = 1e-8;
776 let f0 = system.rhs(x, par);
777 let f1 = system.rhs(x, par + eps);
778 let df_dpar: Array1<f64> = (&f1 - &f0) / eps;
779
780 let mut aug = Array2::zeros((n, n + 1));
785 for i in 0..n {
786 for j in 0..n {
787 aug[[i, j]] = jac[[i, j]];
788 }
789 aug[[i, n]] = df_dpar[i];
790 }
791
792 let mut tangent = Array1::zeros(n + 1);
794 tangent[n] = 1.0; if let Ok(dx) = solve_linear_system(&jac, &(-&df_dpar)) {
798 for i in 0..n {
799 tangent[i] = dx[i];
800 }
801 }
802
803 let norm = tangent.iter().map(|&v| v * v).sum::<f64>().sqrt();
805 tangent = tangent / norm;
806
807 if !forward && tangent[n] > 0.0 || forward && tangent[n] < 0.0 {
809 tangent = -tangent;
810 }
811
812 tangent
813}
814
815fn compute_tangent<S: OdeSystem>(
817 system: &S,
818 x: &Array1<f64>,
819 par: f64,
820 prev_tangent: &Array1<f64>,
821 n: usize,
822) -> Array1<f64> {
823 let new_tangent = compute_initial_tangent(system, x, par, n, true);
824
825 let dot: f64 = new_tangent.iter().zip(prev_tangent.iter()).map(|(&a, &b)| a * b).sum();
827
828 if dot < 0.0 {
829 -new_tangent
830 } else {
831 new_tangent
832 }
833}
834
835fn newton_arclength<S: OdeSystem>(
837 system: &S,
838 mut x: Array1<f64>,
839 mut par: f64,
840 x0: &Array1<f64>,
841 par0: f64,
842 tangent: &Array1<f64>,
843 ds: f64,
844 tol: f64,
845 max_iter: usize,
846) -> Result<(Array1<f64>, f64, usize)> {
847 let n = x.len();
848
849 for iter in 0..max_iter {
850 let f = system.rhs(&x, par);
852
853 let mut g = -ds;
855 for i in 0..n {
856 g += tangent[i] * (x[i] - x0[i]);
857 }
858 g += tangent[n] * (par - par0);
859
860 let f_norm = f.iter().map(|&v| v * v).sum::<f64>().sqrt();
862 if f_norm < tol && g.abs() < tol {
863 return Ok((x, par, iter + 1));
864 }
865
866 let jac = system.jacobian(&x, par)
868 .unwrap_or_else(|| numerical_jacobian(system, &x, par));
869
870 let eps = 1e-8;
872 let f_par = system.rhs(&x, par + eps);
873 let df_dpar: Array1<f64> = (&f_par - &f) / eps;
874
875 let mut aug = Array2::zeros((n + 1, n + 1));
880 for i in 0..n {
881 for j in 0..n {
882 aug[[i, j]] = jac[[i, j]];
883 }
884 aug[[i, n]] = df_dpar[i];
885 }
886 for j in 0..n {
887 aug[[n, j]] = tangent[j];
888 }
889 aug[[n, n]] = tangent[n];
890
891 let mut rhs = Array1::zeros(n + 1);
892 for i in 0..n {
893 rhs[i] = -f[i];
894 }
895 rhs[n] = -g;
896
897 let delta = solve_linear_system(&aug, &rhs)?;
899
900 for i in 0..n {
902 x[i] += delta[i];
903 }
904 par += delta[n];
905 }
906
907 Err(AutoError::ConvergenceFailed(max_iter))
908}
909
910fn detect_bifurcation(
916 prev_eigs: &[(f64, f64)],
917 curr_eigs: &[(f64, f64)],
918) -> Option<BifurcationType> {
919 if prev_eigs.len() != curr_eigs.len() {
920 return None;
921 }
922
923 let mut prev_sorted: Vec<_> = prev_eigs.to_vec();
925 let mut curr_sorted: Vec<_> = curr_eigs.to_vec();
926 prev_sorted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
927 curr_sorted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap());
928
929 for (prev, curr) in prev_sorted.iter().zip(curr_sorted.iter()) {
930 let prev_re = prev.0;
931 let curr_re = curr.0;
932 let prev_im = prev.1;
933 let curr_im = curr.1;
934
935 if prev_re * curr_re < 0.0 && prev_im.abs() < 1e-6 && curr_im.abs() < 1e-6 {
937 return Some(BifurcationType::SaddleNode);
938 }
939
940 if prev_re * curr_re < 0.0 && prev_im.abs() > 1e-6 {
942 return Some(BifurcationType::Hopf);
943 }
944 }
945
946 None
947}
948
949fn find_critical_eigenvalues(eigenvalues: &[(f64, f64)]) -> Vec<(f64, f64)> {
951 eigenvalues
952 .iter()
953 .filter(|&(re, _)| re.abs() < 0.1)
954 .copied()
955 .collect()
956}
957
958pub fn branch_switch<S: OdeSystem>(
964 system: &S,
965 bif_point: &BifurcationPoint,
966 params: &ContinuationParams,
967 perturbation: f64,
968) -> Result<ContinuationBranch> {
969 let mut new_branch = ContinuationBranch::new("switched");
970
971 let tangent = bif_point.tangent.as_ref()
973 .ok_or_else(|| AutoError::InvalidParameter("No tangent at bifurcation".into()))?;
974
975 let n = bif_point.state.len();
976 let mut new_state = bif_point.state.clone();
977 for i in 0..n {
978 new_state[i] += perturbation * tangent[i];
979 }
980 let new_par = bif_point.parameter + perturbation * tangent[n];
981
982 let branch = arclength_continuation(
984 system,
985 new_state,
986 &ContinuationParams {
987 par_start: new_par,
988 ..params.clone()
989 },
990 )?;
991
992 new_branch.points = branch.points;
993 new_branch.bifurcations = branch.bifurcations;
994 new_branch.stats = branch.stats;
995 new_branch.stats.branch_switches = 1;
996
997 Ok(new_branch)
998}
999
1000pub struct FoldNormalForm;
1006
1007impl OdeSystem for FoldNormalForm {
1008 fn dim(&self) -> usize { 1 }
1009
1010 fn rhs(&self, x: &Array1<f64>, mu: f64) -> Array1<f64> {
1011 Array1::from_vec(vec![mu - x[0] * x[0]])
1012 }
1013
1014 fn jacobian(&self, x: &Array1<f64>, _mu: f64) -> Option<Array2<f64>> {
1015 Some(Array2::from_shape_vec((1, 1), vec![-2.0 * x[0]]).unwrap())
1016 }
1017}
1018
1019pub struct HopfNormalForm;
1021
1022impl OdeSystem for HopfNormalForm {
1023 fn dim(&self) -> usize { 2 }
1024
1025 fn rhs(&self, x: &Array1<f64>, mu: f64) -> Array1<f64> {
1026 let r2 = x[0] * x[0] + x[1] * x[1];
1027 Array1::from_vec(vec![
1028 mu * x[0] - x[1] - x[0] * r2,
1029 x[0] + mu * x[1] - x[1] * r2,
1030 ])
1031 }
1032
1033 fn jacobian(&self, x: &Array1<f64>, mu: f64) -> Option<Array2<f64>> {
1034 let r2 = x[0] * x[0] + x[1] * x[1];
1035 Some(Array2::from_shape_vec((2, 2), vec![
1036 mu - 3.0 * x[0] * x[0] - x[1] * x[1],
1037 -1.0 - 2.0 * x[0] * x[1],
1038 1.0 - 2.0 * x[0] * x[1],
1039 mu - x[0] * x[0] - 3.0 * x[1] * x[1],
1040 ]).unwrap())
1041 }
1042}
1043
1044pub struct PitchforkNormalForm;
1046
1047impl OdeSystem for PitchforkNormalForm {
1048 fn dim(&self) -> usize { 1 }
1049
1050 fn rhs(&self, x: &Array1<f64>, mu: f64) -> Array1<f64> {
1051 Array1::from_vec(vec![mu * x[0] - x[0].powi(3)])
1052 }
1053
1054 fn jacobian(&self, x: &Array1<f64>, mu: f64) -> Option<Array2<f64>> {
1055 Some(Array2::from_shape_vec((1, 1), vec![mu - 3.0 * x[0] * x[0]]).unwrap())
1056 }
1057}
1058
1059pub struct Brusselator {
1061 pub a: f64,
1062 pub b: f64,
1063}
1064
1065impl Default for Brusselator {
1066 fn default() -> Self {
1067 Self { a: 1.0, b: 3.0 }
1068 }
1069}
1070
1071impl OdeSystem for Brusselator {
1072 fn dim(&self) -> usize { 2 }
1073
1074 fn rhs(&self, x: &Array1<f64>, _par: f64) -> Array1<f64> {
1075 let x_val = x[0];
1077 let y_val = x[1];
1078 Array1::from_vec(vec![
1079 self.a + x_val * x_val * y_val - (self.b + 1.0) * x_val,
1080 self.b * x_val - x_val * x_val * y_val,
1081 ])
1082 }
1083}
1084
1085pub struct LorenzSystem {
1087 pub sigma: f64,
1088 pub rho: f64,
1089 pub beta: f64,
1090}
1091
1092impl Default for LorenzSystem {
1093 fn default() -> Self {
1094 Self {
1095 sigma: 10.0,
1096 rho: 28.0,
1097 beta: 8.0 / 3.0,
1098 }
1099 }
1100}
1101
1102impl OdeSystem for LorenzSystem {
1103 fn dim(&self) -> usize { 3 }
1104
1105 fn rhs(&self, x: &Array1<f64>, _par: f64) -> Array1<f64> {
1106 Array1::from_vec(vec![
1107 self.sigma * (x[1] - x[0]),
1108 x[0] * (self.rho - x[2]) - x[1],
1109 x[0] * x[1] - self.beta * x[2],
1110 ])
1111 }
1112
1113 fn jacobian(&self, x: &Array1<f64>, _par: f64) -> Option<Array2<f64>> {
1114 Some(Array2::from_shape_vec((3, 3), vec![
1115 -self.sigma, self.sigma, 0.0,
1116 self.rho - x[2], -1.0, -x[0],
1117 x[1], x[0], -self.beta,
1118 ]).unwrap())
1119 }
1120}
1121
1122#[cfg(test)]
1127mod tests {
1128 use super::*;
1129
1130 #[test]
1131 fn test_fold_normal_form() {
1132 let system = FoldNormalForm;
1133
1134 let f = system.rhs(&Array1::from_vec(vec![0.0]), 0.0);
1136 assert!(f[0].abs() < 1e-10);
1137
1138 let f = system.rhs(&Array1::from_vec(vec![1.0]), 1.0);
1140 assert!(f[0].abs() < 1e-10);
1141 }
1142
1143 #[test]
1144 fn test_eigenvalues_2x2() {
1145 let a = Array2::from_shape_vec((2, 2), vec![2.0, 0.0, 0.0, 3.0]).unwrap();
1147 let eigs = eigenvalues_2x2(&a);
1148 assert!((eigs[0].0 - 3.0).abs() < 1e-10 || (eigs[1].0 - 3.0).abs() < 1e-10);
1149 assert!((eigs[0].0 - 2.0).abs() < 1e-10 || (eigs[1].0 - 2.0).abs() < 1e-10);
1150
1151 let b = Array2::from_shape_vec((2, 2), vec![0.0, -1.0, 1.0, 0.0]).unwrap();
1153 let eigs = eigenvalues_2x2(&b);
1154 assert!(eigs[0].0.abs() < 1e-10);
1155 assert!((eigs[0].1.abs() - 1.0).abs() < 1e-10);
1156 }
1157
1158 #[test]
1159 fn test_newton_solver() {
1160 let f = |x: &Array1<f64>| Array1::from_vec(vec![x[0] * x[0] - 2.0]);
1162 let j = |x: &Array1<f64>| Array2::from_shape_vec((1, 1), vec![2.0 * x[0]]).unwrap();
1163
1164 let (sol, _iters) = newton_solve(f, j, Array1::from_vec(vec![1.0]), 1e-10, 20).unwrap();
1165 assert!((sol[0] - 2.0_f64.sqrt()).abs() < 1e-8);
1166 }
1167
1168 #[test]
1169 fn test_linear_solve() {
1170 let a = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1171 let b = Array1::from_vec(vec![5.0, 11.0]);
1172
1173 let x = solve_linear_system(&a, &b).unwrap();
1174
1175 let ax = a.dot(&x);
1177 assert!((ax[0] - b[0]).abs() < 1e-10);
1178 assert!((ax[1] - b[1]).abs() < 1e-10);
1179 }
1180
1181 #[test]
1182 fn test_hopf_equilibrium() {
1183 let system = HopfNormalForm;
1184
1185 let jac = system.jacobian(&Array1::from_vec(vec![0.0, 0.0]), -0.5).unwrap();
1187 let eigs = compute_eigenvalues(&jac);
1188
1189 assert!(eigs.iter().all(|&(re, _)| re < 0.0));
1191 }
1192
1193 #[test]
1194 fn test_natural_continuation() {
1195 let system = FoldNormalForm;
1196 let params = ContinuationParams {
1197 par_start: 0.0,
1198 par_end: 2.0,
1199 ds: 0.1,
1200 max_steps: 30,
1201 detect_bifurcations: false,
1202 ..Default::default()
1203 };
1204
1205 let branch = natural_continuation(&system, Array1::from_vec(vec![0.01]), ¶ms).unwrap();
1206
1207 assert!(branch.points.len() > 10);
1208 assert!(branch.points.last().unwrap().parameter > 1.5);
1209 }
1210
1211 #[test]
1212 fn test_qr_decomposition() {
1213 let a = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).unwrap();
1214 let (q, r) = qr_decomposition(&a);
1215
1216 let qtq = q.t().dot(&q);
1218 assert!((qtq[[0, 0]] - 1.0).abs() < 1e-10);
1219 assert!((qtq[[1, 1]] - 1.0).abs() < 1e-10);
1220
1221 let qr = q.dot(&r);
1223 assert!((qr[[0, 0]] - a[[0, 0]]).abs() < 1e-10);
1224 }
1225
1226 #[test]
1227 fn test_brusselator() {
1228 let system = Brusselator::default();
1229 let eq = Array1::from_vec(vec![system.a, system.b / system.a]);
1230
1231 let f = system.rhs(&eq, 0.0);
1232 assert!(f[0].abs() < 1e-10);
1233 assert!(f[1].abs() < 1e-10);
1234 }
1235}