pub fn largest_lyapunov_exponent(
series: &[f64],
m: usize,
tau: usize,
theiler: usize,
max_iter: usize,
) -> Option<f64> {
let n = series.len();
let n_embed = n.checked_sub((m - 1) * tau)?;
if n_embed < 2 * theiler + 2 || max_iter == 0 {
return None;
}
let mut embedding = vec![0.0_f64; n_embed * m];
for i in 0..n_embed {
for d in 0..m {
embedding[i * m + d] = series[i + d * tau];
}
}
let emb = |i: usize| &embedding[i * m..(i + 1) * m];
#[inline]
fn dist_sq(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y) * (x - y)).sum()
}
let mut nn_idx = vec![0usize; n_embed];
for i in 0..n_embed {
let vi = emb(i);
let mut best_d2 = f64::INFINITY;
let mut best_j = 0;
for j in 0..n_embed {
if (i as isize - j as isize).unsigned_abs() < theiler {
continue;
}
let d2 = dist_sq(vi, emb(j));
if d2 < best_d2 && d2 > 0.0 {
best_d2 = d2;
best_j = j;
}
}
nn_idx[i] = best_j;
}
let mut divergence = vec![0.0_f64; max_iter];
let mut counts = vec![0usize; max_iter];
for i in 0..n_embed {
let j = nn_idx[i];
for k in 0..max_iter {
let i_k = i + k;
let j_k = j + k;
if i_k >= n_embed || j_k >= n_embed {
break;
}
let d2 = dist_sq(emb(i_k), emb(j_k));
if d2 > 0.0 {
divergence[k] += 0.5 * d2.ln(); counts[k] += 1;
}
}
}
let curve: Vec<f64> = divergence
.iter()
.zip(counts.iter())
.map(|(&d, &c)| if c > 0 { d / c as f64 } else { f64::NAN })
.collect();
let valid: Vec<(f64, f64)> = curve
.iter()
.enumerate()
.filter(|(_, v)| v.is_finite())
.map(|(i, &v)| (i as f64, v))
.collect();
if valid.len() < 3 {
return None;
}
let use_n = (valid.len() / 2).max(3);
let pts = &valid[..use_n];
let mx = pts.iter().map(|p| p.0).sum::<f64>() / use_n as f64;
let my = pts.iter().map(|p| p.1).sum::<f64>() / use_n as f64;
let mut sxy = 0.0;
let mut sxx = 0.0;
for &(x, y) in pts {
sxy += (x - mx) * (y - my);
sxx += (x - mx) * (x - mx);
}
if sxx < 1e-30 {
return None;
}
Some(sxy / sxx)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lle_sine_is_near_zero_or_negative() {
let series: Vec<f64> = (0..500)
.map(|i| (i as f64 * 2.0 * std::f64::consts::PI / 50.0).sin())
.collect();
let lle = largest_lyapunov_exponent(&series, 3, 10, 20, 50);
assert!(lle.is_some());
let val = lle.unwrap();
assert!(
val < 0.5,
"periodic LLE should be near 0 or negative, got {}",
val
);
}
#[test]
fn lle_returns_none_for_short_series() {
let series = vec![1.0, 2.0, 3.0];
assert!(largest_lyapunov_exponent(&series, 3, 1, 2, 5).is_none());
}
#[test]
fn lle_logistic_map_is_positive() {
let n = 1000;
let mut series = vec![0.0; n];
series[0] = 0.1;
for i in 1..n {
series[i] = 3.9 * series[i - 1] * (1.0 - series[i - 1]);
}
let lle = largest_lyapunov_exponent(&series, 3, 1, 5, 50);
assert!(lle.is_some());
let val = lle.unwrap();
assert!(
val > 0.0,
"chaotic logistic map LLE should be positive, got {}",
val
);
}
}