oldies_auto/
lib.rs

1//! # AUTO-RS: AUTO Continuation/Bifurcation Software Revival
2//!
3//! Revival of AUTO continuation/bifurcation software in Rust.
4//! Originally created by Eusebius Doedel (1980s) at Concordia University.
5//!
6//! AUTO is the gold standard for numerical continuation and bifurcation
7//! analysis of ODEs, PDEs, and algebraic equations.
8//!
9//! This crate provides:
10//! - Natural parameter continuation
11//! - Pseudo-arclength continuation
12//! - Bifurcation detection (saddle-node, Hopf, branch points)
13//! - Stability analysis via eigenvalue computation
14//! - Branch switching at bifurcation points
15//!
16//! Reference: Doedel, E.J. et al. AUTO-07P: Continuation and Bifurcation
17//! Software for Ordinary Differential Equations.
18
19use 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// ============================================================================
42// CONTINUATION PARAMETERS
43// ============================================================================
44
45/// Continuation control parameters (like AUTO's constants file)
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ContinuationParams {
48    /// Parameter name to continue
49    pub parameter: String,
50    /// Starting parameter value
51    pub par_start: f64,
52    /// Ending parameter value
53    pub par_end: f64,
54    /// Initial step size
55    pub ds: f64,
56    /// Minimum step size
57    pub ds_min: f64,
58    /// Maximum step size
59    pub ds_max: f64,
60    /// Maximum number of continuation steps
61    pub max_steps: usize,
62    /// Newton tolerance
63    pub newton_tol: f64,
64    /// Maximum Newton iterations
65    pub newton_max_iter: usize,
66    /// Number of mesh points for collocation (BVP)
67    pub ntst: usize,
68    /// Number of collocation points per mesh interval
69    pub ncol: usize,
70    /// Adaptive mesh refinement threshold
71    pub adapt_threshold: f64,
72    /// Output every N steps
73    pub output_every: usize,
74    /// Detect bifurcations
75    pub detect_bifurcations: bool,
76    /// Branch switching tolerance
77    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// ============================================================================
103// BIFURCATION TYPES
104// ============================================================================
105
106/// Types of bifurcations detected
107#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
108pub enum BifurcationType {
109    /// Regular point (not a bifurcation)
110    Regular,
111    /// Saddle-node (fold/limit point)
112    SaddleNode,
113    /// Transcritical bifurcation
114    Transcritical,
115    /// Pitchfork bifurcation
116    Pitchfork,
117    /// Hopf bifurcation (birth of limit cycle)
118    Hopf,
119    /// Period-doubling (flip) bifurcation
120    PeriodDoubling,
121    /// Torus (Neimark-Sacker) bifurcation
122    Torus,
123    /// Branch point (multiple solutions)
124    BranchPoint,
125    /// Limit point of cycles
126    LimitPointCycle,
127    /// User-defined zero
128    UserZero,
129}
130
131/// Bifurcation point information
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct BifurcationPoint {
134    pub bif_type: BifurcationType,
135    pub parameter: f64,
136    pub state: Array1<f64>,
137    /// Eigenvalue(s) crossing imaginary axis
138    pub critical_eigenvalues: Vec<(f64, f64)>,  // (real, imag)
139    /// Direction for branch switching
140    pub tangent: Option<Array1<f64>>,
141    /// Period (for periodic orbits)
142    pub period: Option<f64>,
143}
144
145// ============================================================================
146// SOLUTION POINT
147// ============================================================================
148
149/// A point on the continuation branch
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct SolutionPoint {
152    /// Parameter value
153    pub parameter: f64,
154    /// State variables
155    pub state: Array1<f64>,
156    /// Stability (true = stable)
157    pub stable: bool,
158    /// Eigenvalues of the Jacobian
159    pub eigenvalues: Vec<(f64, f64)>,  // (real, imag)
160    /// Period (for periodic solutions)
161    pub period: Option<f64>,
162    /// Floquet multipliers (for periodic solutions)
163    pub floquet_multipliers: Option<Vec<(f64, f64)>>,
164    /// Bifurcation type if this is a special point
165    pub bifurcation: Option<BifurcationType>,
166    /// Arclength along the branch
167    pub arclength: f64,
168    /// Norm of the residual
169    pub residual_norm: f64,
170}
171
172// ============================================================================
173// CONTINUATION BRANCH
174// ============================================================================
175
176/// Result of a continuation run
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ContinuationBranch {
179    pub name: String,
180    pub points: Vec<SolutionPoint>,
181    pub bifurcations: Vec<BifurcationPoint>,
182    /// Is this a branch of equilibria or periodic orbits?
183    pub is_periodic: bool,
184    /// Computation statistics
185    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    /// Get parameter range
200    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    /// Find stable portions
211    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/// Computation statistics
235#[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
246// ============================================================================
247// ODE SYSTEM TRAIT
248// ============================================================================
249
250/// Trait for ODE systems to be continued
251pub trait OdeSystem {
252    /// Dimension of the state space
253    fn dim(&self) -> usize;
254
255    /// Right-hand side: dx/dt = f(x, par)
256    fn rhs(&self, x: &Array1<f64>, par: f64) -> Array1<f64>;
257
258    /// Jacobian df/dx (if not provided, numerical differentiation is used)
259    fn jacobian(&self, _x: &Array1<f64>, _par: f64) -> Option<Array2<f64>> {
260        None
261    }
262
263    /// Parameter sensitivity df/dpar
264    fn par_derivative(&self, _x: &Array1<f64>, _par: f64) -> Option<Array1<f64>> {
265        None
266    }
267}
268
269// ============================================================================
270// NEWTON SOLVER
271// ============================================================================
272
273/// Newton's method for finding roots of F(x) = 0
274pub 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
301/// Simple LU-based linear solver
302fn 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    // Gaussian elimination with partial pivoting
311    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    // Forward elimination
320    for k in 0..n {
321        // Pivot
322        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        // Swap rows
336        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        // Eliminate
345        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    // Back substitution
354    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
366// ============================================================================
367// EIGENVALUE COMPUTATION
368// ============================================================================
369
370/// Compute eigenvalues of a matrix using QR iteration
371pub fn compute_eigenvalues(a: &Array2<f64>) -> Vec<(f64, f64)> {
372    let n = a.nrows();
373    if n == 0 {
374        return vec![];
375    }
376
377    // For small matrices, use direct methods
378    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 iteration for larger matrices
387    qr_eigenvalues(a)
388}
389
390/// Eigenvalues of 2x2 matrix
391fn 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
413/// QR iteration for eigenvalues
414fn 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    // Hessenberg reduction first (simplified)
420    for iter in 0..max_iter {
421        // Shifted QR step
422        let shift = h[[n - 1, n - 1]];
423
424        for i in 0..n {
425            h[[i, i]] -= shift;
426        }
427
428        // QR decomposition (simplified Householder)
429        let (q, r) = qr_decomposition(&h);
430
431        // H = R * Q
432        h = r.dot(&q);
433
434        for i in 0..n {
435            h[[i, i]] += shift;
436        }
437
438        // Check convergence
439        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    // Extract eigenvalues from quasi-upper triangular form
453    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            // Real eigenvalue
458            eigenvalues.push((h[[i, i]], 0.0));
459            i += 1;
460        } else {
461            // Complex conjugate pair from 2x2 block
462            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
476/// QR decomposition (simplified Gram-Schmidt)
477fn 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
504// ============================================================================
505// NATURAL PARAMETER CONTINUATION
506// ============================================================================
507
508/// Natural parameter continuation
509/// Simple method that just varies the parameter and solves at each step
510pub 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        // Solve F(x, par) = 0
524        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        // Compute stability
536        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        // Check for bifurcations
542        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        // Store solution point
561        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        // Update parameter
580        par += direction * params.ds;
581
582        // Check if we've reached the end
583        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
594/// Numerical Jacobian via finite differences
595fn 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
615// ============================================================================
616// PSEUDO-ARCLENGTH CONTINUATION
617// ============================================================================
618
619/// Pseudo-arclength continuation
620/// Parameterizes the curve by arclength to handle turning points
621pub 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    // Extended state: (x, par)
630    let mut x = initial_state.clone();
631    let mut par = params.par_start;
632    let mut ds = params.ds;
633
634    // Initial tangent direction
635    let mut tangent = compute_initial_tangent(system, &x, par, n, params.par_end > params.par_start);
636    let mut arclength = 0.0;
637
638    // First point
639    {
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        // Predictor: move along tangent
660        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        // Corrector: Newton iteration with arclength constraint
667        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                // Update tangent
687                let new_tangent = compute_tangent(system, &new_x, new_par, &tangent, n);
688
689                // Stability
690                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                // Detect bifurcations
696                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                // Store point
715                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                // Adaptive step size
735                if iters < 3 {
736                    ds = (ds * 1.5).min(params.ds_max);
737                }
738
739                // Check termination
740                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                // Reduce step size and try again
748                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
763/// Compute initial tangent vector
764fn 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    // Compute df/dpar numerically
775    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    // Solve [df/dx | df/dpar] * [dx; dpar] = 0
781    // with ||tangent|| = 1
782
783    // Use nullspace of augmented Jacobian
784    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    // Compute nullspace via SVD-like approach (simplified)
793    let mut tangent = Array1::zeros(n + 1);
794    tangent[n] = 1.0;  // Start with pure parameter variation
795
796    // Solve Jac * dx = -df_dpar * dpar
797    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    // Normalize
804    let norm = tangent.iter().map(|&v| v * v).sum::<f64>().sqrt();
805    tangent = tangent / norm;
806
807    // Ensure correct direction
808    if !forward && tangent[n] > 0.0 || forward && tangent[n] < 0.0 {
809        tangent = -tangent;
810    }
811
812    tangent
813}
814
815/// Compute tangent at a new point
816fn 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    // Choose orientation consistent with previous tangent
826    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
835/// Newton iteration with arclength constraint
836fn 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        // System: F(x, par) = 0
851        let f = system.rhs(&x, par);
852
853        // Arclength constraint: tangent . (x - x0, par - par0) - ds = 0
854        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        // Check convergence
861        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        // Jacobian of F
867        let jac = system.jacobian(&x, par)
868            .unwrap_or_else(|| numerical_jacobian(system, &x, par));
869
870        // df/dpar
871        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        // Build augmented system
876        // [J   | df/dpar] [dx  ]   [-F]
877        // [t_x | t_par  ] [dpar] = [-g]
878
879        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        // Solve
898        let delta = solve_linear_system(&aug, &rhs)?;
899
900        // Update
901        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
910// ============================================================================
911// BIFURCATION DETECTION
912// ============================================================================
913
914/// Detect bifurcation by monitoring eigenvalue changes
915fn 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    // Sort eigenvalues by real part for comparison
924    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        // Real eigenvalue crossing zero (saddle-node or transcritical)
936        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        // Complex conjugate pair crossing imaginary axis (Hopf)
941        if prev_re * curr_re < 0.0 && prev_im.abs() > 1e-6 {
942            return Some(BifurcationType::Hopf);
943        }
944    }
945
946    None
947}
948
949/// Find eigenvalues that are near the imaginary axis
950fn 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
958// ============================================================================
959// BRANCH SWITCHING
960// ============================================================================
961
962/// Switch to a new branch at a bifurcation point
963pub 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    // Perturb state in direction of tangent
972    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    // Continue from perturbed state
983    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
1000// ============================================================================
1001// STANDARD TEST PROBLEMS
1002// ============================================================================
1003
1004/// Fold (saddle-node) normal form: dx/dt = mu - x^2
1005pub 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
1019/// Hopf normal form: dx/dt = mu*x - y - x*(x^2+y^2), dy/dt = x + mu*y - y*(x^2+y^2)
1020pub 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
1044/// Pitchfork normal form: dx/dt = mu*x - x^3
1045pub 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
1059/// Brusselator: famous chemical oscillator
1060pub 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        // par could be 'b' for continuation
1076        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
1085/// Lorenz system
1086pub 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// ============================================================================
1123// TESTS
1124// ============================================================================
1125
1126#[cfg(test)]
1127mod tests {
1128    use super::*;
1129
1130    #[test]
1131    fn test_fold_normal_form() {
1132        let system = FoldNormalForm;
1133
1134        // At mu = 0, x = 0 is a solution
1135        let f = system.rhs(&Array1::from_vec(vec![0.0]), 0.0);
1136        assert!(f[0].abs() < 1e-10);
1137
1138        // At mu = 1, x = 1 is a solution
1139        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        // Real eigenvalues
1146        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        // Complex eigenvalues
1152        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        // Solve x^2 - 2 = 0
1161        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        // Check Ax = b
1176        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        // At mu < 0, origin should be stable
1186        let jac = system.jacobian(&Array1::from_vec(vec![0.0, 0.0]), -0.5).unwrap();
1187        let eigs = compute_eigenvalues(&jac);
1188
1189        // Both eigenvalues should have negative real part
1190        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]), &params).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        // Q should be orthogonal: Q^T * Q = I
1217        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        // QR should equal A
1222        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}