use oldies_core::{OdeSystem, Result, StateVector, Time, OldiesError};
use nalgebra::{DMatrix, DVector};
use ndarray::Array1;
use num_complex::Complex64;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BifurcationType {
SaddleNode,
Transcritical,
Pitchfork,
Hopf { supercritical: bool },
PeriodDoubling,
LimitPointCycles,
Torus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FixedPoint {
pub state: Vec<f64>,
pub parameter: f64,
pub eigenvalues: Vec<Complex64>,
pub stable: bool,
pub point_type: FixedPointType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FixedPointType {
StableNode,
UnstableNode,
StableFocus,
UnstableFocus,
Saddle,
Center,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LimitCycle {
pub period: f64,
pub amplitude: f64,
pub parameter: f64,
pub floquet_multipliers: Vec<Complex64>,
pub stable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BifurcationPoint {
pub bifurcation_type: BifurcationType,
pub parameter: f64,
pub state: Vec<f64>,
pub info: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BifurcationDiagram {
pub parameter_name: String,
pub parameter_range: (f64, f64),
pub state_index: usize,
pub fixed_points: Vec<FixedPoint>,
pub limit_cycles: Vec<LimitCycle>,
pub bifurcations: Vec<BifurcationPoint>,
}
#[derive(Debug, Clone)]
pub struct XppModel {
pub name: String,
pub variables: Vec<String>,
pub parameters: Vec<(String, f64)>,
dimension: usize,
}
impl XppModel {
pub fn new(name: &str, variables: Vec<String>) -> Self {
let dimension = variables.len();
Self {
name: name.to_string(),
variables,
parameters: Vec::new(),
dimension,
}
}
pub fn add_parameter(&mut self, name: &str, value: f64) {
self.parameters.push((name.to_string(), value));
}
pub fn get_parameter(&self, name: &str) -> Option<f64> {
self.parameters.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| *v)
}
pub fn set_parameter(&mut self, name: &str, value: f64) -> Result<()> {
for (n, v) in &mut self.parameters {
if n == name {
*v = value;
return Ok(());
}
}
Err(OldiesError::ModelNotFound(format!("Parameter {} not found", name)))
}
}
pub struct BifurcationAnalyzer {
model: XppModel,
tolerance: f64,
max_iterations: usize,
}
impl BifurcationAnalyzer {
pub fn new(model: XppModel) -> Self {
Self {
model,
tolerance: 1e-10,
max_iterations: 100,
}
}
pub fn find_fixed_points<F>(&self, rhs: F, initial_guesses: &[Vec<f64>]) -> Vec<FixedPoint>
where
F: Fn(&[f64], &[(String, f64)]) -> Vec<f64>,
{
let mut fixed_points = Vec::new();
for guess in initial_guesses {
if let Some(fp) = self.newton_raphson(&rhs, guess) {
let is_new = fixed_points.iter().all(|existing: &FixedPoint| {
let dist: f64 = existing.state.iter()
.zip(&fp)
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
dist > self.tolerance * 100.0
});
if is_new {
let jacobian = self.numerical_jacobian(&rhs, &fp);
let eigenvalues = self.compute_eigenvalues(&jacobian);
let stable = eigenvalues.iter().all(|e| e.re < 0.0);
let point_type = classify_fixed_point(&eigenvalues);
fixed_points.push(FixedPoint {
state: fp,
parameter: 0.0, eigenvalues,
stable,
point_type,
});
}
}
}
fixed_points
}
fn newton_raphson<F>(&self, rhs: F, initial: &[f64]) -> Option<Vec<f64>>
where
F: Fn(&[f64], &[(String, f64)]) -> Vec<f64>,
{
let mut x = initial.to_vec();
let n = x.len();
for _ in 0..self.max_iterations {
let f = rhs(&x, &self.model.parameters);
let norm: f64 = f.iter().map(|v| v.powi(2)).sum::<f64>().sqrt();
if norm < self.tolerance {
return Some(x);
}
let jacobian = self.numerical_jacobian(&rhs, &x);
let j = DMatrix::from_row_slice(n, n, &jacobian);
let f_vec = DVector::from_vec(f.iter().map(|v| -*v).collect());
if let Some(lu) = j.lu().solve(&f_vec) {
for i in 0..n {
x[i] += lu[i];
}
} else {
return None; }
}
None
}
fn numerical_jacobian<F>(&self, rhs: F, x: &[f64]) -> Vec<f64>
where
F: Fn(&[f64], &[(String, f64)]) -> Vec<f64>,
{
let n = x.len();
let h = 1e-8;
let mut jacobian = vec![0.0; n * n];
let f0 = rhs(x, &self.model.parameters);
for j in 0..n {
let mut x_plus = x.to_vec();
x_plus[j] += h;
let f_plus = rhs(&x_plus, &self.model.parameters);
for i in 0..n {
jacobian[i * n + j] = (f_plus[i] - f0[i]) / h;
}
}
jacobian
}
fn compute_eigenvalues(&self, matrix: &[f64]) -> Vec<Complex64> {
let n = (matrix.len() as f64).sqrt() as usize;
let m = DMatrix::from_row_slice(n, n, matrix);
if let Some(eigen) = m.clone().try_symmetric_eigen(1e-10, 1000) {
eigen.eigenvalues.iter()
.map(|&v| Complex64::new(v, 0.0))
.collect()
} else {
vec![Complex64::new(0.0, 0.0); n]
}
}
}
fn classify_fixed_point(eigenvalues: &[Complex64]) -> FixedPointType {
let all_real = eigenvalues.iter().all(|e| e.im.abs() < 1e-10);
let all_negative = eigenvalues.iter().all(|e| e.re < 0.0);
let all_positive = eigenvalues.iter().all(|e| e.re > 0.0);
let any_zero = eigenvalues.iter().any(|e| e.re.abs() < 1e-10);
if any_zero {
FixedPointType::Center
} else if all_real {
if all_negative {
FixedPointType::StableNode
} else if all_positive {
FixedPointType::UnstableNode
} else {
FixedPointType::Saddle
}
} else {
if all_negative {
FixedPointType::StableFocus
} else if all_positive {
FixedPointType::UnstableFocus
} else {
FixedPointType::Saddle
}
}
}
pub mod examples {
use super::*;
pub fn lorenz(sigma: f64, rho: f64, beta: f64) -> XppModel {
let mut model = XppModel::new("Lorenz", vec!["x".into(), "y".into(), "z".into()]);
model.add_parameter("sigma", sigma);
model.add_parameter("rho", rho);
model.add_parameter("beta", beta);
model
}
pub fn lorenz_rhs(state: &[f64], params: &[(String, f64)]) -> Vec<f64> {
let x = state[0];
let y = state[1];
let z = state[2];
let sigma = params.iter().find(|(n, _)| n == "sigma").map(|(_, v)| *v).unwrap_or(10.0);
let rho = params.iter().find(|(n, _)| n == "rho").map(|(_, v)| *v).unwrap_or(28.0);
let beta = params.iter().find(|(n, _)| n == "beta").map(|(_, v)| *v).unwrap_or(8.0/3.0);
vec![
sigma * (y - x),
x * (rho - z) - y,
x * y - beta * z,
]
}
pub fn fitzhugh_nagumo(a: f64, b: f64, epsilon: f64) -> XppModel {
let mut model = XppModel::new("FitzHugh-Nagumo", vec!["v".into(), "w".into()]);
model.add_parameter("a", a);
model.add_parameter("b", b);
model.add_parameter("epsilon", epsilon);
model
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lorenz_model() {
let model = examples::lorenz(10.0, 28.0, 8.0/3.0);
assert_eq!(model.variables.len(), 3);
assert_eq!(model.get_parameter("sigma"), Some(10.0));
}
#[test]
fn test_lorenz_rhs() {
let model = examples::lorenz(10.0, 28.0, 8.0/3.0);
let state = vec![1.0, 1.0, 1.0];
let rhs = examples::lorenz_rhs(&state, &model.parameters);
assert_eq!(rhs.len(), 3);
assert!((rhs[0] - 0.0).abs() < 1e-10); }
#[test]
fn test_fixed_point_classification() {
let eig = vec![Complex64::new(-1.0, 0.0), Complex64::new(-2.0, 0.0)];
assert_eq!(classify_fixed_point(&eig), FixedPointType::StableNode);
let eig = vec![Complex64::new(-1.0, 0.0), Complex64::new(1.0, 0.0)];
assert_eq!(classify_fixed_point(&eig), FixedPointType::Saddle);
}
}