#![allow(clippy::needless_range_loop)]
#![allow(dead_code)]
#[inline]
pub fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
#[inline]
pub fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
#[inline]
pub fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
[a[0] * s, a[1] * s, a[2] * s]
}
#[inline]
pub fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
#[inline]
pub fn norm3(a: [f64; 3]) -> f64 {
dot3(a, a).sqrt()
}
#[inline]
pub fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
pub trait OdeSystem {
fn dim(&self) -> usize;
fn rhs(&self, t: f64, x: &[f64]) -> Vec<f64>;
}
pub fn rk4_step(sys: &dyn OdeSystem, t: f64, x: &[f64], h: f64) -> Vec<f64> {
let n = x.len();
let k1 = sys.rhs(t, x);
let x2: Vec<f64> = (0..n).map(|i| x[i] + 0.5 * h * k1[i]).collect();
let k2 = sys.rhs(t + 0.5 * h, &x2);
let x3: Vec<f64> = (0..n).map(|i| x[i] + 0.5 * h * k2[i]).collect();
let k3 = sys.rhs(t + 0.5 * h, &x3);
let x4: Vec<f64> = (0..n).map(|i| x[i] + h * k3[i]).collect();
let k4 = sys.rhs(t + h, &x4);
(0..n)
.map(|i| x[i] + h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
.collect()
}
pub fn integrate_rk4(
sys: &dyn OdeSystem,
x0: &[f64],
t0: f64,
t_end: f64,
h: f64,
) -> Vec<(f64, Vec<f64>)> {
let mut result = Vec::new();
let mut t = t0;
let mut x = x0.to_vec();
result.push((t, x.clone()));
while t + h <= t_end + 1e-12 {
x = rk4_step(sys, t, &x, h);
t += h;
result.push((t, x.clone()));
}
result
}
pub fn numerical_jacobian(sys: &dyn OdeSystem, t: f64, x: &[f64], eps: f64) -> Vec<f64> {
let n = x.len();
let mut jac = vec![0.0f64; n * n];
for j in 0..n {
let mut xp = x.to_vec();
let mut xm = x.to_vec();
xp[j] += eps;
xm[j] -= eps;
let fp = sys.rhs(t, &xp);
let fm = sys.rhs(t, &xm);
for i in 0..n {
jac[i * n + j] = (fp[i] - fm[i]) / (2.0 * eps);
}
}
jac
}
#[derive(Debug, Clone)]
pub struct FixedPointResult {
pub point: Vec<f64>,
pub residual: f64,
pub converged: bool,
pub iterations: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn find_fixed_point(
sys: &dyn OdeSystem,
x0: &[f64],
tol: f64,
max_iter: usize,
eps_jac: f64,
) -> FixedPointResult {
let n = x0.len();
let mut x = x0.to_vec();
for iter in 0..max_iter {
let f = sys.rhs(0.0, &x);
let res: f64 = f.iter().map(|v| v * v).sum::<f64>().sqrt();
if res < tol {
return FixedPointResult {
point: x,
residual: res,
converged: true,
iterations: iter,
};
}
let jac = numerical_jacobian(sys, 0.0, &x, eps_jac);
let delta = gauss_solve(&jac, n, &f.iter().map(|v| -v).collect::<Vec<_>>());
match delta {
Some(d) => {
for i in 0..n {
x[i] += d[i];
}
}
None => break,
}
}
let f = sys.rhs(0.0, &x);
let res: f64 = f.iter().map(|v| v * v).sum::<f64>().sqrt();
FixedPointResult {
point: x,
residual: res,
converged: false,
iterations: max_iter,
}
}
fn gauss_solve(a_flat: &[f64], n: usize, b: &[f64]) -> Option<Vec<f64>> {
let mut mat: Vec<f64> = a_flat.to_vec();
let mut rhs: Vec<f64> = b.to_vec();
for col in 0..n {
let mut max_row = col;
let mut max_val = mat[col * n + col].abs();
for row in (col + 1)..n {
let v = mat[row * n + col].abs();
if v > max_val {
max_val = v;
max_row = row;
}
}
if max_val < 1e-14 {
return None;
}
if max_row != col {
for k in 0..n {
mat.swap(col * n + k, max_row * n + k);
}
rhs.swap(col, max_row);
}
let pivot = mat[col * n + col];
for row in (col + 1)..n {
let factor = mat[row * n + col] / pivot;
for k in col..n {
let val = mat[col * n + k];
mat[row * n + k] -= factor * val;
}
rhs[row] -= factor * rhs[col];
}
}
let mut x = vec![0.0f64; n];
for i in (0..n).rev() {
let mut sum = rhs[i];
for j in (i + 1)..n {
sum -= mat[i * n + j] * x[j];
}
x[i] = sum / mat[i * n + i];
}
Some(x)
}
pub fn eigen2(a: f64, b: f64, c: f64, d: f64) -> ((f64, f64), (f64, f64)) {
let tr = a + d;
let det = a * d - b * c;
let disc = tr * tr - 4.0 * det;
if disc >= 0.0 {
let sq = disc.sqrt();
(((tr + sq) / 2.0, 0.0), ((tr - sq) / 2.0, 0.0))
} else {
let sq = (-disc).sqrt();
((tr / 2.0, sq / 2.0), (tr / 2.0, -sq / 2.0))
}
}
pub fn stability_2d(a: f64, b: f64, c: f64, d: f64) -> &'static str {
let tr = a + d;
let det = a * d - b * c;
let disc = tr * tr - 4.0 * det;
if det < 0.0 {
"saddle"
} else if disc >= 0.0 {
if tr < -1e-10 {
"stable_node"
} else if tr > 1e-10 {
"unstable_node"
} else {
"center"
}
} else if tr < -1e-10 {
"stable_spiral"
} else if tr > 1e-10 {
"unstable_spiral"
} else {
"center"
}
}
pub fn lyapunov_exponents_qr(sys: &dyn OdeSystem, x0: &[f64], h: f64, n_steps: usize) -> Vec<f64> {
let n = sys.dim();
let mut x = x0.to_vec();
let mut q: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut v = vec![0.0f64; n];
v[i] = 1.0;
v
})
.collect();
let mut sums = vec![0.0f64; n];
let total_time = h * n_steps as f64;
for _step in 0..n_steps {
let t = _step as f64 * h;
x = rk4_step(sys, t, &x, h);
let jac = numerical_jacobian(sys, t, &x, 1e-6);
let mut new_q: Vec<Vec<f64>> = Vec::with_capacity(n);
for col in 0..n {
let mut w = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
w[i] += jac[i * n + j] * q[col][j];
}
}
let evolved: Vec<f64> = (0..n).map(|i| q[col][i] + h * w[i]).collect();
new_q.push(evolved);
}
let mut r_diag = vec![0.0f64; n];
for col in 0..n {
let mut v = new_q[col].clone();
for prev in 0..col {
let proj: f64 = (0..n).map(|i| q[prev][i] * v[i]).sum();
for i in 0..n {
v[i] -= proj * q[prev][i];
}
}
let norm: f64 = v.iter().map(|vi| vi * vi).sum::<f64>().sqrt();
r_diag[col] = norm;
if norm > 1e-14 {
q[col] = v.iter().map(|vi| vi / norm).collect();
}
sums[col] += if norm > 1e-14 { norm.ln() } else { -100.0 };
}
let _ = r_diag;
}
let mut exponents: Vec<f64> = sums.iter().map(|s| s / total_time).collect();
exponents.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
exponents
}
#[derive(Debug, Clone, PartialEq)]
pub enum BifurcationType {
SaddleNode,
Transcritical,
Pitchfork,
Hopf,
None,
}
#[derive(Debug, Clone)]
pub struct BifurcationEvent {
pub param_value: f64,
pub bif_type: BifurcationType,
pub fixed_point: Vec<f64>,
}
#[derive(Debug, Clone)]
pub struct LorenzSystem {
pub sigma: f64,
pub rho: f64,
pub beta: f64,
}
impl LorenzSystem {
pub fn new(sigma: f64, rho: f64, beta: f64) -> Self {
Self { sigma, rho, beta }
}
pub fn classic() -> Self {
Self::new(10.0, 28.0, 8.0 / 3.0)
}
pub fn fixed_points(&self) -> Vec<[f64; 3]> {
let mut fps = vec![[0.0, 0.0, 0.0]];
if self.rho > 1.0 {
let c = (self.beta * (self.rho - 1.0)).sqrt();
fps.push([c, c, self.rho - 1.0]);
fps.push([-c, -c, self.rho - 1.0]);
}
fps
}
}
impl OdeSystem for LorenzSystem {
fn dim(&self) -> usize {
3
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
vec![
self.sigma * (x[1] - x[0]),
x[0] * (self.rho - x[2]) - x[1],
x[0] * x[1] - self.beta * x[2],
]
}
}
#[derive(Debug, Clone)]
pub struct RosslerSystem {
pub a: f64,
pub b: f64,
pub c: f64,
}
impl RosslerSystem {
pub fn new(a: f64, b: f64, c: f64) -> Self {
Self { a, b, c }
}
pub fn classic() -> Self {
Self::new(0.2, 0.2, 5.7)
}
}
impl OdeSystem for RosslerSystem {
fn dim(&self) -> usize {
3
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
vec![
-x[1] - x[2],
x[0] + self.a * x[1],
self.b + x[2] * (x[0] - self.c),
]
}
}
#[derive(Debug, Clone)]
pub struct PoincareSectionConfig {
pub axis: usize,
pub level: f64,
pub positive_direction: bool,
}
#[derive(Debug, Clone)]
pub struct PoincareCrossing {
pub time: f64,
pub state: Vec<f64>,
}
pub fn poincare_section(
sys: &dyn OdeSystem,
x0: &[f64],
h: f64,
n_steps: usize,
cfg: &PoincareSectionConfig,
) -> Vec<PoincareCrossing> {
let mut crossings = Vec::new();
let mut t = 0.0f64;
let mut x_prev = x0.to_vec();
let mut x = rk4_step(sys, t, &x_prev, h);
for _i in 1..n_steps {
let v_prev = x_prev[cfg.axis] - cfg.level;
let v_cur = x[cfg.axis] - cfg.level;
let crossing = if cfg.positive_direction {
v_prev < 0.0 && v_cur >= 0.0
} else {
v_prev > 0.0 && v_cur <= 0.0
};
if crossing {
let frac = v_prev.abs() / (v_prev.abs() + v_cur.abs() + 1e-30);
let t_cross = t + frac * h;
let state: Vec<f64> = x_prev
.iter()
.zip(x.iter())
.map(|(a, b)| a + frac * (b - a))
.collect();
crossings.push(PoincareCrossing {
time: t_cross,
state,
});
}
x_prev = x.clone();
x = rk4_step(sys, t, &x, h);
t += h;
}
crossings
}
#[derive(Debug, Clone)]
pub struct LimitCycleEstimate {
pub period: f64,
pub n_cycles: usize,
pub found: bool,
}
pub fn estimate_limit_cycle_period(
sys: &dyn OdeSystem,
x0: &[f64],
h: f64,
n_steps: usize,
poincare_axis: usize,
level: f64,
) -> Option<LimitCycleEstimate> {
let cfg = PoincareSectionConfig {
axis: poincare_axis,
level,
positive_direction: true,
};
let crossings = poincare_section(sys, x0, h, n_steps, &cfg);
if crossings.len() < 2 {
return None;
}
let n = crossings.len();
let total_time = crossings[n - 1].time - crossings[0].time;
let period = total_time / (n - 1) as f64;
Some(LimitCycleEstimate {
period,
n_cycles: n - 1,
found: true,
})
}
#[derive(Debug, Clone)]
pub struct RqaParams {
pub embed_dim: usize,
pub tau: usize,
pub epsilon: f64,
pub min_line: usize,
}
#[derive(Debug, Clone)]
pub struct RqaResult {
pub rr: f64,
pub det: f64,
pub avg_line: f64,
pub l_max: usize,
pub entropy: f64,
pub lam: f64,
pub tt: f64,
}
pub fn delay_embed(series: &[f64], embed_dim: usize, tau: usize) -> Vec<Vec<f64>> {
let n = series.len();
let max_start = (embed_dim - 1) * tau;
if n <= max_start {
return Vec::new();
}
(0..=(n - 1 - max_start))
.map(|i| (0..embed_dim).map(|d| series[i + d * tau]).collect())
.collect()
}
pub fn recurrence_matrix(points: &[Vec<f64>], epsilon: f64) -> Vec<bool> {
let n = points.len();
let mut mat = vec![false; n * n];
for i in 0..n {
for j in 0..n {
let dist: f64 = points[i]
.iter()
.zip(points[j].iter())
.map(|(a, b)| (a - b).powi(2))
.sum::<f64>()
.sqrt();
mat[i * n + j] = dist < epsilon;
}
}
mat
}
pub fn rqa(series: &[f64], params: &RqaParams) -> RqaResult {
let points = delay_embed(series, params.embed_dim, params.tau);
let n = points.len();
if n == 0 {
return RqaResult {
rr: 0.0,
det: 0.0,
avg_line: 0.0,
l_max: 0,
entropy: 0.0,
lam: 0.0,
tt: 0.0,
};
}
let mat = recurrence_matrix(&points, params.epsilon);
let total_off = (n * n - n) as f64;
let recurrent_pts: usize = (0..n)
.flat_map(|i| (0..n).map(move |j| (i, j)))
.filter(|&(i, j)| i != j && mat[i * n + j])
.count();
let rr = if total_off > 0.0 {
recurrent_pts as f64 / total_off
} else {
0.0
};
let mut diag_lines: Vec<usize> = Vec::new();
for diag in (-(n as isize - 1))..(n as isize) {
if diag == 0 {
continue;
}
let mut run = 0usize;
let i_start = if diag < 0 { (-diag) as usize } else { 0 };
let j_start = if diag > 0 { diag as usize } else { 0 };
let len = n - i_start.max(j_start);
for k in 0..len {
let i = i_start + k;
let j = j_start + k;
if mat[i * n + j] {
run += 1;
} else {
if run >= params.min_line {
diag_lines.push(run);
}
run = 0;
}
}
if run >= params.min_line {
diag_lines.push(run);
}
}
let diag_total: usize = diag_lines.iter().sum();
let det = if recurrent_pts > 0 {
diag_total as f64 / recurrent_pts as f64
} else {
0.0
};
let l_max = diag_lines.iter().copied().max().unwrap_or(0);
let avg_line = if !diag_lines.is_empty() {
diag_total as f64 / diag_lines.len() as f64
} else {
0.0
};
let entropy = if !diag_lines.is_empty() {
let total = diag_total as f64;
let mut ent = 0.0f64;
let mut counts: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
for &l in &diag_lines {
*counts.entry(l).or_insert(0) += 1;
}
for &cnt in counts.values() {
let p = cnt as f64 / total;
if p > 0.0 {
ent -= p * p.ln();
}
}
ent
} else {
0.0
};
let mut vert_lines: Vec<usize> = Vec::new();
for col in 0..n {
let mut run = 0usize;
for row in 0..n {
if row != col && mat[row * n + col] {
run += 1;
} else {
if run >= params.min_line {
vert_lines.push(run);
}
run = 0;
}
}
if run >= params.min_line {
vert_lines.push(run);
}
}
let vert_total: usize = vert_lines.iter().sum();
let lam = if recurrent_pts > 0 {
vert_total as f64 / recurrent_pts as f64
} else {
0.0
};
let tt = if !vert_lines.is_empty() {
vert_total as f64 / vert_lines.len() as f64
} else {
0.0
};
RqaResult {
rr,
det,
avg_line,
l_max,
entropy,
lam,
tt,
}
}
pub trait HamiltonianSystem {
fn ndof(&self) -> usize;
fn hamiltonian(&self, q: &[f64], p: &[f64]) -> f64;
fn dh_dq(&self, q: &[f64], p: &[f64]) -> Vec<f64>;
fn dh_dp(&self, q: &[f64], p: &[f64]) -> Vec<f64>;
}
pub struct HamiltonianOde<'a> {
pub ham: &'a dyn HamiltonianSystem,
}
impl<'a> OdeSystem for HamiltonianOde<'a> {
fn dim(&self) -> usize {
2 * self.ham.ndof()
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
let n = self.ham.ndof();
let q = &x[..n];
let p = &x[n..];
let qdot = self.ham.dh_dp(q, p);
let dhdq = self.ham.dh_dq(q, p);
let pdot: Vec<f64> = dhdq.iter().map(|v| -v).collect();
[qdot, pdot].concat()
}
}
pub fn stormer_verlet_step(
q: &[f64],
p: &[f64],
h: f64,
grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
) -> (Vec<f64>, Vec<f64>) {
let n = q.len();
let f = grad_v(q);
let p_half: Vec<f64> = (0..n).map(|i| p[i] - 0.5 * h * f[i]).collect();
let q_new: Vec<f64> = (0..n).map(|i| q[i] + h * p_half[i]).collect();
let f2 = grad_v(&q_new);
let p_new: Vec<f64> = (0..n).map(|i| p_half[i] - 0.5 * h * f2[i]).collect();
(q_new, p_new)
}
pub fn forest_ruth_step(
q: &[f64],
p: &[f64],
h: f64,
grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
) -> (Vec<f64>, Vec<f64>) {
let theta: f64 = 1.0 / (2.0 - 2.0f64.powf(1.0 / 3.0));
let xi = 1.0 - 2.0 * theta;
let n = q.len();
let mut qq = q.to_vec();
let mut pp = p.to_vec();
for &c in &[theta, xi, theta] {
let f = grad_v(&qq);
for i in 0..n {
pp[i] -= 0.5 * c * h * f[i];
}
for i in 0..n {
qq[i] += c * h * pp[i];
}
let f2 = grad_v(&qq);
for i in 0..n {
pp[i] -= 0.5 * c * h * f2[i];
}
}
(qq, pp)
}
pub fn integrate_symplectic(
q0: &[f64],
p0: &[f64],
h: f64,
n_steps: usize,
grad_v: &dyn Fn(&[f64]) -> Vec<f64>,
) -> Vec<(Vec<f64>, Vec<f64>)> {
let mut traj = Vec::with_capacity(n_steps + 1);
let mut q = q0.to_vec();
let mut p = p0.to_vec();
traj.push((q.clone(), p.clone()));
for _ in 0..n_steps {
let (qn, pn) = stormer_verlet_step(&q, &p, h, grad_v);
q = qn;
p = pn;
traj.push((q.clone(), p.clone()));
}
traj
}
pub fn estimate_winding_number(q_traj: &[(Vec<f64>, Vec<f64>)], _period: f64) -> Option<f64> {
if q_traj.len() < 4 {
return None;
}
let mut crossings_0: usize = 0;
let mut crossings_1: usize = 0;
for w in q_traj.windows(2) {
if w[0].0[0] * w[1].0[0] < 0.0 {
crossings_0 += 1;
}
if w[0].0.len() > 1 && w[1].0.len() > 1 && w[0].0[1] * w[1].0[1] < 0.0 {
crossings_1 += 1;
}
}
if crossings_1 == 0 {
return None;
}
Some(crossings_0 as f64 / crossings_1 as f64)
}
pub fn phase_plane_grid(
sys: &dyn OdeSystem,
x_range: (f64, f64, usize),
y_range: (f64, f64, usize),
) -> Vec<(f64, f64, f64, f64)> {
let (x_min, x_max, nx) = x_range;
let (y_min, y_max, ny) = y_range;
let mut out = Vec::with_capacity(nx * ny);
for i in 0..nx {
let xv = x_min + (x_max - x_min) * i as f64 / (nx.saturating_sub(1).max(1)) as f64;
for j in 0..ny {
let yv = y_min + (y_max - y_min) * j as f64 / (ny.saturating_sub(1).max(1)) as f64;
let f = sys.rhs(0.0, &[xv, yv]);
out.push((xv, yv, f[0], f[1]));
}
}
out
}
#[derive(Debug, Clone)]
pub struct VanDerPol {
pub mu: f64,
}
impl VanDerPol {
pub fn new(mu: f64) -> Self {
Self { mu }
}
}
impl OdeSystem for VanDerPol {
fn dim(&self) -> usize {
2
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
vec![x[1], self.mu * (1.0 - x[0] * x[0]) * x[1] - x[0]]
}
}
#[derive(Debug, Clone)]
pub struct DoublePendulum {
pub g: f64,
}
impl DoublePendulum {
pub fn new(g: f64) -> Self {
Self { g }
}
pub fn integrate(&self, state0: [f64; 4], h: f64, n: usize) -> Vec<[f64; 4]> {
let mut traj = Vec::with_capacity(n + 1);
let mut s = state0;
traj.push(s);
for _ in 0..n {
let rhs = self.rhs_arr(s);
let k1 = rhs;
let s2 = [
s[0] + 0.5 * h * k1[0],
s[1] + 0.5 * h * k1[1],
s[2] + 0.5 * h * k1[2],
s[3] + 0.5 * h * k1[3],
];
let k2 = self.rhs_arr(s2);
let s3 = [
s[0] + 0.5 * h * k2[0],
s[1] + 0.5 * h * k2[1],
s[2] + 0.5 * h * k2[2],
s[3] + 0.5 * h * k2[3],
];
let k3 = self.rhs_arr(s3);
let s4 = [
s[0] + h * k3[0],
s[1] + h * k3[1],
s[2] + h * k3[2],
s[3] + h * k3[3],
];
let k4 = self.rhs_arr(s4);
for i in 0..4 {
s[i] += h / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]);
}
traj.push(s);
}
traj
}
fn rhs_arr(&self, s: [f64; 4]) -> [f64; 4] {
let (t1, t2, p1, p2) = (s[0], s[1], s[2], s[3]);
let dt = t1 - t2;
let denom = 16.0 - 9.0 * dt.cos() * dt.cos();
let t1dot = (6.0 * p1 - 3.0 * dt.cos() * p2) / denom;
let t2dot = (8.0 * p2 - 6.0 * dt.cos() * p1) / denom;
let h = t1dot * t2dot * dt.sin();
let p1dot = -3.0 * self.g * t1.sin() - h;
let p2dot = self.g * t2.sin() + h;
[t1dot, t2dot, p1dot, p2dot]
}
pub fn energy(&self, s: [f64; 4]) -> f64 {
let (t1, t2, p1, p2) = (s[0], s[1], s[2], s[3]);
let dt = t1 - t2;
let denom = 16.0 - 9.0 * dt.cos() * dt.cos();
let t1dot = (6.0 * p1 - 3.0 * dt.cos() * p2) / denom;
let t2dot = (8.0 * p2 - 6.0 * dt.cos() * p1) / denom;
let ke = 0.5 * t1dot * t1dot + 0.5 * t2dot * t2dot + t1dot * t2dot * dt.cos();
let pe = -self.g * (2.0 * t1.cos() + t2.cos());
ke + pe
}
}
#[derive(Debug, Clone)]
pub struct DuffingOscillator {
pub delta: f64,
pub alpha: f64,
pub beta: f64,
pub gamma: f64,
pub omega: f64,
}
impl DuffingOscillator {
#[allow(clippy::too_many_arguments)]
pub fn new(delta: f64, alpha: f64, beta: f64, gamma: f64, omega: f64) -> Self {
Self {
delta,
alpha,
beta,
gamma,
omega,
}
}
}
impl OdeSystem for DuffingOscillator {
fn dim(&self) -> usize {
2
}
fn rhs(&self, t: f64, x: &[f64]) -> Vec<f64> {
vec![
x[1],
-self.delta * x[1] - self.alpha * x[0] - self.beta * x[0].powi(3)
+ self.gamma * (self.omega * t).cos(),
]
}
}
pub fn saddle_node_fixed_points(mu: f64) -> Vec<f64> {
if mu > 0.0 {
Vec::new()
} else if mu == 0.0 {
vec![0.0]
} else {
let sq = (-mu).sqrt();
vec![-sq, sq]
}
}
pub fn pitchfork_fixed_points(mu: f64) -> Vec<f64> {
if mu <= 0.0 {
vec![0.0]
} else {
let sq = mu.sqrt();
vec![-sq, 0.0, sq]
}
}
pub fn is_hopf_bifurcation(tr: f64, det: f64, tol: f64) -> bool {
det > tol && tr.abs() < tol
}
pub fn maximal_lyapunov_exponent(
sys: &dyn OdeSystem,
x0: &[f64],
h: f64,
n_steps: usize,
renorm: usize,
) -> f64 {
let n = sys.dim();
let mut x = x0.to_vec();
let mut dv: Vec<f64> = (0..n).map(|i| if i == 0 { 1.0 } else { 0.0 }).collect();
let mut sum_log = 0.0f64;
let mut t = 0.0f64;
let mut count = 0usize;
for step in 0..n_steps {
let x_new = rk4_step(sys, t, &x, h);
let jac = numerical_jacobian(sys, t, &x, 1e-7);
let jdv: Vec<f64> = (0..n)
.map(|i| (0..n).map(|j| jac[i * n + j] * dv[j]).sum::<f64>())
.collect();
let dv_new: Vec<f64> = (0..n).map(|i| dv[i] + h * jdv[i]).collect();
x = x_new;
dv = dv_new;
t += h;
if (step + 1) % renorm == 0 {
let norm: f64 = dv.iter().map(|v| v * v).sum::<f64>().sqrt();
if norm > 1e-14 {
sum_log += norm.ln();
for vi in &mut dv {
*vi /= norm;
}
count += 1;
}
}
}
if count == 0 {
return 0.0;
}
sum_log / (count * renorm) as f64 / h
}
pub fn energy_drift(ham: &dyn HamiltonianSystem, traj: &[(Vec<f64>, Vec<f64>)]) -> f64 {
if traj.is_empty() {
return 0.0;
}
let n = ham.ndof();
let (q0, p0) = &traj[0];
let h0 = ham.hamiltonian(q0, p0);
if h0.abs() < 1e-30 {
return 0.0;
}
traj.iter()
.skip(1)
.map(|(q, p)| {
let _ = n;
((ham.hamiltonian(q, p) - h0) / h0).abs()
})
.fold(0.0f64, f64::max)
}
#[derive(Debug, Clone)]
pub struct HarmonicOscillatorHam {
pub omega: f64,
}
impl HarmonicOscillatorHam {
pub fn new(omega: f64) -> Self {
Self { omega }
}
}
impl HamiltonianSystem for HarmonicOscillatorHam {
fn ndof(&self) -> usize {
1
}
fn hamiltonian(&self, q: &[f64], p: &[f64]) -> f64 {
0.5 * p[0] * p[0] + 0.5 * self.omega * self.omega * q[0] * q[0]
}
fn dh_dq(&self, q: &[f64], _p: &[f64]) -> Vec<f64> {
vec![self.omega * self.omega * q[0]]
}
fn dh_dp(&self, _q: &[f64], p: &[f64]) -> Vec<f64> {
vec![p[0]]
}
}
#[allow(clippy::too_many_arguments)]
pub fn detect_heteroclinic(
sys: &dyn OdeSystem,
fp_a: &[f64],
fp_b: &[f64],
unstable_dir: &[f64],
eps_perturb: f64,
h: f64,
n_steps: usize,
tol: f64,
) -> Option<f64> {
let n = fp_a.len();
let mut x: Vec<f64> = (0..n)
.map(|i| fp_a[i] + eps_perturb * unstable_dir[i])
.collect();
let mut t = 0.0f64;
for _ in 0..n_steps {
let dist: f64 = (0..n).map(|i| (x[i] - fp_b[i]).powi(2)).sum::<f64>().sqrt();
if dist < tol {
return Some(t);
}
x = rk4_step(sys, t, &x, h);
t += h;
}
None
}
pub fn kaplan_yorke_dim(exponents: &[f64]) -> f64 {
let n = exponents.len();
let mut sum = 0.0f64;
let mut j = 0usize;
for (i, &le) in exponents.iter().enumerate() {
sum += le;
if sum < 0.0 {
j = i;
sum -= le;
break;
}
if i == n - 1 {
return n as f64;
}
j = i + 1;
}
if j == 0 && exponents[0] < 0.0 {
return 0.0;
}
let denom = exponents[j].abs();
if denom < 1e-14 {
return j as f64;
}
j as f64 + sum / denom
}
#[derive(Debug, Clone)]
pub struct KuramotoModel {
pub omegas: Vec<f64>,
pub k: f64,
}
impl KuramotoModel {
pub fn new(omegas: Vec<f64>, k: f64) -> Self {
Self { omegas, k }
}
pub fn order_parameter(theta: &[f64]) -> (f64, f64) {
let n = theta.len() as f64;
let re: f64 = theta.iter().map(|&t| t.cos()).sum::<f64>() / n;
let im: f64 = theta.iter().map(|&t| t.sin()).sum::<f64>() / n;
let r = (re * re + im * im).sqrt();
let psi = im.atan2(re);
(r, psi)
}
}
impl OdeSystem for KuramotoModel {
fn dim(&self) -> usize {
self.omegas.len()
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
let n = x.len();
let k_over_n = self.k / n as f64;
(0..n)
.map(|i| {
let coupling: f64 = (0..n).map(|j| (x[j] - x[i]).sin()).sum();
self.omegas[i] + k_over_n * coupling
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct Brusselator {
pub a: f64,
pub b: f64,
}
impl Brusselator {
pub fn new(a: f64, b: f64) -> Self {
Self { a, b }
}
pub fn fixed_point(&self) -> (f64, f64) {
(self.a, self.b / self.a)
}
pub fn hopf_b(&self) -> f64 {
1.0 + self.a * self.a
}
}
impl OdeSystem for Brusselator {
fn dim(&self) -> usize {
2
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
vec![
self.a - (self.b + 1.0) * x[0] + x[0] * x[0] * x[1],
self.b * x[0] - x[0] * x[0] * x[1],
]
}
}
#[cfg(test)]
mod tests {
use super::*;
struct ShoOde {
omega: f64,
}
impl OdeSystem for ShoOde {
fn dim(&self) -> usize {
2
}
fn rhs(&self, _t: f64, x: &[f64]) -> Vec<f64> {
vec![x[1], -self.omega * self.omega * x[0]]
}
}
#[test]
fn test_rk4_sho_energy_conservation() {
let sys = ShoOde { omega: 1.0 };
let x0 = vec![1.0, 0.0];
let energy0 = 0.5 * (x0[0] * x0[0] + x0[1] * x0[1]);
let h = 0.01;
let mut x = x0.clone();
for i in 0..628 {
x = rk4_step(&sys, i as f64 * h, &x, h);
}
let energy1 = 0.5 * (x[0] * x[0] + x[1] * x[1]);
assert!((energy1 - energy0).abs() < 1e-4);
}
#[test]
fn test_integrate_rk4_step_count() {
let sys = ShoOde { omega: 1.0 };
let traj = integrate_rk4(&sys, &[1.0, 0.0], 0.0, 1.0, 0.1);
assert_eq!(traj.len(), 11);
}
#[test]
fn test_numerical_jacobian_sho() {
let sys = ShoOde { omega: 2.0 };
let jac = numerical_jacobian(&sys, 0.0, &[0.0, 0.0], 1e-5);
assert!((jac[0] - 0.0).abs() < 1e-6);
assert!((jac[1] - 1.0).abs() < 1e-6);
assert!((jac[2] - (-4.0)).abs() < 1e-6);
assert!((jac[3] - 0.0).abs() < 1e-6);
}
#[test]
fn test_find_fixed_point_sho_origin() {
let sys = ShoOde { omega: 1.0 };
let res = find_fixed_point(&sys, &[0.1, 0.1], 1e-10, 50, 1e-7);
assert!(res.converged);
assert!(res.point[0].abs() < 1e-6);
assert!(res.point[1].abs() < 1e-6);
}
#[test]
fn test_gauss_solve_2x2() {
let a = vec![2.0, 1.0, 1.0, 3.0];
let b = vec![5.0, 10.0];
let x = gauss_solve(&a, 2, &b).unwrap();
assert!((x[0] - 1.0).abs() < 1e-10);
assert!((x[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_eigen2_real() {
let ((r1, i1), (r2, i2)) = eigen2(3.0, 0.0, 0.0, 2.0);
assert!((r1 - 3.0).abs() < 1e-10 || (r1 - 2.0).abs() < 1e-10);
assert!(i1.abs() < 1e-10);
assert!(i2.abs() < 1e-10);
let _ = r2;
}
#[test]
fn test_eigen2_complex() {
let ((r1, i1), (_r2, _i2)) = eigen2(0.0, 1.0, -1.0, 0.0);
assert!(r1.abs() < 1e-10);
assert!((i1.abs() - 1.0).abs() < 1e-10);
}
#[test]
fn test_stability_saddle() {
let s = stability_2d(1.0, 0.0, 0.0, -1.0);
assert_eq!(s, "saddle");
}
#[test]
fn test_stability_stable_spiral() {
let s = stability_2d(-0.1, 1.0, -1.0, -0.1);
assert_eq!(s, "stable_spiral");
}
#[test]
fn test_lorenz_fixed_points_classic() {
let lor = LorenzSystem::classic();
let fps = lor.fixed_points();
assert_eq!(fps.len(), 3);
let c1 = fps[1];
assert!((c1[0] - c1[1]).abs() < 1e-10);
}
#[test]
fn test_lorenz_rhs_origin() {
let lor = LorenzSystem::classic();
let f = lor.rhs(0.0, &[0.0, 0.0, 0.0]);
assert!(f.iter().all(|v| v.abs() < 1e-12));
}
#[test]
fn test_rossler_dim() {
let ros = RosslerSystem::classic();
assert_eq!(ros.dim(), 3);
}
#[test]
fn test_poincare_sho_crossings() {
let sys = ShoOde { omega: 1.0 };
let cfg = PoincareSectionConfig {
axis: 0,
level: 0.0,
positive_direction: true,
};
let h = 0.01;
let n_steps = (10.0 * 2.0 * std::f64::consts::PI / h) as usize;
let crossings = poincare_section(&sys, &[1.0, 0.0], h, n_steps, &cfg);
assert!(
crossings.len() >= 8 && crossings.len() <= 12,
"expected ~10 crossings, got {}",
crossings.len()
);
}
#[test]
fn test_limit_cycle_period_sho() {
let sys = ShoOde { omega: 1.0 };
let h = 0.005;
let n_steps = (20.0 * 2.0 * std::f64::consts::PI / h) as usize;
let est = estimate_limit_cycle_period(&sys, &[1.0, 0.0], h, n_steps, 0, 0.0);
let est = est.unwrap();
let expected = 2.0 * std::f64::consts::PI;
assert!(
(est.period - expected).abs() < 0.05,
"period={} expected≈{}",
est.period,
expected
);
}
#[test]
fn test_delay_embed_shape() {
let series: Vec<f64> = (0..20).map(|i| i as f64).collect();
let pts = delay_embed(&series, 3, 2);
assert_eq!(pts.len(), 16);
assert_eq!(pts[0].len(), 3);
assert_eq!(pts[0], vec![0.0, 2.0, 4.0]);
}
#[test]
fn test_recurrence_matrix_diagonal() {
let pts = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![2.0, 0.0]];
let mat = recurrence_matrix(&pts, 0.5);
assert!(mat[0]);
assert!(mat[3 + 1]);
assert!(mat[2 * 3 + 2]);
}
#[test]
fn test_rqa_periodic() {
let series: Vec<f64> = (0..200)
.map(|i| (2.0 * std::f64::consts::PI * i as f64 / 20.0).sin())
.collect();
let params = RqaParams {
embed_dim: 2,
tau: 5,
epsilon: 0.2,
min_line: 2,
};
let res = rqa(&series, ¶ms);
assert!(res.rr > 0.0);
assert!(res.det >= 0.0);
}
#[test]
fn test_stormer_verlet_energy_sho() {
let omega = 1.0f64;
let grad_v = |q: &[f64]| vec![omega * omega * q[0]];
let traj = integrate_symplectic(&[1.0], &[0.0], 0.01, 1000, &grad_v);
let energy0 = 0.5 * 0.0_f64.powi(2) + 0.5 * omega * omega * 1.0_f64.powi(2);
let (q_end, p_end) = &traj[traj.len() - 1];
let energy_end = 0.5 * p_end[0].powi(2) + 0.5 * omega * omega * q_end[0].powi(2);
assert!(
(energy_end - energy0).abs() < 1e-4,
"energy drift = {}",
(energy_end - energy0).abs()
);
}
#[test]
fn test_forest_ruth_returns_same_dim() {
let grad_v = |q: &[f64]| vec![q[0]];
let (q_new, p_new) = forest_ruth_step(&[1.0], &[0.5], 0.1, &grad_v);
assert_eq!(q_new.len(), 1);
assert_eq!(p_new.len(), 1);
}
#[test]
fn test_hamiltonian_sho_energy_drift() {
let ham = HarmonicOscillatorHam::new(1.0);
let grad_v = |q: &[f64]| ham.dh_dq(q, &[0.0]);
let traj = integrate_symplectic(&[1.0], &[0.0], 0.01, 500, &grad_v);
let pairs: Vec<(Vec<f64>, Vec<f64>)> = traj;
let drift = energy_drift(&ham, &pairs);
assert!(drift < 1e-3, "energy drift = {}", drift);
}
#[test]
fn test_saddle_node_fps() {
let fps_neg = saddle_node_fixed_points(-4.0);
assert_eq!(fps_neg.len(), 2);
assert!((fps_neg[0].abs() - 2.0).abs() < 1e-10);
let fps_pos = saddle_node_fixed_points(1.0);
assert!(fps_pos.is_empty());
let fps_zero = saddle_node_fixed_points(0.0);
assert_eq!(fps_zero.len(), 1);
}
#[test]
fn test_pitchfork_fps() {
let fps = pitchfork_fixed_points(4.0);
assert_eq!(fps.len(), 3);
let fps_neg = pitchfork_fixed_points(-1.0);
assert_eq!(fps_neg.len(), 1);
}
#[test]
fn test_is_hopf() {
assert!(is_hopf_bifurcation(0.0, 1.0, 1e-6));
assert!(!is_hopf_bifurcation(0.5, 1.0, 1e-6));
assert!(!is_hopf_bifurcation(0.0, -1.0, 1e-6));
}
#[test]
fn test_brusselator_fixed_point() {
let br = Brusselator::new(2.0, 3.0);
let (xfp, yfp) = br.fixed_point();
let f = br.rhs(0.0, &[xfp, yfp]);
assert!(f[0].abs() < 1e-10);
assert!(f[1].abs() < 1e-10);
}
#[test]
fn test_brusselator_hopf_b() {
let br = Brusselator::new(2.0, 5.0);
let hopf = br.hopf_b(); assert!((hopf - 5.0).abs() < 1e-10);
}
#[test]
fn test_vdp_origin_unstable() {
let vdp = VanDerPol::new(1.0);
let jac = numerical_jacobian(&vdp, 0.0, &[0.0, 0.0], 1e-5);
let tr = jac[0] + jac[3];
assert!(tr > 0.0);
}
#[test]
fn test_kuramoto_fully_synced() {
let theta = vec![1.0; 5];
let (r, _psi) = KuramotoModel::order_parameter(&theta);
assert!((r - 1.0).abs() < 1e-10);
}
#[test]
fn test_kuramoto_incoherent() {
use std::f64::consts::PI;
let n = 8;
let theta: Vec<f64> = (0..n).map(|i| 2.0 * PI * i as f64 / n as f64).collect();
let (r, _) = KuramotoModel::order_parameter(&theta);
assert!(r < 0.05);
}
#[test]
fn test_kaplan_yorke_dim() {
let les = vec![0.9, 0.0, -14.6];
let d = kaplan_yorke_dim(&les);
assert!(d > 2.0 && d < 2.2, "KY dim = {}", d);
}
#[test]
fn test_mle_lorenz_positive() {
let lor = LorenzSystem::classic();
let x0 = vec![0.1, 0.0, 0.0];
let mle = maximal_lyapunov_exponent(&lor, &x0, 0.01, 1000, 10);
assert!(mle > -1.0, "MLE = {}", mle);
}
#[test]
fn test_double_pendulum_energy() {
let dp = DoublePendulum::new(9.81);
let s0 = [0.1, 0.05, 0.0, 0.0];
let e0 = dp.energy(s0);
let traj = dp.integrate(s0, 0.001, 100);
let e_end = dp.energy(*traj.last().unwrap());
assert!(
(e_end - e0).abs() < 0.02,
"energy drift = {}",
(e_end - e0).abs()
);
}
#[test]
fn test_duffing_dim() {
let duf = DuffingOscillator::new(0.1, -1.0, 1.0, 0.3, 1.2);
assert_eq!(duf.dim(), 2);
let f = duf.rhs(0.0, &[0.0, 0.0]);
assert_eq!(f.len(), 2);
}
#[test]
fn test_phase_plane_grid_count() {
let sys = ShoOde { omega: 1.0 };
let grid = phase_plane_grid(&sys, (-2.0, 2.0, 5), (-2.0, 2.0, 5));
assert_eq!(grid.len(), 25);
}
#[test]
fn test_vec3_ops() {
let a = [1.0, 2.0, 3.0];
let b = [4.0, 5.0, 6.0];
let s = add3(a, b);
assert_eq!(s, [5.0, 7.0, 9.0]);
let d = sub3(b, a);
assert_eq!(d, [3.0, 3.0, 3.0]);
let sc = scale3(a, 2.0);
assert_eq!(sc, [2.0, 4.0, 6.0]);
assert!((dot3(a, b) - 32.0).abs() < 1e-10);
assert!((norm3([3.0, 4.0, 0.0]) - 5.0).abs() < 1e-10);
}
#[test]
fn test_cross3() {
let a = [1.0, 0.0, 0.0];
let b = [0.0, 1.0, 0.0];
let c = cross3(a, b);
assert!((c[0] - 0.0).abs() < 1e-10);
assert!((c[1] - 0.0).abs() < 1e-10);
assert!((c[2] - 1.0).abs() < 1e-10);
}
#[test]
fn test_rossler_rhs_values() {
let ros = RosslerSystem::new(0.2, 0.2, 5.7);
let f = ros.rhs(0.0, &[1.0, 0.0, 0.0]);
assert!((f[0] - 0.0).abs() < 1e-10);
assert!((f[1] - 1.0).abs() < 1e-10);
assert!((f[2] - 0.2).abs() < 1e-10);
}
#[test]
fn test_ham_sho_value() {
let ham = HarmonicOscillatorHam::new(2.0);
let h = ham.hamiltonian(&[1.0], &[0.0]);
assert!((h - 2.0).abs() < 1e-10);
}
#[test]
fn test_hamiltonian_ode_rhs() {
let ham = HarmonicOscillatorHam::new(1.0);
let ode = HamiltonianOde { ham: &ham };
let f = ode.rhs(0.0, &[0.0, 1.0]);
assert!((f[0] - 1.0).abs() < 1e-10);
assert!((f[1] - 0.0).abs() < 1e-10);
}
#[test]
fn test_lyapunov_qr_sho_near_zero() {
let sys = ShoOde { omega: 1.0 };
let les = lyapunov_exponents_qr(&sys, &[1.0, 0.0], 0.1, 200);
assert_eq!(les.len(), 2);
let sum: f64 = les.iter().sum();
assert!(sum.abs() < 1.0, "sum of LEs = {}", sum);
}
#[test]
fn test_detect_heteroclinic_none() {
let sys = ShoOde { omega: 1.0 };
let result = detect_heteroclinic(
&sys,
&[0.0, 0.0],
&[5.0, 0.0],
&[1.0, 0.0],
0.01,
0.01,
100,
0.1,
);
let _ = result;
}
}