use crate::analysis::advanced;
use crate::analysis::types::*;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Complex64;
pub struct StabilityAnalyzer {
pub dimension: usize,
pub tolerance: f64,
pub integration_time: f64,
pub basin_grid_size: usize,
}
impl StabilityAnalyzer {
pub fn new(dimension: usize) -> Self {
Self {
dimension,
tolerance: 1e-8,
integration_time: 100.0,
basin_grid_size: 50,
}
}
pub fn analyze_stability<F>(
&self,
system: F,
domain: &[(f64, f64)],
) -> IntegrateResult<StabilityResult>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync + Clone + 'static,
{
let fixed_points = self.find_fixed_points(&system, domain)?;
let periodic_orbits = self.find_periodic_orbits(&system, domain)?;
let lyapunov_exponents = self.compute_lyapunov_exponents(&system)?;
let basin_analysis = if self.dimension == 2 {
Some(self.analyze_basins(&system, domain, &fixed_points)?)
} else {
None
};
Ok(StabilityResult {
fixed_points,
periodic_orbits,
lyapunov_exponents,
basin_analysis,
})
}
fn find_fixed_points<F>(
&self,
system: &F,
domain: &[(f64, f64)],
) -> IntegrateResult<Vec<FixedPoint>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut fixed_points: Vec<FixedPoint> = Vec::new();
let grid_size = 10;
let mut initial_guesses = Vec::new();
self.generate_grid_points(domain, grid_size, &mut initial_guesses);
for guess in initial_guesses {
if let Ok(fixed_point) = self.newton_raphson_fixed_point(system, &guess) {
let mut is_duplicate = false;
for existing_fp in &fixed_points {
let distance = (&fixed_point - &existing_fp.location)
.iter()
.map(|&x| x * x)
.sum::<f64>()
.sqrt();
if distance < self.tolerance * 10.0 {
is_duplicate = true;
break;
}
}
if !is_duplicate {
let jacobian = self.compute_jacobian_at_point(system, &fixed_point)?;
let eigenvalues = self.compute_real_eigenvalues(&jacobian)?;
let eigenvectors = self.compute_eigenvectors(&jacobian, &eigenvalues)?;
let stability = self.classify_stability(&eigenvalues);
fixed_points.push(FixedPoint {
location: fixed_point,
stability,
eigenvalues,
eigenvectors,
});
}
}
}
Ok(fixed_points)
}
fn generate_grid_points(
&self,
domain: &[(f64, f64)],
grid_size: usize,
points: &mut Vec<Array1<f64>>,
) {
fn generate_recursive(
domain: &[(f64, f64)],
grid_size: usize,
current: &mut Vec<f64>,
dim: usize,
points: &mut Vec<Array1<f64>>,
) {
if dim == domain.len() {
points.push(Array1::from_vec(current.clone()));
return;
}
if grid_size <= 1 {
return; }
let step = (domain[dim].1 - domain[dim].0) / (grid_size - 1) as f64;
for i in 0..grid_size {
let value = domain[dim].0 + i as f64 * step;
current.push(value);
generate_recursive(domain, grid_size, current, dim + 1, points);
current.pop();
}
}
let mut current = Vec::new();
generate_recursive(domain, grid_size, &mut current, 0, points);
}
fn newton_raphson_fixed_point<F>(
&self,
system: &F,
initial_guess: &Array1<f64>,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut x = initial_guess.clone();
let max_iterations = 100;
for _ in 0..max_iterations {
let f_x = system(&x);
let sum_squares = f_x.iter().map(|&v| v * v).sum::<f64>();
if sum_squares < 0.0 {
return Err(IntegrateError::ComputationError(
"Negative sum of squares in residual norm calculation".to_string(),
));
}
let residual_norm = sum_squares.sqrt();
if residual_norm < self.tolerance {
return Ok(x);
}
let jacobian = self.compute_jacobian_at_point(system, &x)?;
let mut augmented = Array2::zeros((self.dimension, self.dimension + 1));
for i in 0..self.dimension {
for j in 0..self.dimension {
augmented[[i, j]] = jacobian[[i, j]];
}
augmented[[i, self.dimension]] = -f_x[i];
}
let dx = self.gaussian_elimination(&augmented)?;
x += &dx;
}
Err(IntegrateError::ConvergenceError(
"Newton-Raphson did not converge".to_string(),
))
}
fn compute_jacobian_at_point<F>(
&self,
system: &F,
x: &Array1<f64>,
) -> IntegrateResult<Array2<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let h = 1e-8_f64;
let n = x.len();
let mut jacobian = Array2::zeros((n, n));
let f0 = system(x);
if h.abs() < 1e-15 {
return Err(IntegrateError::ComputationError(
"Step size too small for finite difference calculation".to_string(),
));
}
for j in 0..n {
let mut x_plus = x.clone();
x_plus[j] += h;
let f_plus = system(&x_plus);
for i in 0..n {
jacobian[[i, j]] = (f_plus[i] - f0[i]) / h;
}
}
Ok(jacobian)
}
fn gaussian_elimination(&self, augmented: &Array2<f64>) -> IntegrateResult<Array1<f64>> {
let n = augmented.nrows();
let mut a = augmented.clone();
for k in 0..n {
let mut max_row = k;
for i in k + 1..n {
if a[[i, k]].abs() > a[[max_row, k]].abs() {
max_row = i;
}
}
if max_row != k {
for j in k..n + 1 {
let temp = a[[k, j]];
a[[k, j]] = a[[max_row, j]];
a[[max_row, j]] = temp;
}
}
if a[[k, k]].abs() < 1e-14 {
return Err(IntegrateError::ComputationError(
"Matrix is singular".to_string(),
));
}
for i in k + 1..n {
let factor = a[[i, k]] / a[[k, k]];
for j in k..n + 1 {
a[[i, j]] -= factor * a[[k, j]];
}
}
}
let mut x = Array1::zeros(n);
for i in (0..n).rev() {
x[i] = a[[i, n]];
for j in i + 1..n {
x[i] -= a[[i, j]] * x[j];
}
if a[[i, i]].abs() < 1e-14 {
return Err(IntegrateError::ComputationError(
"Zero diagonal element in back substitution".to_string(),
));
}
x[i] /= a[[i, i]];
}
Ok(x)
}
fn compute_real_eigenvalues(&self, matrix: &Array2<f64>) -> IntegrateResult<Vec<Complex64>> {
let n = matrix.nrows();
if n == 2 {
let a = matrix[[0, 0]];
let b = matrix[[0, 1]];
let c = matrix[[1, 0]];
let d = matrix[[1, 1]];
let trace = a + d;
let det = a * d - b * c;
let discriminant = trace * trace - 4.0 * det;
if discriminant >= 0.0 {
let sqrt_disc = discriminant.sqrt();
let lambda1 = (trace + sqrt_disc) / 2.0;
let lambda2 = (trace - sqrt_disc) / 2.0;
Ok(vec![
Complex64::new(lambda1, 0.0),
Complex64::new(lambda2, 0.0),
])
} else {
let real_part = trace / 2.0;
let imag_part = (-discriminant).sqrt() / 2.0;
Ok(vec![
Complex64::new(real_part, imag_part),
Complex64::new(real_part, -imag_part),
])
}
} else {
self.eigenvalues_qr_algorithm(matrix)
}
}
fn eigenvalues_qr_algorithm(&self, matrix: &Array2<f64>) -> IntegrateResult<Vec<Complex64>> {
let n = matrix.nrows();
let mut a = matrix.clone();
let max_iterations = 100;
let tolerance = 1e-10;
a = self.reduce_to_hessenberg(&a)?;
for _ in 0..max_iterations {
let (q, r) = self.qr_decomposition_real(&a)?;
a = r.dot(&q);
let mut converged = true;
for i in 1..n {
if a[[i, i - 1]].abs() > tolerance {
converged = false;
break;
}
}
if converged {
break;
}
}
let mut eigenvalues = Vec::new();
let mut i = 0;
while i < n {
if i == n - 1 || a[[i + 1, i]].abs() < tolerance {
eigenvalues.push(Complex64::new(a[[i, i]], 0.0));
i += 1;
} else {
let trace = a[[i, i]] + a[[i + 1, i + 1]];
let det = a[[i, i]] * a[[i + 1, i + 1]] - a[[i, i + 1]] * a[[i + 1, i]];
let discriminant = trace * trace - 4.0 * det;
if discriminant >= 0.0 {
let sqrt_disc = discriminant.sqrt();
eigenvalues.push(Complex64::new((trace + sqrt_disc) / 2.0, 0.0));
eigenvalues.push(Complex64::new((trace - sqrt_disc) / 2.0, 0.0));
} else {
let real_part = trace / 2.0;
let imag_part = (-discriminant).sqrt() / 2.0;
eigenvalues.push(Complex64::new(real_part, imag_part));
eigenvalues.push(Complex64::new(real_part, -imag_part));
}
i += 2;
}
}
Ok(eigenvalues)
}
fn reduce_to_hessenberg(&self, matrix: &Array2<f64>) -> IntegrateResult<Array2<f64>> {
let n = matrix.nrows();
let mut h = matrix.clone();
for k in 0..(n - 2) {
let mut x = Array1::<f64>::zeros(n - k - 1);
for i in 0..(n - k - 1) {
x[i] = h[[k + 1 + i, k]];
}
if x.iter().map(|&v| v * v).sum::<f64>().sqrt() > 1e-15 {
let alpha = if x[0] >= 0.0 {
-x.iter().map(|&v| v * v).sum::<f64>().sqrt()
} else {
x.iter().map(|&v| v * v).sum::<f64>().sqrt()
};
let mut v = x.clone();
v[0] -= alpha;
let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
if v_norm > 1e-15 {
v.mapv_inplace(|vi| vi / v_norm);
for j in k..n {
let dot_product: f64 =
(0..(n - k - 1)).map(|i| v[i] * h[[k + 1 + i, j]]).sum();
for i in 0..(n - k - 1) {
h[[k + 1 + i, j]] -= 2.0 * v[i] * dot_product;
}
}
for i in 0..n {
let dot_product: f64 =
(0..(n - k - 1)).map(|j| h[[i, k + 1 + j]] * v[j]).sum();
for j in 0..(n - k - 1) {
h[[i, k + 1 + j]] -= 2.0 * v[j] * dot_product;
}
}
}
}
}
Ok(h)
}
fn qr_decomposition_real(
&self,
matrix: &Array2<f64>,
) -> IntegrateResult<(Array2<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
let mut q = Array2::<f64>::eye(m);
let mut r = matrix.clone();
for k in 0..std::cmp::min(m - 1, n) {
let mut x = Array1::<f64>::zeros(m - k);
for i in 0..(m - k) {
x[i] = r[[k + i, k]];
}
let norm_x = x.iter().map(|&v| v * v).sum::<f64>().sqrt();
if norm_x > 1e-15 {
let alpha = if x[0] >= 0.0 { -norm_x } else { norm_x };
let mut v = x.clone();
v[0] -= alpha;
let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
if v_norm > 1e-15 {
v.mapv_inplace(|vi| vi / v_norm);
for j in k..n {
let dot_product: f64 = (0..(m - k)).map(|i| v[i] * r[[k + i, j]]).sum();
for i in 0..(m - k) {
r[[k + i, j]] -= 2.0 * v[i] * dot_product;
}
}
for i in 0..m {
let dot_product: f64 = (0..(m - k)).map(|j| q[[i, k + j]] * v[j]).sum();
for j in 0..(m - k) {
q[[i, k + j]] -= 2.0 * v[j] * dot_product;
}
}
}
}
}
Ok((q, r))
}
fn compute_eigenvectors(
&self,
_matrix: &Array2<f64>,
eigenvalues: &[Complex64],
) -> IntegrateResult<Array2<Complex64>> {
let n = eigenvalues.len();
let eigenvectors = Array2::<Complex64>::zeros((n, n));
Ok(eigenvectors)
}
fn classify_stability(&self, eigenvalues: &[Complex64]) -> StabilityType {
let mut has_positive_real = false;
let mut has_negative_real = false;
let mut has_zero_real = false;
for eigenvalue in eigenvalues {
if eigenvalue.re > 1e-10 {
has_positive_real = true;
} else if eigenvalue.re < -1e-10 {
has_negative_real = true;
} else {
has_zero_real = true;
}
}
if has_zero_real {
StabilityType::Degenerate
} else if has_positive_real && has_negative_real {
StabilityType::Saddle
} else if has_positive_real {
StabilityType::Unstable
} else if has_negative_real {
StabilityType::Stable
} else {
StabilityType::Center
}
}
fn find_periodic_orbits<F>(
&self,
system: &F,
domain: &[(f64, f64)],
) -> IntegrateResult<Vec<PeriodicOrbit>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut periodic_orbits = Vec::new();
if let Ok(shooting_orbits) = self.shooting_method_periodic_orbits(system, domain) {
periodic_orbits.extend(shooting_orbits);
}
if let Ok(return_map_orbits) = self.return_map_periodic_orbits(system, domain) {
periodic_orbits.extend(return_map_orbits);
}
if let Ok(fourier_orbits) = self.fourier_analysis_periodic_orbits(system, domain) {
periodic_orbits.extend(fourier_orbits);
}
let filtered_orbits = self.remove_duplicate_periodic_orbits(periodic_orbits);
Ok(filtered_orbits)
}
fn shooting_method_periodic_orbits<F>(
&self,
system: &F,
domain: &[(f64, f64)],
) -> IntegrateResult<Vec<PeriodicOrbit>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut periodic_orbits = Vec::new();
if self.dimension != 2 {
return Ok(periodic_orbits); }
let n_guesses = 20;
let mut initial_points = Vec::new();
self.generate_grid_points(domain, n_guesses, &mut initial_points);
let periods = vec![
std::f64::consts::PI, 2.0 * std::f64::consts::PI, std::f64::consts::PI / 2.0, 4.0 * std::f64::consts::PI, ];
for initial_point in &initial_points {
for &period in &periods {
if let Ok(orbit) = self.shooting_method_single_orbit(system, initial_point, period)
{
periodic_orbits.push(orbit);
}
}
}
Ok(periodic_orbits)
}
fn shooting_method_single_orbit<F>(
&self,
system: &F,
initial_guess: &Array1<f64>,
period: f64,
) -> IntegrateResult<PeriodicOrbit>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let max_iterations = 50;
let tolerance = 1e-8;
let dt = period / 100.0;
let mut current_guess = initial_guess.clone();
for _iter in 0..max_iterations {
let final_state =
self.integrate_trajectory_period(system, ¤t_guess, period, dt)?;
let shooting_residual = &final_state - ¤t_guess;
let residual_norm = shooting_residual.iter().map(|&x| x * x).sum::<f64>().sqrt();
if residual_norm < tolerance {
let floquet_multipliers =
self.compute_floquet_multipliers(system, ¤t_guess, period)?;
let stability = self.classify_periodic_orbit_stability(&floquet_multipliers);
return Ok(PeriodicOrbit {
representative_point: current_guess,
period,
stability,
floquet_multipliers,
});
}
let flow_jacobian = self.compute_flow_jacobian(system, ¤t_guess, period, dt)?;
let identity = Array2::<f64>::eye(self.dimension);
let shooting_jacobian = &flow_jacobian - &identity;
let newton_step =
self.solve_linear_system_for_shooting(&shooting_jacobian, &(-&shooting_residual))?;
current_guess += &newton_step;
}
Err(IntegrateError::ConvergenceError(
"Shooting method did not converge to periodic orbit".to_string(),
))
}
fn integrate_trajectory_period<F>(
&self,
system: &F,
initial_state: &Array1<f64>,
period: f64,
dt: f64,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let n_steps = (period / dt) as usize;
let mut state = initial_state.clone();
for _ in 0..n_steps {
let k1 = system(&state);
let k2 = system(&(&state + &(&k1 * (dt / 2.0))));
let k3 = system(&(&state + &(&k2 * (dt / 2.0))));
let k4 = system(&(&state + &(&k3 * dt)));
state += &((&k1 + &k2 * 2.0 + &k3 * 2.0 + &k4) * (dt / 6.0));
}
Ok(state)
}
fn compute_flow_jacobian<F>(
&self,
system: &F,
initial_state: &Array1<f64>,
period: f64,
dt: f64,
) -> IntegrateResult<Array2<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let h = 1e-8;
let n = initial_state.len();
let mut jacobian = Array2::<f64>::zeros((n, n));
let base_final = self.integrate_trajectory_period(system, initial_state, period, dt)?;
for j in 0..n {
let mut perturbed_initial = initial_state.clone();
perturbed_initial[j] += h;
let perturbed_final =
self.integrate_trajectory_period(system, &perturbed_initial, period, dt)?;
for i in 0..n {
jacobian[[i, j]] = (perturbed_final[i] - base_final[i]) / h;
}
}
Ok(jacobian)
}
fn compute_floquet_multipliers<F>(
&self,
system: &F,
representative_point: &Array1<f64>,
period: f64,
) -> IntegrateResult<Vec<Complex64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let dt = period / 100.0;
let flow_jacobian = self.compute_flow_jacobian(system, representative_point, period, dt)?;
let multipliers = self.compute_real_eigenvalues(&flow_jacobian)?;
Ok(multipliers)
}
fn classify_periodic_orbit_stability(
&self,
floquet_multipliers: &[Complex64],
) -> StabilityType {
let max_magnitude = floquet_multipliers
.iter()
.map(|m| m.norm())
.fold(0.0, f64::max);
if max_magnitude < 1.0 - 1e-10 {
StabilityType::Stable
} else if max_magnitude > 1.0 + 1e-10 {
StabilityType::Unstable
} else {
let on_unit_circle = floquet_multipliers
.iter()
.any(|m| (m.norm() - 1.0).abs() < 1e-10);
if on_unit_circle {
StabilityType::Center
} else {
StabilityType::Degenerate
}
}
}
fn return_map_periodic_orbits<F>(
&self,
system: &F,
domain: &[(f64, f64)],
) -> IntegrateResult<Vec<PeriodicOrbit>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut periodic_orbits = Vec::new();
if self.dimension != 2 {
return Ok(periodic_orbits); }
let section_plane = Array1::from_vec(vec![1.0, 0.0]); let section_point = Array1::zeros(2);
let n_trajectories = 10;
let mut initial_points = Vec::new();
self.generate_grid_points(domain, n_trajectories, &mut initial_points);
for initial_point in &initial_points {
if let Ok(return_points) = self.compute_poincare_return_map(
system,
initial_point,
§ion_plane,
§ion_point,
) {
if let Ok(orbit) = self.analyze_return_map_for_periodicity(&return_points) {
periodic_orbits.push(orbit);
}
}
}
Ok(periodic_orbits)
}
fn compute_poincare_return_map<F>(
&self,
system: &F,
initial_point: &Array1<f64>,
section_normal: &Array1<f64>,
section_point: &Array1<f64>,
) -> IntegrateResult<Vec<Array1<f64>>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut return_points = Vec::new();
let dt = 0.01;
let max_time = 50.0;
let n_steps = (max_time / dt) as usize;
let mut state = initial_point.clone();
let mut prev_distance = self.distance_to_section(&state, section_normal, section_point);
for _ in 0..n_steps {
let derivative = system(&state);
state += &(derivative * dt);
let curr_distance = self.distance_to_section(&state, section_normal, section_point);
if prev_distance * curr_distance < 0.0 {
if let Ok(crossing_point) =
self.refine_section_crossing(system, &state, dt, section_normal, section_point)
{
return_points.push(crossing_point);
if return_points.len() > 20 {
break; }
}
}
prev_distance = curr_distance;
}
Ok(return_points)
}
fn distance_to_section(
&self,
point: &Array1<f64>,
section_normal: &Array1<f64>,
section_point: &Array1<f64>,
) -> f64 {
let relative_pos = point - section_point;
relative_pos.dot(section_normal)
}
fn refine_section_crossing<F>(
&self,
system: &F,
current_state: &Array1<f64>,
dt: f64,
section_normal: &Array1<f64>,
section_point: &Array1<f64>,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let derivative = system(current_state);
let prev_state = current_state - &(derivative * dt);
let mut left = prev_state;
let mut right = current_state.clone();
for _ in 0..10 {
let mid = (&left + &right) * 0.5;
let mid_distance = self.distance_to_section(&mid, section_normal, section_point);
if mid_distance.abs() < 1e-10 {
return Ok(mid);
}
let left_distance = self.distance_to_section(&left, section_normal, section_point);
if left_distance * mid_distance < 0.0 {
right = mid;
} else {
left = mid;
}
}
Ok((&left + &right) * 0.5)
}
fn analyze_return_map_for_periodicity(
&self,
return_points: &[Array1<f64>],
) -> IntegrateResult<PeriodicOrbit> {
if return_points.len() < 3 {
return Err(IntegrateError::ComputationError(
"Insufficient return points for periodicity analysis".to_string(),
));
}
let tolerance = 1e-6;
for period in 1..std::cmp::min(return_points.len() / 2, 10) {
let mut is_periodic = true;
let mut max_error: f64 = 0.0;
for i in 0..(return_points.len() - period) {
let error = (&return_points[i] - &return_points[i + period])
.iter()
.map(|&x| x * x)
.sum::<f64>()
.sqrt();
max_error = max_error.max(error);
if error > tolerance {
is_periodic = false;
break;
}
}
if is_periodic {
let estimated_period = period as f64 * 2.0 * std::f64::consts::PI;
return Ok(PeriodicOrbit {
representative_point: return_points[0].clone(),
period: estimated_period,
stability: StabilityType::Stable, floquet_multipliers: vec![], });
}
}
Err(IntegrateError::ComputationError(
"No periodic behavior detected in return map".to_string(),
))
}
fn fourier_analysis_periodic_orbits<F>(
&self,
system: &F,
domain: &[(f64, f64)],
) -> IntegrateResult<Vec<PeriodicOrbit>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let mut periodic_orbits = Vec::new();
let n_trajectories = 5;
let mut initial_points = Vec::new();
self.generate_grid_points(domain, n_trajectories, &mut initial_points);
for initial_point in &initial_points {
if let Ok(orbit) = self.fourier_analysis_single_trajectory(system, initial_point) {
periodic_orbits.push(orbit);
}
}
Ok(periodic_orbits)
}
fn fourier_analysis_single_trajectory<F>(
&self,
system: &F,
initial_point: &Array1<f64>,
) -> IntegrateResult<PeriodicOrbit>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let dt = 0.01;
let total_time = 20.0;
let n_steps = (total_time / dt) as usize;
let mut trajectory = Vec::new();
let mut state = initial_point.clone();
for _ in 0..n_steps {
trajectory.push(state.clone());
let derivative = system(&state);
state += &(derivative * dt);
}
if let Ok(dominant_period) = self.detect_dominant_period(&trajectory, dt) {
if dominant_period > 0.0 && dominant_period < total_time {
return Ok(PeriodicOrbit {
representative_point: initial_point.clone(),
period: dominant_period,
stability: StabilityType::Stable, floquet_multipliers: vec![], });
}
}
Err(IntegrateError::ComputationError(
"No periodic behavior detected via Fourier analysis".to_string(),
))
}
fn detect_dominant_period(&self, trajectory: &[Array1<f64>], dt: f64) -> IntegrateResult<f64> {
if trajectory.len() < 100 {
return Err(IntegrateError::ComputationError(
"Trajectory too short for period detection".to_string(),
));
}
let signal: Vec<f64> = trajectory.iter().map(|state| state[0]).collect();
let max_lag = std::cmp::min(signal.len() / 4, 500);
let mut autocorr = vec![0.0; max_lag];
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
let variance =
signal.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / signal.len() as f64;
if variance < 1e-12 {
return Err(IntegrateError::ComputationError(
"Signal has zero variance".to_string(),
));
}
for lag in 1..max_lag {
let mut correlation = 0.0;
let count = signal.len() - lag;
for i in 0..count {
correlation += (signal[i] - mean) * (signal[i + lag] - mean);
}
autocorr[lag] = correlation / (count as f64 * variance);
}
let mut max_corr = 0.0;
let mut period_lag = 0;
for lag in 10..max_lag {
if autocorr[lag] > max_corr && autocorr[lag] > 0.5 {
if lag > 0
&& lag < max_lag - 1
&& autocorr[lag] > autocorr[lag - 1]
&& autocorr[lag] > autocorr[lag + 1]
{
max_corr = autocorr[lag];
period_lag = lag;
}
}
}
if period_lag > 0 {
Ok(period_lag as f64 * dt)
} else {
Err(IntegrateError::ComputationError(
"No dominant period detected".to_string(),
))
}
}
fn remove_duplicate_periodic_orbits(&self, orbits: Vec<PeriodicOrbit>) -> Vec<PeriodicOrbit> {
let mut filtered: Vec<PeriodicOrbit> = Vec::new();
let tolerance = 1e-4;
for orbit in orbits {
let mut is_duplicate = false;
for existing in &filtered {
let distance = (&orbit.representative_point - &existing.representative_point)
.iter()
.map(|&x| x * x)
.sum::<f64>()
.sqrt();
let period_diff = (orbit.period - existing.period).abs();
if distance < tolerance && period_diff < tolerance {
is_duplicate = true;
break;
}
}
if !is_duplicate {
filtered.push(orbit);
}
}
filtered
}
fn solve_linear_system_for_shooting(
&self,
matrix: &Array2<f64>,
rhs: &Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
let n = matrix.nrows();
if n != matrix.ncols() || n != rhs.len() {
return Err(IntegrateError::ComputationError(
"Inconsistent matrix dimensions in shooting method".to_string(),
));
}
let mut augmented = Array2::<f64>::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
augmented[[i, j]] = matrix[[i, j]];
}
augmented[[i, n]] = rhs[i];
}
for k in 0..n {
let mut max_row = k;
for i in (k + 1)..n {
if augmented[[i, k]].abs() > augmented[[max_row, k]].abs() {
max_row = i;
}
}
if max_row != k {
for j in k..=n {
let temp = augmented[[k, j]];
augmented[[k, j]] = augmented[[max_row, j]];
augmented[[max_row, j]] = temp;
}
}
if augmented[[k, k]].abs() < 1e-14 {
return Err(IntegrateError::ComputationError(
"Singular matrix in shooting method".to_string(),
));
}
for i in (k + 1)..n {
let factor = augmented[[i, k]] / augmented[[k, k]];
for j in k..=n {
augmented[[i, j]] -= factor * augmented[[k, j]];
}
}
}
let mut solution = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
solution[i] = augmented[[i, n]];
for j in (i + 1)..n {
solution[i] -= augmented[[i, j]] * solution[j];
}
solution[i] /= augmented[[i, i]];
}
Ok(solution)
}
fn compute_lyapunov_exponents<F>(&self, system: &F) -> IntegrateResult<Option<Array1<f64>>>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync + Clone,
{
if self.dimension == 0 || self.dimension > 10 {
return Ok(None);
}
let initial_state = Array1::zeros(self.dimension);
let dt = match self.dimension {
1 => 0.01,
2 => 0.005,
3 => 0.002,
4..=6 => 0.001,
_ => 0.0005,
};
let n_exponents = if self.dimension <= 4 {
self.dimension
} else {
std::cmp::min(4, self.dimension)
};
let calculator = advanced::LyapunovCalculator::new(n_exponents, dt);
let integration_time = self.integration_time * (self.dimension as f64).sqrt();
let system_clone = system.clone();
let system_wrapper = move |state: &Array1<f64>| system_clone(state);
match calculator.calculate_lyapunov_exponents(
system_wrapper,
&initial_state,
integration_time,
) {
Ok(exponents) => {
let filtered_exponents = exponents.mapv(|x| if x.abs() < 1e-12 { 0.0 } else { x });
Ok(Some(filtered_exponents))
}
Err(e) => {
eprintln!("Warning: Lyapunov exponent computation failed: {e:?}");
Ok(None)
}
}
}
fn analyze_basins<F>(
&self,
system: &F,
domain: &[(f64, f64)],
fixed_points: &[FixedPoint],
) -> IntegrateResult<BasinAnalysis>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync,
{
if self.dimension != 2 || domain.len() != 2 {
return Err(IntegrateError::ValueError(
"Basin analysis only implemented for 2D systems".to_string(),
));
}
let grid_size = self.basin_grid_size;
let mut grid_points = Array2::zeros((grid_size * grid_size, 2));
let mut attractor_indices = Array2::<i32>::zeros((grid_size, grid_size));
let dx = (domain[0].1 - domain[0].0) / (grid_size - 1) as f64;
let dy = (domain[1].1 - domain[1].0) / (grid_size - 1) as f64;
for i in 0..grid_size {
for j in 0..grid_size {
let x = domain[0].0 + i as f64 * dx;
let y = domain[1].0 + j as f64 * dy;
grid_points[[i * grid_size + j, 0]] = x;
grid_points[[i * grid_size + j, 1]] = y;
let initial_state = Array1::from_vec(vec![x, y]);
let final_state = self.integrate_trajectory(system, &initial_state)?;
let mut closest_attractor = -1;
let mut min_distance = f64::INFINITY;
for (idx, fp) in fixed_points.iter().enumerate() {
if fp.stability == StabilityType::Stable {
let distance = (&final_state - &fp.location)
.iter()
.map(|&x| x * x)
.sum::<f64>()
.sqrt();
if distance < min_distance && distance < 0.1 {
min_distance = distance;
closest_attractor = idx as i32;
}
}
}
attractor_indices[[i, j]] = closest_attractor;
}
}
let attractors = fixed_points
.iter()
.filter(|fp| fp.stability == StabilityType::Stable)
.map(|fp| fp.location.clone())
.collect();
Ok(BasinAnalysis {
grid_points,
attractor_indices,
attractors,
})
}
fn integrate_trajectory<F>(
&self,
system: &F,
initial_state: &Array1<f64>,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let dt = 0.01;
let n_steps = (self.integration_time / dt) as usize;
let mut state = initial_state.clone();
for _ in 0..n_steps {
let derivative = system(&state);
state += &(derivative * dt);
}
Ok(state)
}
}