use crate::analysis::types::*;
use crate::error::{IntegrateError, IntegrateResult};
use scirs2_core::ndarray::{Array1, Array2};
use scirs2_core::numeric::Complex64;
use scirs2_core::random::{Rng, RngExt};
use std::collections::HashSet;
pub struct PoincareAnalyzer {
pub section_normal: Array1<f64>,
pub section_point: Array1<f64>,
pub crossing_direction: i8,
pub crossing_tolerance: f64,
}
impl PoincareAnalyzer {
pub fn new(
section_normal: Array1<f64>,
section_point: Array1<f64>,
crossing_direction: i8,
) -> Self {
Self {
section_normal,
section_point,
crossing_direction,
crossing_tolerance: 1e-8,
}
}
pub fn analyze_trajectory(
&self,
trajectory: &[Array1<f64>],
times: &[f64],
) -> IntegrateResult<PoincareMap> {
let mut crossings = Vec::new();
let mut crossing_times = Vec::new();
for i in 1..trajectory.len() {
if let Some((crossing_point, crossing_time)) =
self.detect_crossing(&trajectory[i - 1], &trajectory[i], times[i - 1], times[i])?
{
crossings.push(crossing_point);
crossing_times.push(crossing_time);
}
}
let return_map = if crossings.len() > 1 {
Some(self.compute_return_map(&crossings)?)
} else {
None
};
let periodic_orbits = self.detect_periodic_orbits(&crossings)?;
Ok(PoincareMap {
crossings,
crossing_times,
return_map,
periodic_orbits,
section_normal: self.section_normal.clone(),
section_point: self.section_point.clone(),
})
}
fn detect_crossing(
&self,
point1: &Array1<f64>,
point2: &Array1<f64>,
t1: f64,
t2: f64,
) -> IntegrateResult<Option<(Array1<f64>, f64)>> {
let d1 = self.distance_from_section(point1);
let d2 = self.distance_from_section(point2);
let crossed = match self.crossing_direction {
1 => d1 < 0.0 && d2 > 0.0, -1 => d1 > 0.0 && d2 < 0.0, 0 => d1 * d2 < 0.0, _ => false,
};
if !crossed {
return Ok(None);
}
let alpha = d1.abs() / (d1.abs() + d2.abs());
let crossing_point = (1.0 - alpha) * point1 + alpha * point2;
let crossing_time = (1.0 - alpha) * t1 + alpha * t2;
Ok(Some((crossing_point, crossing_time)))
}
fn distance_from_section(&self, point: &Array1<f64>) -> f64 {
let relative_pos = point - &self.section_point;
relative_pos.dot(&self.section_normal)
}
fn compute_return_map(&self, crossings: &[Array1<f64>]) -> IntegrateResult<ReturnMap> {
let mut current_points = Vec::new();
let mut next_points = Vec::new();
for i in 0..crossings.len() - 1 {
let current_projected = self.project_to_section(&crossings[i]);
let next_projected = self.project_to_section(&crossings[i + 1]);
current_points.push(current_projected);
next_points.push(next_projected);
}
Ok(ReturnMap {
current_points,
next_points,
})
}
fn project_to_section(&self, point: &Array1<f64>) -> Array1<f64> {
let distance = self.distance_from_section(point);
point - distance * &self.section_normal
}
fn detect_periodic_orbits(
&self,
crossings: &[Array1<f64>],
) -> IntegrateResult<Vec<PeriodicOrbit>> {
let mut periodic_orbits = Vec::new();
let tolerance = 1e-6;
for i in 0..crossings.len() {
for j in (i + 2)..crossings.len() {
let distance = self.euclidean_distance(&crossings[i], &crossings[j]);
if distance < tolerance {
let period_length = j - i;
let representative_point = crossings[i].clone();
let mut is_periodic = true;
for k in 1..period_length {
if i + k < crossings.len() && j + k < crossings.len() {
let dist =
self.euclidean_distance(&crossings[i + k], &crossings[j + k]);
if dist > tolerance {
is_periodic = false;
break;
}
}
}
if is_periodic {
let period = (j - i) as f64;
periodic_orbits.push(PeriodicOrbit {
representative_point,
period,
stability: StabilityType::Stable, floquet_multipliers: vec![], });
}
}
}
}
Ok(periodic_orbits)
}
fn euclidean_distance(&self, a: &Array1<f64>, b: &Array1<f64>) -> f64 {
(a - b).iter().map(|&x| x * x).sum::<f64>().sqrt()
}
}
#[derive(Debug, Clone)]
pub struct PoincareMap {
pub crossings: Vec<Array1<f64>>,
pub crossing_times: Vec<f64>,
pub return_map: Option<ReturnMap>,
pub periodic_orbits: Vec<PeriodicOrbit>,
pub section_normal: Array1<f64>,
pub section_point: Array1<f64>,
}
#[derive(Debug, Clone)]
pub struct ReturnMap {
pub current_points: Vec<Array1<f64>>,
pub next_points: Vec<Array1<f64>>,
}
pub struct LyapunovCalculator {
pub n_exponents: usize,
pub perturbation_magnitude: f64,
pub renormalization_interval: usize,
pub dt: f64,
}
impl LyapunovCalculator {
pub fn new(nexponents: usize, dt: f64) -> Self {
Self {
n_exponents: nexponents,
perturbation_magnitude: 1e-8,
renormalization_interval: 100,
dt,
}
}
pub fn calculate_lyapunov_exponents<F>(
&self,
system: F,
initial_state: &Array1<f64>,
total_time: f64,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync,
{
let n_steps = (total_time / self.dt) as usize;
let dim = initial_state.len();
if self.n_exponents > dim {
return Err(IntegrateError::ValueError(
"Number of exponents cannot exceed system dimension".to_string(),
));
}
let mut state = initial_state.clone();
let mut tangent_vectors = self.initialize_tangent_vectors(dim)?;
let mut lyapunov_sums = Array1::zeros(self.n_exponents);
for step in 0..n_steps {
let derivative = system(&state);
state += &(derivative * self.dt);
for i in 0..self.n_exponents {
let jacobian = self.compute_jacobian(&system, &state)?;
let tangent_derivative = jacobian.dot(&tangent_vectors.column(i));
let old_tangent = tangent_vectors.column(i).to_owned();
tangent_vectors
.column_mut(i)
.assign(&(&old_tangent + &(tangent_derivative * self.dt)));
}
if step % self.renormalization_interval == 0 && step > 0 {
let (q, r) = self.qr_decomposition(&tangent_vectors)?;
tangent_vectors = q;
for i in 0..self.n_exponents {
lyapunov_sums[i] += r[[i, i]].abs().ln();
}
}
}
let lyapunov_exponents = lyapunov_sums / total_time;
Ok(lyapunov_exponents)
}
fn initialize_tangent_vectors(&self, dim: usize) -> IntegrateResult<Array2<f64>> {
let mut vectors = Array2::zeros((dim, self.n_exponents));
let mut rng = scirs2_core::random::rng();
for i in 0..self.n_exponents {
for j in 0..dim {
vectors[[j, i]] = rng.random::<f64>() - 0.5;
}
}
for i in 0..self.n_exponents {
for j in 0..i {
let projection = vectors.column(i).dot(&vectors.column(j));
let col_j = vectors.column(j).to_owned();
let mut col_i = vectors.column_mut(i);
col_i -= &(projection * &col_j);
}
let norm = vectors.column(i).iter().map(|&x| x * x).sum::<f64>().sqrt();
if norm > 1e-12 {
vectors.column_mut(i).mapv_inplace(|x| x / norm);
}
}
Ok(vectors)
}
fn compute_jacobian<F>(&self, system: &F, state: &Array1<f64>) -> IntegrateResult<Array2<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let dim = state.len();
let mut jacobian = Array2::zeros((dim, dim));
let h = self.perturbation_magnitude;
for j in 0..dim {
let mut state_plus = state.clone();
let mut state_minus = state.clone();
state_plus[j] += h;
state_minus[j] -= h;
let f_plus = system(&state_plus);
let f_minus = system(&state_minus);
for i in 0..dim {
jacobian[[i, j]] = (f_plus[i] - f_minus[i]) / (2.0 * h);
}
}
Ok(jacobian)
}
fn qr_decomposition(
&self,
matrix: &Array2<f64>,
) -> IntegrateResult<(Array2<f64>, Array2<f64>)> {
let (m, n) = matrix.dim();
let mut q = matrix.clone();
let mut r = Array2::zeros((n, n));
for j in 0..n {
for i in 0..j {
r[[i, j]] = q.column(j).dot(&q.column(i));
let col_i = q.column(i).to_owned();
let mut col_j = q.column_mut(j);
col_j -= &(r[[i, j]] * &col_i);
}
r[[j, j]] = q.column(j).iter().map(|&x| x * x).sum::<f64>().sqrt();
if r[[j, j]] > 1e-12 {
q.column_mut(j).mapv_inplace(|x| x / r[[j, j]]);
}
}
Ok((q, r))
}
pub fn calculate_largest_lyapunov_exponent<F>(
&self,
system: F,
initial_state: &Array1<f64>,
total_time: f64,
min_separation: f64,
max_separation: f64,
) -> IntegrateResult<f64>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync,
{
let n_steps = (total_time / self.dt) as usize;
let dim = initial_state.len();
let mut reference_state = initial_state.clone();
let mut nearby_state = initial_state.clone();
nearby_state[0] += self.perturbation_magnitude;
let mut lyapunov_sum = 0.0;
let mut n_rescales = 0;
for _step in 0..n_steps {
let ref_derivative = system(&reference_state);
let nearby_derivative = system(&nearby_state);
reference_state += &(ref_derivative * self.dt);
nearby_state += &(nearby_derivative * self.dt);
let separation_vector = &nearby_state - &reference_state;
let separation = separation_vector.iter().map(|&x| x * x).sum::<f64>().sqrt();
if (separation > max_separation || separation < min_separation) && separation > 1e-15 {
lyapunov_sum += separation.ln();
n_rescales += 1;
let scale_factor = self.perturbation_magnitude / separation;
nearby_state = &reference_state + &(separation_vector * scale_factor);
}
}
if n_rescales > 0 {
Ok(lyapunov_sum / total_time)
} else {
Ok(0.0) }
}
pub fn estimate_lyapunov_from_timeseries(
&self,
timeseries: &Array1<f64>,
embedding_dimension: usize,
delay: usize,
) -> IntegrateResult<f64> {
let n = timeseries.len();
if n < embedding_dimension * delay + 1 {
return Err(IntegrateError::ValueError(
"Time series too short for embedding".to_string(),
));
}
let n_vectors = n - embedding_dimension * delay;
let mut embedded_vectors = Vec::new();
for i in 0..n_vectors {
let mut vector = Array1::zeros(embedding_dimension);
for j in 0..embedding_dimension {
vector[j] = timeseries[i + j * delay];
}
embedded_vectors.push(vector);
}
let mut lyapunov_sum = 0.0;
let mut count = 0;
let min_time_separation = 2 * delay;
for i in 0..embedded_vectors.len() - 1 {
let mut min_distance = f64::INFINITY;
let mut nearest_index = None;
for j in 0..embedded_vectors.len() - 1 {
if (j as i32 - i as i32).abs() >= min_time_separation as i32 {
let distance =
self.euclidean_distance_arrays(&embedded_vectors[i], &embedded_vectors[j]);
if distance < min_distance && distance > 1e-12 {
min_distance = distance;
nearest_index = Some(j);
}
}
}
if let Some(j) = nearest_index {
if i + 1 < embedded_vectors.len() && j + 1 < embedded_vectors.len() {
let initial_distance = min_distance;
let final_distance = self.euclidean_distance_arrays(
&embedded_vectors[i + 1],
&embedded_vectors[j + 1],
);
if final_distance > 1e-12 && initial_distance > 1e-12 {
lyapunov_sum += (final_distance / initial_distance).ln();
count += 1;
}
}
}
}
if count > 0 {
Ok(lyapunov_sum / (count as f64))
} else {
Ok(0.0)
}
}
fn euclidean_distance_arrays(&self, a: &Array1<f64>, b: &Array1<f64>) -> f64 {
(a - b).iter().map(|&x| x * x).sum::<f64>().sqrt()
}
pub fn calculate_lyapunov_spectrum_with_errors<F>(
&self,
system: F,
initial_state: &Array1<f64>,
total_time: f64,
n_trials: usize,
) -> IntegrateResult<(Array1<f64>, Array1<f64>)>
where
F: Fn(&Array1<f64>) -> Array1<f64> + Send + Sync + Clone,
{
let dim = initial_state.len();
let n_exponents = self.n_exponents.min(dim);
let mut all_exponents = Array2::zeros((n_trials, n_exponents));
let mut rng = scirs2_core::random::rng();
for trial in 0..n_trials {
let mut perturbed_initial = initial_state.clone();
for i in 0..dim {
perturbed_initial[i] += (rng.random::<f64>() - 0.5) * 1e-6;
}
let exponents =
self.calculate_lyapunov_exponents(system.clone(), &perturbed_initial, total_time)?;
for i in 0..n_exponents {
all_exponents[[trial, i]] = exponents[i];
}
}
let mut means = Array1::zeros(n_exponents);
let mut std_devs = Array1::zeros(n_exponents);
for i in 0..n_exponents {
let column = all_exponents.column(i);
let mean = column.sum() / n_trials as f64;
let variance =
column.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n_trials as f64;
means[i] = mean;
std_devs[i] = variance.sqrt();
}
Ok((means, std_devs))
}
}
pub struct FractalAnalyzer {
pub scale_range: (f64, f64),
pub n_scales: usize,
pub box_counting_method: BoxCountingMethod,
}
#[derive(Debug, Clone, Copy)]
pub enum BoxCountingMethod {
Standard,
Differential,
Correlation,
}
impl Default for FractalAnalyzer {
fn default() -> Self {
Self::new()
}
}
impl FractalAnalyzer {
pub fn new() -> Self {
Self {
scale_range: (1e-4, 1e-1),
n_scales: 20,
box_counting_method: BoxCountingMethod::Standard,
}
}
pub fn calculate_fractal_dimension(
&self,
attractor_points: &[Array1<f64>],
) -> IntegrateResult<FractalDimension> {
match self.box_counting_method {
BoxCountingMethod::Standard => self.box_counting_dimension(attractor_points),
BoxCountingMethod::Differential => self.differential_box_counting(attractor_points),
BoxCountingMethod::Correlation => self.correlation_dimension(attractor_points),
}
}
fn box_counting_dimension(&self, points: &[Array1<f64>]) -> IntegrateResult<FractalDimension> {
if points.is_empty() {
return Err(IntegrateError::ValueError(
"Cannot analyze empty point set".to_string(),
));
}
let dim = points[0].len();
let (min_bounds, max_bounds) = self.find_bounding_box(points);
let domain_size = max_bounds
.iter()
.zip(min_bounds.iter())
.map(|(&max, &min)| max - min)
.fold(0.0f64, |acc, x| acc.max(x));
let mut scales = Vec::new();
let mut counts = Vec::new();
for i in 0..self.n_scales {
let t = i as f64 / (self.n_scales - 1) as f64;
let scale = self.scale_range.0 * (self.scale_range.1 / self.scale_range.0).powf(t);
let box_size = scale * domain_size;
let count = self.count_occupied_boxes(points, &min_bounds, box_size, dim)?;
scales.push(scale);
counts.push(count as f64);
}
let slope = self.calculate_slope_from_log_data(&scales, &counts)?;
let dimension = -slope;
let r_squared = self.calculate_r_squared(&scales, &counts, slope)?;
Ok(FractalDimension {
dimension,
method: self.box_counting_method,
scales,
counts,
r_squared,
})
}
fn differential_box_counting(
&self,
points: &[Array1<f64>],
) -> IntegrateResult<FractalDimension> {
self.box_counting_dimension(points)
}
fn correlation_dimension(&self, points: &[Array1<f64>]) -> IntegrateResult<FractalDimension> {
let n_points = points.len();
let mut scales = Vec::new();
let mut correlations = Vec::new();
for i in 0..self.n_scales {
let t = i as f64 / (self.n_scales - 1) as f64;
let r = self.scale_range.0 * (self.scale_range.1 / self.scale_range.0).powf(t);
let mut count = 0;
for i in 0..n_points {
for j in i + 1..n_points {
let distance = self.euclidean_distance(&points[i], &points[j]);
if distance < r {
count += 1;
}
}
}
let correlation = 2.0 * count as f64 / (n_points * (n_points - 1)) as f64;
scales.push(r);
correlations.push(correlation);
}
let filtered_data: Vec<(f64, f64)> = scales
.iter()
.zip(correlations.iter())
.filter(|(_, &c)| c > 0.0)
.map(|(&s, &c)| (s, c))
.collect();
if filtered_data.len() < 2 {
return Err(IntegrateError::ComputationError(
"Insufficient data for correlation dimension calculation".to_string(),
));
}
let filtered_scales: Vec<f64> = filtered_data.iter().map(|(s, _)| *s).collect();
let filtered_correlations: Vec<f64> = filtered_data.iter().map(|(_, c)| *c).collect();
let dimension =
self.calculate_slope_from_log_data(&filtered_scales, &filtered_correlations)?;
Ok(FractalDimension {
dimension,
method: BoxCountingMethod::Correlation,
scales,
counts: correlations,
r_squared: self.calculate_r_squared(
&filtered_scales,
&filtered_correlations,
dimension,
)?,
})
}
fn find_bounding_box(&self, points: &[Array1<f64>]) -> (Array1<f64>, Array1<f64>) {
let dim = points[0].len();
let mut min_bounds = Array1::from_elem(dim, f64::INFINITY);
let mut max_bounds = Array1::from_elem(dim, f64::NEG_INFINITY);
for point in points {
for i in 0..dim {
min_bounds[i] = min_bounds[i].min(point[i]);
max_bounds[i] = max_bounds[i].max(point[i]);
}
}
(min_bounds, max_bounds)
}
fn count_occupied_boxes(
&self,
points: &[Array1<f64>],
min_bounds: &Array1<f64>,
box_size: f64,
dim: usize,
) -> IntegrateResult<usize> {
let mut occupied_boxes = HashSet::new();
for point in points {
let mut box_index = Vec::with_capacity(dim);
for i in 0..dim {
let index = ((point[i] - min_bounds[i]) / box_size).floor() as i64;
box_index.push(index);
}
occupied_boxes.insert(box_index);
}
Ok(occupied_boxes.len())
}
fn calculate_slope_from_log_data(
&self,
x_data: &[f64],
y_data: &[f64],
) -> IntegrateResult<f64> {
if x_data.len() != y_data.len() || x_data.len() < 2 {
return Err(IntegrateError::ValueError(
"Insufficient data for slope calculation".to_string(),
));
}
let n = x_data.len() as f64;
let log_x: Vec<f64> = x_data.iter().map(|&x| x.ln()).collect();
let log_y: Vec<f64> = y_data.iter().map(|&y| y.ln()).collect();
let sum_x: f64 = log_x.iter().sum();
let sum_y: f64 = log_y.iter().sum();
let sum_xy: f64 = log_x.iter().zip(log_y.iter()).map(|(&x, &y)| x * y).sum();
let sum_xx: f64 = log_x.iter().map(|&x| x * x).sum();
let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
Ok(slope)
}
fn calculate_r_squared(
&self,
x_data: &[f64],
y_data: &[f64],
slope: f64,
) -> IntegrateResult<f64> {
let log_x: Vec<f64> = x_data.iter().map(|&x| x.ln()).collect();
let log_y: Vec<f64> = y_data.iter().map(|&y| y.ln()).collect();
let mean_y = log_y.iter().sum::<f64>() / log_y.len() as f64;
let mean_x = log_x.iter().sum::<f64>() / log_x.len() as f64;
let intercept = mean_y - slope * mean_x;
let mut ss_tot = 0.0;
let mut ss_res = 0.0;
for i in 0..log_y.len() {
let y_pred = slope * log_x[i] + intercept;
ss_res += (log_y[i] - y_pred).powi(2);
ss_tot += (log_y[i] - mean_y).powi(2);
}
let r_squared = 1.0 - (ss_res / ss_tot);
Ok(r_squared)
}
fn euclidean_distance(&self, a: &Array1<f64>, b: &Array1<f64>) -> f64 {
a.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).powi(2))
.sum::<f64>()
.sqrt()
}
}
#[derive(Debug, Clone)]
pub struct FractalDimension {
pub dimension: f64,
pub method: BoxCountingMethod,
pub scales: Vec<f64>,
pub counts: Vec<f64>,
pub r_squared: f64,
}
pub struct RecurrenceAnalyzer {
pub threshold: f64,
pub embedding_dimension: usize,
pub time_delay: usize,
pub distance_metric: DistanceMetric,
}
#[derive(Debug, Clone, Copy)]
pub enum DistanceMetric {
Euclidean,
Maximum,
Manhattan,
}
impl RecurrenceAnalyzer {
pub fn new(threshold: f64, embedding_dimension: usize, timedelay: usize) -> Self {
Self {
threshold,
embedding_dimension,
time_delay: timedelay,
distance_metric: DistanceMetric::Euclidean,
}
}
pub fn analyze_recurrence(&self, timeseries: &[f64]) -> IntegrateResult<RecurrenceAnalysis> {
let embedded_vectors = self.create_embedding(timeseries)?;
let recurrence_matrix = self.compute_recurrence_matrix(&embedded_vectors)?;
let rqa_measures = self.calculate_rqa_measures(&recurrence_matrix)?;
Ok(RecurrenceAnalysis {
recurrence_matrix,
embedded_vectors,
rqa_measures,
threshold: self.threshold,
embedding_dimension: self.embedding_dimension,
time_delay: self.time_delay,
})
}
fn create_embedding(&self, timeseries: &[f64]) -> IntegrateResult<Vec<Array1<f64>>> {
let n = timeseries.len();
let embedded_length = n - (self.embedding_dimension - 1) * self.time_delay;
if embedded_length <= 0 {
return Err(IntegrateError::ValueError(
"Time series too short for given embedding parameters".to_string(),
));
}
let mut embedded_vectors = Vec::with_capacity(embedded_length);
for i in 0..embedded_length {
let mut vector = Array1::zeros(self.embedding_dimension);
for j in 0..self.embedding_dimension {
vector[j] = timeseries[i + j * self.time_delay];
}
embedded_vectors.push(vector);
}
Ok(embedded_vectors)
}
fn compute_recurrence_matrix(&self, vectors: &[Array1<f64>]) -> IntegrateResult<Array2<bool>> {
let n = vectors.len();
let mut recurrence_matrix = Array2::from_elem((n, n), false);
for i in 0..n {
for j in 0..n {
let distance = self.calculate_distance(&vectors[i], &vectors[j]);
recurrence_matrix[[i, j]] = distance <= self.threshold;
}
}
Ok(recurrence_matrix)
}
fn calculate_distance(&self, a: &Array1<f64>, b: &Array1<f64>) -> f64 {
match self.distance_metric {
DistanceMetric::Euclidean => a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).powi(2))
.sum::<f64>()
.sqrt(),
DistanceMetric::Maximum => a
.iter()
.zip(b.iter())
.map(|(&x, &y)| (x - y).abs())
.fold(0.0, f64::max),
DistanceMetric::Manhattan => a.iter().zip(b.iter()).map(|(&x, &y)| (x - y).abs()).sum(),
}
}
fn calculate_rqa_measures(
&self,
recurrence_matrix: &Array2<bool>,
) -> IntegrateResult<RQAMeasures> {
let n = recurrence_matrix.nrows();
let total_points = (n * n) as f64;
let recurrent_points = recurrence_matrix.iter().filter(|&&x| x).count() as f64;
let recurrence_rate = recurrent_points / total_points;
let diagonal_lines = self.find_diagonal_lines(recurrence_matrix, 2)?;
let diagonal_points: usize = diagonal_lines.iter().map(|line| line.length).sum();
let determinism = diagonal_points as f64 / recurrent_points;
let avg_diagonal_length = if !diagonal_lines.is_empty() {
diagonal_points as f64 / diagonal_lines.len() as f64
} else {
0.0
};
let max_diagonal_length = diagonal_lines
.iter()
.map(|line| line.length)
.max()
.unwrap_or(0) as f64;
let vertical_lines = self.find_vertical_lines(recurrence_matrix, 2)?;
let vertical_points: usize = vertical_lines.iter().map(|line| line.length).sum();
let laminarity = vertical_points as f64 / recurrent_points;
let trapping_time = if !vertical_lines.is_empty() {
vertical_points as f64 / vertical_lines.len() as f64
} else {
0.0
};
Ok(RQAMeasures {
recurrence_rate,
determinism,
avg_diagonal_length,
max_diagonal_length,
laminarity,
trapping_time,
})
}
fn find_diagonal_lines(
&self,
matrix: &Array2<bool>,
min_length: usize,
) -> IntegrateResult<Vec<RecurrentLine>> {
let n = matrix.nrows();
let mut lines = Vec::new();
for k in -(n as i32 - 1)..(n as i32) {
let mut current_length = 0;
let mut start_i = 0;
let mut start_j = 0;
let (start_row, start_col) = if k >= 0 {
(0, k as usize)
} else {
((-k) as usize, 0)
};
let max_steps = n - start_row.max(start_col);
for step in 0..max_steps {
let i = start_row + step;
let j = start_col + step;
if i < n && j < n && matrix[[i, j]] {
if current_length == 0 {
start_i = i;
start_j = j;
}
current_length += 1;
} else {
if current_length >= min_length {
lines.push(RecurrentLine {
start_i,
start_j,
length: current_length,
line_type: LineType::Diagonal,
});
}
current_length = 0;
}
}
if current_length >= min_length {
lines.push(RecurrentLine {
start_i,
start_j,
length: current_length,
line_type: LineType::Diagonal,
});
}
}
Ok(lines)
}
fn find_vertical_lines(
&self,
matrix: &Array2<bool>,
min_length: usize,
) -> IntegrateResult<Vec<RecurrentLine>> {
let n = matrix.nrows();
let mut lines = Vec::new();
for j in 0..n {
let mut current_length = 0;
let mut start_i = 0;
for i in 0..n {
if matrix[[i, j]] {
if current_length == 0 {
start_i = i;
}
current_length += 1;
} else {
if current_length >= min_length {
lines.push(RecurrentLine {
start_i,
start_j: j,
length: current_length,
line_type: LineType::Vertical,
});
}
current_length = 0;
}
}
if current_length >= min_length {
lines.push(RecurrentLine {
start_i,
start_j: j,
length: current_length,
line_type: LineType::Vertical,
});
}
}
Ok(lines)
}
}
#[derive(Debug, Clone)]
pub struct RecurrenceAnalysis {
pub recurrence_matrix: Array2<bool>,
pub embedded_vectors: Vec<Array1<f64>>,
pub rqa_measures: RQAMeasures,
pub threshold: f64,
pub embedding_dimension: usize,
pub time_delay: usize,
}
#[derive(Debug, Clone)]
pub struct RQAMeasures {
pub recurrence_rate: f64,
pub determinism: f64,
pub avg_diagonal_length: f64,
pub max_diagonal_length: f64,
pub laminarity: f64,
pub trapping_time: f64,
}
#[derive(Debug, Clone)]
pub struct RecurrentLine {
pub start_i: usize,
pub start_j: usize,
pub length: usize,
pub line_type: LineType,
}
#[derive(Debug, Clone, Copy)]
pub enum LineType {
Diagonal,
Vertical,
Horizontal,
}
pub struct ContinuationAnalyzer {
pub param_range: (f64, f64),
pub n_steps: usize,
pub tol: f64,
pub max_newton_iter: usize,
}
impl ContinuationAnalyzer {
pub fn new(paramrange: (f64, f64), n_steps: usize) -> Self {
Self {
param_range: paramrange,
n_steps,
tol: 1e-8,
max_newton_iter: 50,
}
}
pub fn trace_bifurcation_curve<F>(
&self,
system: F,
initial_state: &Array1<f64>,
) -> IntegrateResult<ContinuationResult>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let mut bifurcation_points = Vec::new();
let mut fixed_points = Vec::new();
let (param_start, param_end) = self.param_range;
let step = (param_end - param_start) / self.n_steps as f64;
let mut current_state = initial_state.clone();
for i in 0..=self.n_steps {
let param = param_start + i as f64 * step;
let fixed_point = self.find_fixed_point(&system, ¤t_state, param)?;
let jac = self.numerical_jacobian(&system, &fixed_point, param)?;
let eigenvalues = self.compute_eigenvalues(&jac)?;
if let Some(bif_type) = self.detect_bifurcation(&eigenvalues) {
bifurcation_points.push(BifurcationPointData {
parameter: param,
state: fixed_point.clone(),
bifurcation_type: bif_type,
});
}
fixed_points.push(FixedPointData {
parameter: param,
state: fixed_point.clone(),
eigenvalues: eigenvalues.clone(),
stability: self.classify_stability(&eigenvalues),
});
current_state = fixed_point;
}
Ok(ContinuationResult {
bifurcation_points,
fixed_points,
parameter_range: self.param_range,
})
}
fn find_fixed_point<F>(
&self,
system: &F,
initial_guess: &Array1<f64>,
parameter: f64,
) -> IntegrateResult<Array1<f64>>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let mut x = initial_guess.clone();
for _ in 0..self.max_newton_iter {
let f = system(&x, parameter);
let norm_f = f.iter().map(|&v| v * v).sum::<f64>().sqrt();
if norm_f < self.tol {
return Ok(x);
}
let jac = self.numerical_jacobian(system, &x, parameter)?;
let delta_x = self.solve_linear_system(&jac, &f)?;
x = &x - &delta_x;
}
Err(IntegrateError::ConvergenceError(
"Fixed point not found".to_string(),
))
}
fn numerical_jacobian<F>(
&self,
system: &F,
x: &Array1<f64>,
parameter: f64,
) -> IntegrateResult<Array2<f64>>
where
F: Fn(&Array1<f64>, f64) -> Array1<f64>,
{
let n = x.len();
let mut jac = Array2::zeros((n, n));
let h = 1e-8;
for j in 0..n {
let mut x_plus = x.clone();
let mut x_minus = x.clone();
x_plus[j] += h;
x_minus[j] -= h;
let f_plus = system(&x_plus, parameter);
let f_minus = system(&x_minus, parameter);
for i in 0..n {
jac[[i, j]] = (f_plus[i] - f_minus[i]) / (2.0 * h);
}
}
Ok(jac)
}
fn solve_linear_system(
&self,
a: &Array2<f64>,
b: &Array1<f64>,
) -> IntegrateResult<Array1<f64>> {
let n = a.nrows();
let mut aug = Array2::zeros((n, n + 1));
for i in 0..n {
for j in 0..n {
aug[[i, j]] = a[[i, j]];
}
aug[[i, n]] = b[i];
}
for k in 0..n {
let mut max_row = k;
for i in k + 1..n {
if aug[[i, k]].abs() > aug[[max_row, k]].abs() {
max_row = i;
}
}
for j in 0..n + 1 {
let temp = aug[[k, j]];
aug[[k, j]] = aug[[max_row, j]];
aug[[max_row, j]] = temp;
}
for i in k + 1..n {
let factor = aug[[i, k]] / aug[[k, k]];
for j in k..n + 1 {
aug[[i, j]] -= factor * aug[[k, j]];
}
}
}
let mut x = Array1::zeros(n);
for i in (0..n).rev() {
x[i] = aug[[i, n]];
for j in i + 1..n {
x[i] -= aug[[i, j]] * x[j];
}
x[i] /= aug[[i, i]];
}
Ok(x)
}
fn compute_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();
Ok(vec![
Complex64::new((trace + sqrt_disc) / 2.0, 0.0),
Complex64::new((trace - sqrt_disc) / 2.0, 0.0),
])
} else {
let sqrt_disc = (-discriminant).sqrt();
Ok(vec![
Complex64::new(trace / 2.0, sqrt_disc / 2.0),
Complex64::new(trace / 2.0, -sqrt_disc / 2.0),
])
}
} else {
Err(IntegrateError::InvalidInput(
"Only 2x2 matrices supported".to_string(),
))
}
}
fn detect_bifurcation(&self, eigenvalues: &[Complex64]) -> Option<BifurcationType> {
for eigenval in eigenvalues {
if eigenval.im == 0.0 && eigenval.re.abs() < 1e-6 {
return Some(BifurcationType::SaddleNode);
}
if eigenval.im != 0.0 && eigenval.re.abs() < 1e-6 {
return Some(BifurcationType::Hopf);
}
}
None
}
fn classify_stability(&self, eigenvalues: &[Complex64]) -> StabilityType {
for eigenval in eigenvalues {
if eigenval.re > 1e-12 {
return StabilityType::Unstable;
}
if eigenval.re.abs() < 1e-12 {
return StabilityType::Marginally;
}
}
StabilityType::Stable
}
}
pub struct MonodromyAnalyzer {
pub period: f64,
pub tol: f64,
pub n_steps: usize,
}
impl MonodromyAnalyzer {
pub fn new(period: f64, nsteps: usize) -> Self {
Self {
period,
tol: 1e-8,
n_steps: nsteps,
}
}
pub fn compute_monodromy_matrix<F>(
&self,
system: F,
initial_state: &Array1<f64>,
) -> IntegrateResult<MonodromyResult>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let n = initial_state.len();
let dt = self.period / self.n_steps as f64;
let mut fundamental_matrix = Array2::eye(n);
let mut current_state = initial_state.clone();
for _ in 0..self.n_steps {
let jac = self.numerical_jacobian(&system, ¤t_state)?;
fundamental_matrix = &fundamental_matrix + &(jac.dot(&fundamental_matrix) * dt);
let k1 = system(¤t_state);
let k2 = system(&(¤t_state + &(&k1 * (dt / 2.0))));
let k3 = system(&(¤t_state + &(&k2 * (dt / 2.0))));
let k4 = system(&(¤t_state + &(&k3 * dt)));
current_state = ¤t_state + &((&k1 + &k2 * 2.0 + &k3 * 2.0 + &k4) * (dt / 6.0));
}
let eigenvalues = self.compute_eigenvalues(&fundamental_matrix)?;
let stability = self.classify_periodic_stability(&eigenvalues);
Ok(MonodromyResult {
monodromy_matrix: fundamental_matrix,
eigenvalues,
stability,
period: self.period,
final_state: current_state,
})
}
fn numerical_jacobian<F>(&self, system: &F, x: &Array1<f64>) -> IntegrateResult<Array2<f64>>
where
F: Fn(&Array1<f64>) -> Array1<f64>,
{
let n = x.len();
let mut jac = Array2::zeros((n, n));
let h = 1e-8;
for j in 0..n {
let mut x_plus = x.clone();
let mut x_minus = x.clone();
x_plus[j] += h;
x_minus[j] -= h;
let f_plus = system(&x_plus);
let f_minus = system(&x_minus);
for i in 0..n {
jac[[i, j]] = (f_plus[i] - f_minus[i]) / (2.0 * h);
}
}
Ok(jac)
}
fn compute_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();
Ok(vec![
Complex64::new((trace + sqrt_disc) / 2.0, 0.0),
Complex64::new((trace - sqrt_disc) / 2.0, 0.0),
])
} else {
let sqrt_disc = (-discriminant).sqrt();
Ok(vec![
Complex64::new(trace / 2.0, sqrt_disc / 2.0),
Complex64::new(trace / 2.0, -sqrt_disc / 2.0),
])
}
} else {
Err(IntegrateError::InvalidInput(
"Only 2x2 matrices supported".to_string(),
))
}
}
fn classify_periodic_stability(&self, multipliers: &[Complex64]) -> PeriodicStabilityType {
let max_magnitude = multipliers.iter().map(|m| m.norm()).fold(0.0, f64::max);
if max_magnitude > 1.0 + 1e-6 {
PeriodicStabilityType::Unstable
} else if (max_magnitude - 1.0).abs() < 1e-6 {
PeriodicStabilityType::Marginally
} else {
PeriodicStabilityType::Stable
}
}
}
#[derive(Debug, Clone)]
pub struct ContinuationResult {
pub bifurcation_points: Vec<BifurcationPointData>,
pub fixed_points: Vec<FixedPointData>,
pub parameter_range: (f64, f64),
}
#[derive(Debug, Clone)]
pub struct FixedPointData {
pub parameter: f64,
pub state: Array1<f64>,
pub eigenvalues: Vec<Complex64>,
pub stability: StabilityType,
}
#[derive(Debug, Clone)]
pub struct BifurcationPointData {
pub parameter: f64,
pub state: Array1<f64>,
pub bifurcation_type: BifurcationType,
}
#[derive(Debug, Clone)]
pub struct MonodromyResult {
pub monodromy_matrix: Array2<f64>,
pub eigenvalues: Vec<Complex64>,
pub stability: PeriodicStabilityType,
pub period: f64,
pub final_state: Array1<f64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BifurcationType {
SaddleNode,
Hopf,
PeriodDoubling,
Transcritical,
Pitchfork,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PeriodicStabilityType {
Stable,
Unstable,
Marginally,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_poincare_analyzer() {
let mut trajectory = Vec::new();
let mut times = Vec::new();
for i in 0..100 {
let t = i as f64 * 0.1;
let x = t.cos();
let y = t.sin();
let z = 0.1 * t.sin();
trajectory.push(Array1::from_vec(vec![x, y, z]));
times.push(t);
}
let section_normal = Array1::from_vec(vec![0.0, 0.0, 1.0]);
let section_point = Array1::from_vec(vec![0.0, 0.0, 0.0]);
let analyzer = PoincareAnalyzer::new(section_normal, section_point, 1);
let result = analyzer
.analyze_trajectory(&trajectory, ×)
.expect("Operation failed");
assert!(!result.crossings.is_empty());
}
#[test]
fn test_lyapunov_calculator() {
let system =
|state: &Array1<f64>| -> Array1<f64> { Array1::from_vec(vec![-state[0], -state[1]]) };
let calculator = LyapunovCalculator::new(2, 0.01);
let initial_state = Array1::from_vec(vec![1.0, 1.0]);
let exponents = calculator
.calculate_lyapunov_exponents(system, &initial_state, 10.0)
.expect("Operation failed");
assert!(exponents[0] < 0.0);
assert!(exponents[1] < 0.0);
}
#[test]
fn test_fractal_analyzer() {
let mut points = Vec::new();
let mut rng = scirs2_core::random::rng();
for _i in 0..500 {
let x = rng.random::<f64>() * 2.0 - 1.0; let y = rng.random::<f64>() * 2.0 - 1.0; let point = Array1::from_vec(vec![x, y]);
points.push(point);
}
let mut analyzer = FractalAnalyzer::new();
analyzer.scale_range = (0.1, 0.5); analyzer.n_scales = 5; analyzer.box_counting_method = BoxCountingMethod::Standard;
let result = analyzer
.calculate_fractal_dimension(&points)
.expect("Operation failed");
assert!(result.dimension.is_finite(), "Dimension should be finite");
assert!(
result.dimension > 0.0,
"Dimension should be positive, got: {}",
result.dimension
);
assert!(
result.dimension <= 3.0,
"Dimension should not exceed embedding dimension, got: {}",
result.dimension
);
assert!(
result.dimension >= 1.5 && result.dimension <= 2.5,
"2D filled area should have dimension between 1.5 and 2.5, got: {}",
result.dimension
);
assert!(
result.r_squared >= 0.0 && result.r_squared <= 1.0,
"R-squared should be in [0,1], got: {}",
result.r_squared
);
println!(
"Fractal dimension: {}, R²: {}",
result.dimension, result.r_squared
);
}
#[test]
fn test_recurrence_analyzer() {
let timeseries: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
let analyzer = RecurrenceAnalyzer::new(0.1, 3, 1);
let result = analyzer
.analyze_recurrence(×eries)
.expect("Operation failed");
assert!(result.rqa_measures.recurrence_rate > 0.0);
assert!(result.rqa_measures.recurrence_rate <= 1.0);
assert!(result.rqa_measures.determinism >= 0.0);
assert!(result.rqa_measures.determinism <= 1.0);
}
}