Skip to main content

scirs2_integrate/analysis/
stability.rs

1//! Stability analysis tools for dynamical systems
2//!
3//! This module provides the StabilityAnalyzer and related functionality
4//! for assessing the stability of fixed points and periodic orbits.
5
6use crate::analysis::advanced;
7use crate::analysis::types::*;
8use crate::error::{IntegrateError, IntegrateResult};
9use scirs2_core::ndarray::{Array1, Array2};
10use scirs2_core::numeric::Complex64;
11
12/// Stability analyzer for dynamical systems
13pub struct StabilityAnalyzer {
14    /// System dimension
15    pub dimension: usize,
16    /// Tolerance for fixed point detection
17    pub tolerance: f64,
18    /// Integration time for trajectory analysis
19    pub integration_time: f64,
20    /// Number of test points for basin analysis
21    pub basin_grid_size: usize,
22}
23
24impl StabilityAnalyzer {
25    /// Create a new stability analyzer
26    pub fn new(dimension: usize) -> Self {
27        Self {
28            dimension,
29            tolerance: 1e-8,
30            integration_time: 100.0,
31            basin_grid_size: 50,
32        }
33    }
34
35    /// Perform comprehensive stability analysis
36    pub fn analyze_stability<F>(
37        &self,
38        system: F,
39        domain: &[(f64, f64)],
40    ) -> IntegrateResult<StabilityResult>
41    where
42        F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync + Clone + 'static,
43    {
44        // Find fixed points
45        let fixed_points = self.find_fixed_points(&system, domain)?;
46
47        // Find periodic orbits (simplified)
48        let periodic_orbits = self.find_periodic_orbits(&system, domain)?;
49
50        // Compute Lyapunov exponents
51        let lyapunov_exponents = self.compute_lyapunov_exponents(&system)?;
52
53        // Analyze basins of attraction
54        let basin_analysis = if self.dimension == 2 {
55            Some(self.analyze_basins(&system, domain, &fixed_points)?)
56        } else {
57            None
58        };
59
60        Ok(StabilityResult {
61            fixed_points,
62            periodic_orbits,
63            lyapunov_exponents,
64            basin_analysis,
65        })
66    }
67
68    /// Find fixed points in the given domain
69    fn find_fixed_points<F>(
70        &self,
71        system: &F,
72        domain: &[(f64, f64)],
73    ) -> IntegrateResult<Vec<FixedPoint>>
74    where
75        F: Fn(&Array1<f64>) -> Array1<f64>,
76    {
77        let mut fixed_points: Vec<FixedPoint> = Vec::new();
78        let grid_size = 10; // Number of initial guesses per dimension
79
80        // Generate grid of initial guesses
81        let mut initial_guesses = Vec::new();
82        self.generate_grid_points(domain, grid_size, &mut initial_guesses);
83
84        for guess in initial_guesses {
85            if let Ok(fixed_point) = self.newton_raphson_fixed_point(system, &guess) {
86                // Check if this fixed point is already found
87                let mut is_duplicate = false;
88                for existing_fp in &fixed_points {
89                    let distance = (&fixed_point - &existing_fp.location)
90                        .iter()
91                        .map(|&x| x * x)
92                        .sum::<f64>()
93                        .sqrt();
94
95                    if distance < self.tolerance * 10.0 {
96                        is_duplicate = true;
97                        break;
98                    }
99                }
100
101                if !is_duplicate {
102                    // Compute stability
103                    let jacobian = self.compute_jacobian_at_point(system, &fixed_point)?;
104                    let eigenvalues = self.compute_real_eigenvalues(&jacobian)?;
105                    let eigenvectors = self.compute_eigenvectors(&jacobian, &eigenvalues)?;
106                    let stability = self.classify_stability(&eigenvalues);
107
108                    fixed_points.push(FixedPoint {
109                        location: fixed_point,
110                        stability,
111                        eigenvalues,
112                        eigenvectors,
113                    });
114                }
115            }
116        }
117
118        Ok(fixed_points)
119    }
120
121    /// Generate grid of points in domain
122    fn generate_grid_points(
123        &self,
124        domain: &[(f64, f64)],
125        grid_size: usize,
126        points: &mut Vec<Array1<f64>>,
127    ) {
128        fn generate_recursive(
129            domain: &[(f64, f64)],
130            grid_size: usize,
131            current: &mut Vec<f64>,
132            dim: usize,
133            points: &mut Vec<Array1<f64>>,
134        ) {
135            if dim == domain.len() {
136                points.push(Array1::from_vec(current.clone()));
137                return;
138            }
139
140            // Check for division by zero in step calculation
141            if grid_size <= 1 {
142                return; // Skip invalid grid size
143            }
144            let step = (domain[dim].1 - domain[dim].0) / (grid_size - 1) as f64;
145            for i in 0..grid_size {
146                let value = domain[dim].0 + i as f64 * step;
147                current.push(value);
148                generate_recursive(domain, grid_size, current, dim + 1, points);
149                current.pop();
150            }
151        }
152
153        let mut current = Vec::new();
154        generate_recursive(domain, grid_size, &mut current, 0, points);
155    }
156
157    /// Find fixed point using Newton-Raphson method
158    fn newton_raphson_fixed_point<F>(
159        &self,
160        system: &F,
161        initial_guess: &Array1<f64>,
162    ) -> IntegrateResult<Array1<f64>>
163    where
164        F: Fn(&Array1<f64>) -> Array1<f64>,
165    {
166        let mut x = initial_guess.clone();
167        let max_iterations = 100;
168
169        for _ in 0..max_iterations {
170            let f_x = system(&x);
171            let sum_squares = f_x.iter().map(|&v| v * v).sum::<f64>();
172            if sum_squares < 0.0 {
173                return Err(IntegrateError::ComputationError(
174                    "Negative sum of squares in residual norm calculation".to_string(),
175                ));
176            }
177            let residual_norm = sum_squares.sqrt();
178
179            if residual_norm < self.tolerance {
180                return Ok(x);
181            }
182
183            let jacobian = self.compute_jacobian_at_point(system, &x)?;
184
185            // Solve J * dx = -f(x)
186            let mut augmented = Array2::zeros((self.dimension, self.dimension + 1));
187            for i in 0..self.dimension {
188                for j in 0..self.dimension {
189                    augmented[[i, j]] = jacobian[[i, j]];
190                }
191                augmented[[i, self.dimension]] = -f_x[i];
192            }
193
194            let dx = self.gaussian_elimination(&augmented)?;
195            x += &dx;
196        }
197
198        Err(IntegrateError::ConvergenceError(
199            "Newton-Raphson did not converge".to_string(),
200        ))
201    }
202
203    /// Compute Jacobian at a specific point
204    fn compute_jacobian_at_point<F>(
205        &self,
206        system: &F,
207        x: &Array1<f64>,
208    ) -> IntegrateResult<Array2<f64>>
209    where
210        F: Fn(&Array1<f64>) -> Array1<f64>,
211    {
212        let h = 1e-8_f64;
213        let n = x.len();
214        let mut jacobian = Array2::zeros((n, n));
215
216        let f0 = system(x);
217
218        // Check for valid step size
219        if h.abs() < 1e-15 {
220            return Err(IntegrateError::ComputationError(
221                "Step size too small for finite difference calculation".to_string(),
222            ));
223        }
224
225        for j in 0..n {
226            let mut x_plus = x.clone();
227            x_plus[j] += h;
228            let f_plus = system(&x_plus);
229
230            for i in 0..n {
231                jacobian[[i, j]] = (f_plus[i] - f0[i]) / h;
232            }
233        }
234
235        Ok(jacobian)
236    }
237
238    /// Solve linear system using Gaussian elimination
239    fn gaussian_elimination(&self, augmented: &Array2<f64>) -> IntegrateResult<Array1<f64>> {
240        let n = augmented.nrows();
241        let mut a = augmented.clone();
242
243        // Forward elimination
244        for k in 0..n {
245            // Find pivot
246            let mut max_row = k;
247            for i in k + 1..n {
248                if a[[i, k]].abs() > a[[max_row, k]].abs() {
249                    max_row = i;
250                }
251            }
252
253            // Swap rows
254            if max_row != k {
255                for j in k..n + 1 {
256                    let temp = a[[k, j]];
257                    a[[k, j]] = a[[max_row, j]];
258                    a[[max_row, j]] = temp;
259                }
260            }
261
262            // Check for singularity
263            if a[[k, k]].abs() < 1e-14 {
264                return Err(IntegrateError::ComputationError(
265                    "Matrix is singular".to_string(),
266                ));
267            }
268
269            // Eliminate
270            for i in k + 1..n {
271                let factor = a[[i, k]] / a[[k, k]];
272                for j in k..n + 1 {
273                    a[[i, j]] -= factor * a[[k, j]];
274                }
275            }
276        }
277
278        // Back substitution
279        let mut x = Array1::zeros(n);
280        for i in (0..n).rev() {
281            x[i] = a[[i, n]];
282            for j in i + 1..n {
283                x[i] -= a[[i, j]] * x[j];
284            }
285            // Check for zero diagonal element
286            if a[[i, i]].abs() < 1e-14 {
287                return Err(IntegrateError::ComputationError(
288                    "Zero diagonal element in back substitution".to_string(),
289                ));
290            }
291            x[i] /= a[[i, i]];
292        }
293
294        Ok(x)
295    }
296
297    /// Compute real eigenvalues (simplified implementation)
298    fn compute_real_eigenvalues(&self, matrix: &Array2<f64>) -> IntegrateResult<Vec<Complex64>> {
299        // For now, use a simplified approach for 2x2 matrices
300        let n = matrix.nrows();
301
302        if n == 2 {
303            let a = matrix[[0, 0]];
304            let b = matrix[[0, 1]];
305            let c = matrix[[1, 0]];
306            let d = matrix[[1, 1]];
307
308            let trace = a + d;
309            let det = a * d - b * c;
310            let discriminant = trace * trace - 4.0 * det;
311
312            if discriminant >= 0.0 {
313                let sqrt_disc = discriminant.sqrt();
314                let lambda1 = (trace + sqrt_disc) / 2.0;
315                let lambda2 = (trace - sqrt_disc) / 2.0;
316                Ok(vec![
317                    Complex64::new(lambda1, 0.0),
318                    Complex64::new(lambda2, 0.0),
319                ])
320            } else {
321                let real_part = trace / 2.0;
322                let imag_part = (-discriminant).sqrt() / 2.0;
323                Ok(vec![
324                    Complex64::new(real_part, imag_part),
325                    Complex64::new(real_part, -imag_part),
326                ])
327            }
328        } else {
329            // For higher dimensions, use the QR algorithm
330            self.eigenvalues_qr_algorithm(matrix)
331        }
332    }
333
334    /// Compute eigenvalues using QR algorithm for larger matrices
335    fn eigenvalues_qr_algorithm(&self, matrix: &Array2<f64>) -> IntegrateResult<Vec<Complex64>> {
336        let n = matrix.nrows();
337        let mut a = matrix.clone();
338        let max_iterations = 100;
339        let tolerance = 1e-10;
340
341        // First, reduce to upper Hessenberg form for better convergence
342        a = self.reduce_to_hessenberg(&a)?;
343
344        // Apply QR iterations
345        for _ in 0..max_iterations {
346            let (q, r) = self.qr_decomposition_real(&a)?;
347            a = r.dot(&q);
348
349            // Check convergence by examining sub-diagonal elements
350            let mut converged = true;
351            for i in 1..n {
352                if a[[i, i - 1]].abs() > tolerance {
353                    converged = false;
354                    break;
355                }
356            }
357
358            if converged {
359                break;
360            }
361        }
362
363        // Extract eigenvalues from the diagonal
364        let mut eigenvalues = Vec::new();
365        let mut i = 0;
366        while i < n {
367            if i == n - 1 || a[[i + 1, i]].abs() < tolerance {
368                // Real eigenvalue
369                eigenvalues.push(Complex64::new(a[[i, i]], 0.0));
370                i += 1;
371            } else {
372                // Complex conjugate pair
373                let trace = a[[i, i]] + a[[i + 1, i + 1]];
374                let det = a[[i, i]] * a[[i + 1, i + 1]] - a[[i, i + 1]] * a[[i + 1, i]];
375                let discriminant = trace * trace - 4.0 * det;
376
377                if discriminant >= 0.0 {
378                    let sqrt_disc = discriminant.sqrt();
379                    eigenvalues.push(Complex64::new((trace + sqrt_disc) / 2.0, 0.0));
380                    eigenvalues.push(Complex64::new((trace - sqrt_disc) / 2.0, 0.0));
381                } else {
382                    let real_part = trace / 2.0;
383                    let imag_part = (-discriminant).sqrt() / 2.0;
384                    eigenvalues.push(Complex64::new(real_part, imag_part));
385                    eigenvalues.push(Complex64::new(real_part, -imag_part));
386                }
387                i += 2;
388            }
389        }
390
391        Ok(eigenvalues)
392    }
393
394    /// Reduce matrix to upper Hessenberg form using Householder reflections
395    fn reduce_to_hessenberg(&self, matrix: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
396        let n = matrix.nrows();
397        let mut h = matrix.clone();
398
399        for k in 0..(n - 2) {
400            // Extract the column below the diagonal
401            let mut x = Array1::<f64>::zeros(n - k - 1);
402            for i in 0..(n - k - 1) {
403                x[i] = h[[k + 1 + i, k]];
404            }
405
406            if x.iter().map(|&v| v * v).sum::<f64>().sqrt() > 1e-15 {
407                // Compute Householder vector
408                let alpha = if x[0] >= 0.0 {
409                    -x.iter().map(|&v| v * v).sum::<f64>().sqrt()
410                } else {
411                    x.iter().map(|&v| v * v).sum::<f64>().sqrt()
412                };
413
414                let mut v = x.clone();
415                v[0] -= alpha;
416                let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
417
418                if v_norm > 1e-15 {
419                    v.mapv_inplace(|vi| vi / v_norm);
420
421                    // Apply Householder reflection: H = I - 2*v*v^T
422                    // H * A
423                    for j in k..n {
424                        let dot_product: f64 =
425                            (0..(n - k - 1)).map(|i| v[i] * h[[k + 1 + i, j]]).sum();
426                        for i in 0..(n - k - 1) {
427                            h[[k + 1 + i, j]] -= 2.0 * v[i] * dot_product;
428                        }
429                    }
430
431                    // A * H
432                    for i in 0..n {
433                        let dot_product: f64 =
434                            (0..(n - k - 1)).map(|j| h[[i, k + 1 + j]] * v[j]).sum();
435                        for j in 0..(n - k - 1) {
436                            h[[i, k + 1 + j]] -= 2.0 * v[j] * dot_product;
437                        }
438                    }
439                }
440            }
441        }
442
443        Ok(h)
444    }
445
446    /// QR decomposition for real matrices
447    fn qr_decomposition_real(
448        &self,
449        matrix: &Array2<f64>,
450    ) -> IntegrateResult<(Array2<f64>, Array2<f64>)> {
451        let (m, n) = matrix.dim();
452        let mut q = Array2::<f64>::eye(m);
453        let mut r = matrix.clone();
454
455        for k in 0..std::cmp::min(m - 1, n) {
456            // Extract column k from row k onwards
457            let mut x = Array1::<f64>::zeros(m - k);
458            for i in 0..(m - k) {
459                x[i] = r[[k + i, k]];
460            }
461
462            // Compute Householder vector
463            let norm_x = x.iter().map(|&v| v * v).sum::<f64>().sqrt();
464            if norm_x > 1e-15 {
465                let alpha = if x[0] >= 0.0 { -norm_x } else { norm_x };
466
467                let mut v = x.clone();
468                v[0] -= alpha;
469                let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
470
471                if v_norm > 1e-15 {
472                    v.mapv_inplace(|vi| vi / v_norm);
473
474                    // Apply Householder reflection to R
475                    for j in k..n {
476                        let dot_product: f64 = (0..(m - k)).map(|i| v[i] * r[[k + i, j]]).sum();
477                        for i in 0..(m - k) {
478                            r[[k + i, j]] -= 2.0 * v[i] * dot_product;
479                        }
480                    }
481
482                    // Apply Householder reflection to Q
483                    for i in 0..m {
484                        let dot_product: f64 = (0..(m - k)).map(|j| q[[i, k + j]] * v[j]).sum();
485                        for j in 0..(m - k) {
486                            q[[i, k + j]] -= 2.0 * v[j] * dot_product;
487                        }
488                    }
489                }
490            }
491        }
492
493        Ok((q, r))
494    }
495
496    /// Compute eigenvectors (simplified)
497    fn compute_eigenvectors(
498        &self,
499        _matrix: &Array2<f64>,
500        eigenvalues: &[Complex64],
501    ) -> IntegrateResult<Array2<Complex64>> {
502        let n = eigenvalues.len();
503        let eigenvectors = Array2::<Complex64>::zeros((n, n));
504
505        // Simplified: return identity matrix
506        // In practice, would solve (A - λI)v = 0 for each eigenvalue
507        Ok(eigenvectors)
508    }
509
510    /// Classify stability based on eigenvalues
511    fn classify_stability(&self, eigenvalues: &[Complex64]) -> StabilityType {
512        let mut has_positive_real = false;
513        let mut has_negative_real = false;
514        let mut has_zero_real = false;
515
516        for eigenvalue in eigenvalues {
517            if eigenvalue.re > 1e-10 {
518                has_positive_real = true;
519            } else if eigenvalue.re < -1e-10 {
520                has_negative_real = true;
521            } else {
522                has_zero_real = true;
523            }
524        }
525
526        if has_zero_real {
527            StabilityType::Degenerate
528        } else if has_positive_real && has_negative_real {
529            StabilityType::Saddle
530        } else if has_positive_real {
531            StabilityType::Unstable
532        } else if has_negative_real {
533            StabilityType::Stable
534        } else {
535            StabilityType::Center
536        }
537    }
538
539    /// Find periodic orbits using multiple detection methods
540    fn find_periodic_orbits<F>(
541        &self,
542        system: &F,
543        domain: &[(f64, f64)],
544    ) -> IntegrateResult<Vec<PeriodicOrbit>>
545    where
546        F: Fn(&Array1<f64>) -> Array1<f64>,
547    {
548        let mut periodic_orbits = Vec::new();
549
550        // Method 1: Shooting method for periodic orbits
551        if let Ok(shooting_orbits) = self.shooting_method_periodic_orbits(system, domain) {
552            periodic_orbits.extend(shooting_orbits);
553        }
554
555        // Method 2: Return map analysis
556        if let Ok(return_map_orbits) = self.return_map_periodic_orbits(system, domain) {
557            periodic_orbits.extend(return_map_orbits);
558        }
559
560        // Method 3: Fourier analysis of trajectories
561        if let Ok(fourier_orbits) = self.fourier_analysis_periodic_orbits(system, domain) {
562            periodic_orbits.extend(fourier_orbits);
563        }
564
565        // Remove duplicates based on spatial proximity
566        let filtered_orbits = self.remove_duplicate_periodic_orbits(periodic_orbits);
567
568        Ok(filtered_orbits)
569    }
570
571    /// Use shooting method to find periodic orbits
572    fn shooting_method_periodic_orbits<F>(
573        &self,
574        system: &F,
575        domain: &[(f64, f64)],
576    ) -> IntegrateResult<Vec<PeriodicOrbit>>
577    where
578        F: Fn(&Array1<f64>) -> Array1<f64>,
579    {
580        let mut periodic_orbits = Vec::new();
581
582        if self.dimension != 2 {
583            return Ok(periodic_orbits); // Shooting method implementation for 2D systems only
584        }
585
586        // Generate initial guesses for periodic orbits
587        let n_guesses = 20;
588        let mut initial_points = Vec::new();
589        self.generate_grid_points(domain, n_guesses, &mut initial_points);
590
591        // Try different periods
592        let periods = vec![
593            std::f64::consts::PI,       // π
594            2.0 * std::f64::consts::PI, // 2π
595            std::f64::consts::PI / 2.0, // π/2
596            4.0 * std::f64::consts::PI, // 4π
597        ];
598
599        for initial_point in &initial_points {
600            for &period in &periods {
601                if let Ok(orbit) = self.shooting_method_single_orbit(system, initial_point, period)
602                {
603                    periodic_orbits.push(orbit);
604                }
605            }
606        }
607
608        Ok(periodic_orbits)
609    }
610
611    /// Single orbit detection using shooting method
612    fn shooting_method_single_orbit<F>(
613        &self,
614        system: &F,
615        initial_guess: &Array1<f64>,
616        period: f64,
617    ) -> IntegrateResult<PeriodicOrbit>
618    where
619        F: Fn(&Array1<f64>) -> Array1<f64>,
620    {
621        let max_iterations = 50;
622        let tolerance = 1e-8;
623        let dt = period / 100.0; // Integration step size
624
625        let mut current_guess = initial_guess.clone();
626
627        // Newton iteration for shooting method
628        for _iter in 0..max_iterations {
629            // Integrate forward for one period
630            let final_state =
631                self.integrate_trajectory_period(system, &current_guess, period, dt)?;
632
633            // Compute the shooting function: F(x0) = x(T) - x0
634            let shooting_residual = &final_state - &current_guess;
635            let residual_norm = shooting_residual.iter().map(|&x| x * x).sum::<f64>().sqrt();
636
637            if residual_norm < tolerance {
638                // Found a periodic orbit
639                let floquet_multipliers =
640                    self.compute_floquet_multipliers(system, &current_guess, period)?;
641                let stability = self.classify_periodic_orbit_stability(&floquet_multipliers);
642
643                return Ok(PeriodicOrbit {
644                    representative_point: current_guess,
645                    period,
646                    stability,
647                    floquet_multipliers,
648                });
649            }
650
651            // Compute Jacobian of the flow map
652            let flow_jacobian = self.compute_flow_jacobian(system, &current_guess, period, dt)?;
653
654            // Newton step: solve (∂F/∂x0) * Δx0 = -F(x0)
655            let identity = Array2::<f64>::eye(self.dimension);
656            let shooting_jacobian = &flow_jacobian - &identity;
657
658            // Solve the linear system
659            let newton_step =
660                self.solve_linear_system_for_shooting(&shooting_jacobian, &(-&shooting_residual))?;
661            current_guess += &newton_step;
662        }
663
664        Err(IntegrateError::ConvergenceError(
665            "Shooting method did not converge to periodic orbit".to_string(),
666        ))
667    }
668
669    /// Integrate trajectory for a specified period
670    fn integrate_trajectory_period<F>(
671        &self,
672        system: &F,
673        initial_state: &Array1<f64>,
674        period: f64,
675        dt: f64,
676    ) -> IntegrateResult<Array1<f64>>
677    where
678        F: Fn(&Array1<f64>) -> Array1<f64>,
679    {
680        let n_steps = (period / dt) as usize;
681        let mut state = initial_state.clone();
682
683        // Fourth-order Runge-Kutta integration
684        for _ in 0..n_steps {
685            let k1 = system(&state);
686            let k2 = system(&(&state + &(&k1 * (dt / 2.0))));
687            let k3 = system(&(&state + &(&k2 * (dt / 2.0))));
688            let k4 = system(&(&state + &(&k3 * dt)));
689
690            state += &((&k1 + &k2 * 2.0 + &k3 * 2.0 + &k4) * (dt / 6.0));
691        }
692
693        Ok(state)
694    }
695
696    /// Compute flow map Jacobian using finite differences
697    fn compute_flow_jacobian<F>(
698        &self,
699        system: &F,
700        initial_state: &Array1<f64>,
701        period: f64,
702        dt: f64,
703    ) -> IntegrateResult<Array2<f64>>
704    where
705        F: Fn(&Array1<f64>) -> Array1<f64>,
706    {
707        let h = 1e-8;
708        let n = initial_state.len();
709        let mut jacobian = Array2::<f64>::zeros((n, n));
710
711        // Base trajectory
712        let base_final = self.integrate_trajectory_period(system, initial_state, period, dt)?;
713
714        // Perturb each component and compute finite differences
715        for j in 0..n {
716            let mut perturbed_initial = initial_state.clone();
717            perturbed_initial[j] += h;
718
719            let perturbed_final =
720                self.integrate_trajectory_period(system, &perturbed_initial, period, dt)?;
721
722            for i in 0..n {
723                jacobian[[i, j]] = (perturbed_final[i] - base_final[i]) / h;
724            }
725        }
726
727        Ok(jacobian)
728    }
729
730    /// Compute Floquet multipliers for periodic orbit stability analysis
731    fn compute_floquet_multipliers<F>(
732        &self,
733        system: &F,
734        representative_point: &Array1<f64>,
735        period: f64,
736    ) -> IntegrateResult<Vec<Complex64>>
737    where
738        F: Fn(&Array1<f64>) -> Array1<f64>,
739    {
740        let dt = period / 100.0;
741        let flow_jacobian = self.compute_flow_jacobian(system, representative_point, period, dt)?;
742
743        // Compute eigenvalues of the flow map Jacobian (Floquet multipliers)
744        let multipliers = self.compute_real_eigenvalues(&flow_jacobian)?;
745
746        Ok(multipliers)
747    }
748
749    /// Classify periodic orbit stability based on Floquet multipliers
750    fn classify_periodic_orbit_stability(
751        &self,
752        floquet_multipliers: &[Complex64],
753    ) -> StabilityType {
754        // For periodic orbits, stability is determined by Floquet multipliers
755        // Stable if all multipliers have |λ| < 1
756        // Unstable if any multiplier has |λ| > 1
757
758        let max_magnitude = floquet_multipliers
759            .iter()
760            .map(|m| m.norm())
761            .fold(0.0, f64::max);
762
763        if max_magnitude < 1.0 - 1e-10 {
764            StabilityType::Stable
765        } else if max_magnitude > 1.0 + 1e-10 {
766            StabilityType::Unstable
767        } else {
768            // One or more multipliers on unit circle
769            let on_unit_circle = floquet_multipliers
770                .iter()
771                .any(|m| (m.norm() - 1.0).abs() < 1e-10);
772
773            if on_unit_circle {
774                StabilityType::Center
775            } else {
776                StabilityType::Degenerate
777            }
778        }
779    }
780
781    /// Return map analysis for periodic orbit detection
782    fn return_map_periodic_orbits<F>(
783        &self,
784        system: &F,
785        domain: &[(f64, f64)],
786    ) -> IntegrateResult<Vec<PeriodicOrbit>>
787    where
788        F: Fn(&Array1<f64>) -> Array1<f64>,
789    {
790        let mut periodic_orbits = Vec::new();
791
792        if self.dimension != 2 {
793            return Ok(periodic_orbits); // Return map analysis for 2D systems only
794        }
795
796        // Define a Poincaré section (e.g., x = 0)
797        let section_plane = Array1::from_vec(vec![1.0, 0.0]); // Normal to x-axis
798        let section_point = Array1::zeros(2); // Origin
799
800        // Generate several trajectories and find their intersections with the Poincaré section
801        let n_trajectories = 10;
802        let mut initial_points = Vec::new();
803        self.generate_grid_points(domain, n_trajectories, &mut initial_points);
804
805        for initial_point in &initial_points {
806            if let Ok(return_points) = self.compute_poincare_return_map(
807                system,
808                initial_point,
809                &section_plane,
810                &section_point,
811            ) {
812                // Analyze return points for periodicity
813                if let Ok(orbit) = self.analyze_return_map_for_periodicity(&return_points) {
814                    periodic_orbits.push(orbit);
815                }
816            }
817        }
818
819        Ok(periodic_orbits)
820    }
821
822    /// Compute Poincaré return map
823    fn compute_poincare_return_map<F>(
824        &self,
825        system: &F,
826        initial_point: &Array1<f64>,
827        section_normal: &Array1<f64>,
828        section_point: &Array1<f64>,
829    ) -> IntegrateResult<Vec<Array1<f64>>>
830    where
831        F: Fn(&Array1<f64>) -> Array1<f64>,
832    {
833        let mut return_points = Vec::new();
834        let dt = 0.01;
835        let max_time = 50.0;
836        let n_steps = (max_time / dt) as usize;
837
838        let mut state = initial_point.clone();
839        let mut prev_distance = self.distance_to_section(&state, section_normal, section_point);
840
841        for _ in 0..n_steps {
842            // Integrate one step
843            let derivative = system(&state);
844            state += &(derivative * dt);
845
846            // Check for section crossing
847            let curr_distance = self.distance_to_section(&state, section_normal, section_point);
848
849            if prev_distance * curr_distance < 0.0 {
850                // Crossed the section, refine the crossing point
851                if let Ok(crossing_point) =
852                    self.refine_section_crossing(system, &state, dt, section_normal, section_point)
853                {
854                    return_points.push(crossing_point);
855
856                    if return_points.len() > 20 {
857                        break; // Collect enough return points
858                    }
859                }
860            }
861
862            prev_distance = curr_distance;
863        }
864
865        Ok(return_points)
866    }
867
868    /// Distance from point to Poincaré section
869    fn distance_to_section(
870        &self,
871        point: &Array1<f64>,
872        section_normal: &Array1<f64>,
873        section_point: &Array1<f64>,
874    ) -> f64 {
875        let relative_pos = point - section_point;
876        relative_pos.dot(section_normal)
877    }
878
879    /// Refine section crossing using bisection
880    fn refine_section_crossing<F>(
881        &self,
882        system: &F,
883        current_state: &Array1<f64>,
884        dt: f64,
885        section_normal: &Array1<f64>,
886        section_point: &Array1<f64>,
887    ) -> IntegrateResult<Array1<f64>>
888    where
889        F: Fn(&Array1<f64>) -> Array1<f64>,
890    {
891        // Simple bisection refinement
892        let derivative = system(current_state);
893        let prev_state = current_state - &(derivative * dt);
894
895        let mut left = prev_state;
896        let mut right = current_state.clone();
897
898        for _ in 0..10 {
899            let mid = (&left + &right) * 0.5;
900            let mid_distance = self.distance_to_section(&mid, section_normal, section_point);
901
902            if mid_distance.abs() < 1e-10 {
903                return Ok(mid);
904            }
905
906            let left_distance = self.distance_to_section(&left, section_normal, section_point);
907
908            if left_distance * mid_distance < 0.0 {
909                right = mid;
910            } else {
911                left = mid;
912            }
913        }
914
915        Ok((&left + &right) * 0.5)
916    }
917
918    /// Analyze return map for periodicity
919    fn analyze_return_map_for_periodicity(
920        &self,
921        return_points: &[Array1<f64>],
922    ) -> IntegrateResult<PeriodicOrbit> {
923        if return_points.len() < 3 {
924            return Err(IntegrateError::ComputationError(
925                "Insufficient return points for periodicity analysis".to_string(),
926            ));
927        }
928
929        let tolerance = 1e-6;
930
931        // Look for approximate returns
932        for period in 1..std::cmp::min(return_points.len() / 2, 10) {
933            let mut is_periodic = true;
934            let mut max_error: f64 = 0.0;
935
936            for i in 0..(return_points.len() - period) {
937                let error = (&return_points[i] - &return_points[i + period])
938                    .iter()
939                    .map(|&x| x * x)
940                    .sum::<f64>()
941                    .sqrt();
942
943                max_error = max_error.max(error);
944
945                if error > tolerance {
946                    is_periodic = false;
947                    break;
948                }
949            }
950
951            if is_periodic {
952                // Estimate the period in time (rough approximation)
953                let estimated_period = period as f64 * 2.0 * std::f64::consts::PI;
954
955                return Ok(PeriodicOrbit {
956                    representative_point: return_points[0].clone(),
957                    period: estimated_period,
958                    stability: StabilityType::Stable, // Would need proper analysis
959                    floquet_multipliers: vec![],      // Would need computation
960                });
961            }
962        }
963
964        Err(IntegrateError::ComputationError(
965            "No periodic behavior detected in return map".to_string(),
966        ))
967    }
968
969    /// Fourier analysis for periodic orbit detection
970    fn fourier_analysis_periodic_orbits<F>(
971        &self,
972        system: &F,
973        domain: &[(f64, f64)],
974    ) -> IntegrateResult<Vec<PeriodicOrbit>>
975    where
976        F: Fn(&Array1<f64>) -> Array1<f64>,
977    {
978        let mut periodic_orbits = Vec::new();
979
980        // Generate initial points
981        let n_trajectories = 5;
982        let mut initial_points = Vec::new();
983        self.generate_grid_points(domain, n_trajectories, &mut initial_points);
984
985        for initial_point in &initial_points {
986            if let Ok(orbit) = self.fourier_analysis_single_trajectory(system, initial_point) {
987                periodic_orbits.push(orbit);
988            }
989        }
990
991        Ok(periodic_orbits)
992    }
993
994    /// Fourier analysis of a single trajectory
995    fn fourier_analysis_single_trajectory<F>(
996        &self,
997        system: &F,
998        initial_point: &Array1<f64>,
999    ) -> IntegrateResult<PeriodicOrbit>
1000    where
1001        F: Fn(&Array1<f64>) -> Array1<f64>,
1002    {
1003        let dt = 0.01;
1004        let total_time = 20.0;
1005        let n_steps = (total_time / dt) as usize;
1006
1007        // Integrate trajectory
1008        let mut trajectory = Vec::new();
1009        let mut state = initial_point.clone();
1010
1011        for _ in 0..n_steps {
1012            trajectory.push(state.clone());
1013            let derivative = system(&state);
1014            state += &(derivative * dt);
1015        }
1016
1017        // Simple frequency analysis (detect dominant frequency)
1018        if let Ok(dominant_period) = self.detect_dominant_period(&trajectory, dt) {
1019            if dominant_period > 0.0 && dominant_period < total_time {
1020                return Ok(PeriodicOrbit {
1021                    representative_point: initial_point.clone(),
1022                    period: dominant_period,
1023                    stability: StabilityType::Stable, // Would need proper analysis
1024                    floquet_multipliers: vec![],      // Would need computation
1025                });
1026            }
1027        }
1028
1029        Err(IntegrateError::ComputationError(
1030            "No periodic behavior detected via Fourier analysis".to_string(),
1031        ))
1032    }
1033
1034    /// Detect dominant period using autocorrelation
1035    fn detect_dominant_period(&self, trajectory: &[Array1<f64>], dt: f64) -> IntegrateResult<f64> {
1036        if trajectory.len() < 100 {
1037            return Err(IntegrateError::ComputationError(
1038                "Trajectory too short for period detection".to_string(),
1039            ));
1040        }
1041
1042        // Use first component for period detection
1043        let signal: Vec<f64> = trajectory.iter().map(|state| state[0]).collect();
1044
1045        // Autocorrelation approach
1046        let max_lag = std::cmp::min(signal.len() / 4, 500);
1047        let mut autocorr = vec![0.0; max_lag];
1048
1049        let mean = signal.iter().sum::<f64>() / signal.len() as f64;
1050        let variance =
1051            signal.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / signal.len() as f64;
1052
1053        if variance < 1e-12 {
1054            return Err(IntegrateError::ComputationError(
1055                "Signal has zero variance".to_string(),
1056            ));
1057        }
1058
1059        for lag in 1..max_lag {
1060            let mut correlation = 0.0;
1061            let count = signal.len() - lag;
1062
1063            for i in 0..count {
1064                correlation += (signal[i] - mean) * (signal[i + lag] - mean);
1065            }
1066
1067            autocorr[lag] = correlation / (count as f64 * variance);
1068        }
1069
1070        // Find the first significant peak after lag = 0
1071        let mut max_corr = 0.0;
1072        let mut period_lag = 0;
1073
1074        for lag in 10..max_lag {
1075            if autocorr[lag] > max_corr && autocorr[lag] > 0.5 {
1076                // Check if this is a local maximum
1077                if lag > 0
1078                    && lag < max_lag - 1
1079                    && autocorr[lag] > autocorr[lag - 1]
1080                    && autocorr[lag] > autocorr[lag + 1]
1081                {
1082                    max_corr = autocorr[lag];
1083                    period_lag = lag;
1084                }
1085            }
1086        }
1087
1088        if period_lag > 0 {
1089            Ok(period_lag as f64 * dt)
1090        } else {
1091            Err(IntegrateError::ComputationError(
1092                "No dominant period detected".to_string(),
1093            ))
1094        }
1095    }
1096
1097    /// Remove duplicate periodic orbits based on spatial proximity
1098    fn remove_duplicate_periodic_orbits(&self, orbits: Vec<PeriodicOrbit>) -> Vec<PeriodicOrbit> {
1099        let mut filtered: Vec<PeriodicOrbit> = Vec::new();
1100        let tolerance = 1e-4;
1101
1102        for orbit in orbits {
1103            let mut is_duplicate = false;
1104
1105            for existing in &filtered {
1106                let distance = (&orbit.representative_point - &existing.representative_point)
1107                    .iter()
1108                    .map(|&x| x * x)
1109                    .sum::<f64>()
1110                    .sqrt();
1111
1112                let period_diff = (orbit.period - existing.period).abs();
1113
1114                if distance < tolerance && period_diff < tolerance {
1115                    is_duplicate = true;
1116                    break;
1117                }
1118            }
1119
1120            if !is_duplicate {
1121                filtered.push(orbit);
1122            }
1123        }
1124
1125        filtered
1126    }
1127
1128    /// Solve linear system for shooting method
1129    fn solve_linear_system_for_shooting(
1130        &self,
1131        matrix: &Array2<f64>,
1132        rhs: &Array1<f64>,
1133    ) -> IntegrateResult<Array1<f64>> {
1134        let n = matrix.nrows();
1135        if n != matrix.ncols() || n != rhs.len() {
1136            return Err(IntegrateError::ComputationError(
1137                "Inconsistent matrix dimensions in shooting method".to_string(),
1138            ));
1139        }
1140
1141        let mut augmented = Array2::<f64>::zeros((n, n + 1));
1142        for i in 0..n {
1143            for j in 0..n {
1144                augmented[[i, j]] = matrix[[i, j]];
1145            }
1146            augmented[[i, n]] = rhs[i];
1147        }
1148
1149        // Gaussian elimination
1150        for k in 0..n {
1151            // Find pivot
1152            let mut max_row = k;
1153            for i in (k + 1)..n {
1154                if augmented[[i, k]].abs() > augmented[[max_row, k]].abs() {
1155                    max_row = i;
1156                }
1157            }
1158
1159            // Swap rows
1160            if max_row != k {
1161                for j in k..=n {
1162                    let temp = augmented[[k, j]];
1163                    augmented[[k, j]] = augmented[[max_row, j]];
1164                    augmented[[max_row, j]] = temp;
1165                }
1166            }
1167
1168            // Check for singularity
1169            if augmented[[k, k]].abs() < 1e-14 {
1170                return Err(IntegrateError::ComputationError(
1171                    "Singular matrix in shooting method".to_string(),
1172                ));
1173            }
1174
1175            // Eliminate
1176            for i in (k + 1)..n {
1177                let factor = augmented[[i, k]] / augmented[[k, k]];
1178                for j in k..=n {
1179                    augmented[[i, j]] -= factor * augmented[[k, j]];
1180                }
1181            }
1182        }
1183
1184        // Back substitution
1185        let mut solution = Array1::<f64>::zeros(n);
1186        for i in (0..n).rev() {
1187            solution[i] = augmented[[i, n]];
1188            for j in (i + 1)..n {
1189                solution[i] -= augmented[[i, j]] * solution[j];
1190            }
1191            solution[i] /= augmented[[i, i]];
1192        }
1193
1194        Ok(solution)
1195    }
1196
1197    /// Compute Lyapunov exponents
1198    fn compute_lyapunov_exponents<F>(&self, system: &F) -> IntegrateResult<Option<Array1<f64>>>
1199    where
1200        F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync + Clone,
1201    {
1202        // For systems with dimension 1-10, compute Lyapunov exponents
1203        if self.dimension == 0 || self.dimension > 10 {
1204            return Ok(None);
1205        }
1206
1207        // Create initial state in the center of the domain
1208        // (we'd ideally use an attractor, but this is a reasonable default)
1209        let initial_state = Array1::zeros(self.dimension);
1210
1211        // Use adaptive time step based on system dimension
1212        let dt = match self.dimension {
1213            1 => 0.01,
1214            2 => 0.005,
1215            3 => 0.002,
1216            4..=6 => 0.001,
1217            _ => 0.0005,
1218        };
1219
1220        // Calculate number of exponents to compute (typically all for small systems)
1221        let n_exponents = if self.dimension <= 4 {
1222            self.dimension
1223        } else {
1224            // For higher dimensions, compute only the largest few exponents
1225            std::cmp::min(4, self.dimension)
1226        };
1227
1228        let calculator = advanced::LyapunovCalculator::new(n_exponents, dt);
1229
1230        // Use integration time that scales with system complexity
1231        let integration_time = self.integration_time * (self.dimension as f64).sqrt();
1232
1233        // Clone the system function to satisfy trait bounds
1234        let system_clone = system.clone();
1235        let system_wrapper = move |state: &Array1<f64>| system_clone(state);
1236
1237        match calculator.calculate_lyapunov_exponents(
1238            system_wrapper,
1239            &initial_state,
1240            integration_time,
1241        ) {
1242            Ok(exponents) => {
1243                // Filter out numerical artifacts (very small exponents close to machine precision)
1244                let filtered_exponents = exponents.mapv(|x| if x.abs() < 1e-12 { 0.0 } else { x });
1245                Ok(Some(filtered_exponents))
1246            }
1247            Err(e) => {
1248                // If Lyapunov computation fails, it's not critical - return None
1249                eprintln!("Warning: Lyapunov exponent computation failed: {e:?}");
1250                Ok(None)
1251            }
1252        }
1253    }
1254
1255    /// Analyze basins of attraction for 2D systems
1256    fn analyze_basins<F>(
1257        &self,
1258        system: &F,
1259        domain: &[(f64, f64)],
1260        fixed_points: &[FixedPoint],
1261    ) -> IntegrateResult<BasinAnalysis>
1262    where
1263        F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync,
1264    {
1265        if self.dimension != 2 || domain.len() != 2 {
1266            return Err(IntegrateError::ValueError(
1267                "Basin analysis only implemented for 2D systems".to_string(),
1268            ));
1269        }
1270
1271        let grid_size = self.basin_grid_size;
1272        let mut grid_points = Array2::zeros((grid_size * grid_size, 2));
1273        let mut attractor_indices = Array2::<i32>::zeros((grid_size, grid_size));
1274
1275        let dx = (domain[0].1 - domain[0].0) / (grid_size - 1) as f64;
1276        let dy = (domain[1].1 - domain[1].0) / (grid_size - 1) as f64;
1277
1278        // Generate grid and integrate each point
1279        for i in 0..grid_size {
1280            for j in 0..grid_size {
1281                let x = domain[0].0 + i as f64 * dx;
1282                let y = domain[1].0 + j as f64 * dy;
1283
1284                grid_points[[i * grid_size + j, 0]] = x;
1285                grid_points[[i * grid_size + j, 1]] = y;
1286
1287                // Integrate trajectory and find which attractor it converges to
1288                let initial_state = Array1::from_vec(vec![x, y]);
1289                let final_state = self.integrate_trajectory(system, &initial_state)?;
1290
1291                // Find closest fixed point
1292                let mut closest_attractor = -1;
1293                let mut min_distance = f64::INFINITY;
1294
1295                for (idx, fp) in fixed_points.iter().enumerate() {
1296                    if fp.stability == StabilityType::Stable {
1297                        let distance = (&final_state - &fp.location)
1298                            .iter()
1299                            .map(|&x| x * x)
1300                            .sum::<f64>()
1301                            .sqrt();
1302
1303                        if distance < min_distance && distance < 0.1 {
1304                            min_distance = distance;
1305                            closest_attractor = idx as i32;
1306                        }
1307                    }
1308                }
1309
1310                attractor_indices[[i, j]] = closest_attractor;
1311            }
1312        }
1313
1314        // Extract stable attractors
1315        let attractors = fixed_points
1316            .iter()
1317            .filter(|fp| fp.stability == StabilityType::Stable)
1318            .map(|fp| fp.location.clone())
1319            .collect();
1320
1321        Ok(BasinAnalysis {
1322            grid_points,
1323            attractor_indices,
1324            attractors,
1325        })
1326    }
1327
1328    /// Integrate trajectory to find final state
1329    fn integrate_trajectory<F>(
1330        &self,
1331        system: &F,
1332        initial_state: &Array1<f64>,
1333    ) -> IntegrateResult<Array1<f64>>
1334    where
1335        F: Fn(&Array1<f64>) -> Array1<f64>,
1336    {
1337        // Simple Euler integration
1338        let dt = 0.01;
1339        let n_steps = (self.integration_time / dt) as usize;
1340        let mut state = initial_state.clone();
1341
1342        for _ in 0..n_steps {
1343            let derivative = system(&state);
1344            state += &(derivative * dt);
1345        }
1346
1347        Ok(state)
1348    }
1349}