pub fn delay_embedding(series: &[Vec<f32>], tau: usize, embed_dim: usize) -> Vec<Vec<f32>> {
let n = series.len();
if n == 0 || embed_dim == 0 {
return Vec::new();
}
let n_features = series[0].len();
let out_dim = n_features * embed_dim;
(0..n)
.map(|t| {
let mut row = vec![0.0f32; out_dim];
for lag in 0..embed_dim {
let src_t = (t as isize) - (lag * tau) as isize;
if src_t >= 0 {
let src = &series[src_t as usize];
let offset = lag * n_features;
row[offset..offset + n_features].copy_from_slice(src);
}
}
row
})
.collect()
}
pub fn correlation_dimension(points: &[Vec<f32>], r_min: f32, r_max: f32, n_steps: usize) -> f64 {
let n = points.len();
if n < 4 || r_min <= 0.0 || r_max <= r_min || n_steps < 2 {
return 0.0;
}
let log_rmin = (r_min as f64).ln();
let log_rmax = (r_max as f64).ln();
let radii: Vec<f64> = (0..n_steps)
.map(|i| {
let t = i as f64 / (n_steps - 1) as f64;
(log_rmin + t * (log_rmax - log_rmin)).exp()
})
.collect();
let total_pairs = (n * (n - 1) / 2) as f64;
let mut log_r_fit: Vec<f64> = Vec::with_capacity(n_steps);
let mut log_c_fit: Vec<f64> = Vec::with_capacity(n_steps);
for &r in &radii {
let mut count = 0usize;
for i in 0..n {
for j in (i + 1)..n {
if euclidean_dist_sq(&points[i], &points[j]) < (r * r) as f32 {
count += 1;
}
}
}
if count < 2 {
continue;
}
let c = count as f64 / total_pairs;
log_r_fit.push(r.ln());
log_c_fit.push(c.ln());
}
let m = log_r_fit.len();
if m < 2 {
return 0.0;
}
let mean_r = log_r_fit.iter().sum::<f64>() / m as f64;
let mean_c = log_c_fit.iter().sum::<f64>() / m as f64;
let num: f64 = log_r_fit
.iter()
.zip(log_c_fit.iter())
.map(|(r, c)| (r - mean_r) * (c - mean_c))
.sum();
let den: f64 = log_r_fit.iter().map(|r| (r - mean_r).powi(2)).sum();
if den.abs() < 1e-15 {
return 0.0;
}
(num / den).max(0.0)
}
pub fn min_cache_size_takens(dim_estimate: f64) -> usize {
2 * dim_estimate.ceil() as usize + 1
}
fn euclidean_dist_sq(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b.iter()).map(|(x, y)| (x - y).powi(2)).sum()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delay_embedding_shape() {
let series: Vec<Vec<f32>> = (0..10).map(|i| vec![i as f32, i as f32 + 1.0]).collect();
let embedded = delay_embedding(&series, 1, 3);
assert_eq!(embedded.len(), 10);
assert_eq!(embedded[0].len(), 6); }
#[test]
fn test_delay_embedding_tau1_lag0_matches_original() {
let series: Vec<Vec<f32>> = (0..5).map(|i| vec![i as f32]).collect();
let embedded = delay_embedding(&series, 1, 2);
for (t, row) in embedded.iter().enumerate() {
assert_eq!(row[0], t as f32, "lag-0 mismatch at t={t}");
}
}
#[test]
fn test_delay_embedding_zero_pads_before_start() {
let series: Vec<Vec<f32>> = (0..4).map(|i| vec![i as f32]).collect();
let embedded = delay_embedding(&series, 1, 3);
assert_eq!(embedded[0], vec![0.0, 0.0, 0.0]);
assert_eq!(embedded[1], vec![1.0, 0.0, 0.0]);
assert_eq!(embedded[2], vec![2.0, 1.0, 0.0]);
}
#[test]
fn test_delay_embedding_empty_series() {
let embedded = delay_embedding(&[], 1, 3);
assert!(embedded.is_empty());
}
#[test]
fn test_correlation_dim_line() {
let points: Vec<Vec<f32>> = (0..50).map(|i| vec![i as f32 / 49.0]).collect();
let d = correlation_dimension(&points, 0.01, 1.0, 20);
assert!(
(d - 1.0).abs() < 0.3,
"line should have corr dim ≈ 1.0, got {d:.3}"
);
}
#[test]
fn test_correlation_dim_circle() {
let points: Vec<Vec<f32>> = (0..60)
.map(|i| {
let theta = 2.0 * std::f32::consts::PI * i as f32 / 60.0;
vec![theta.cos(), theta.sin()]
})
.collect();
let d = correlation_dimension(&points, 0.05, 1.5, 20);
assert!(
(d - 1.0).abs() < 0.4,
"circle should have corr dim ≈ 1.0, got {d:.3}"
);
}
#[test]
fn test_correlation_dim_plane() {
let points: Vec<Vec<f32>> = (0..10)
.flat_map(|i| (0..10).map(move |j| vec![i as f32 / 9.0, j as f32 / 9.0]))
.collect();
let d = correlation_dimension(&points, 0.05, 1.0, 20);
assert!(
(d - 2.0).abs() < 0.5,
"plane should have corr dim ≈ 2.0, got {d:.3}"
);
}
#[test]
fn test_correlation_dim_degenerate() {
let d = correlation_dimension(&[vec![1.0f32], vec![2.0f32]], 0.1, 1.0, 10);
assert_eq!(d, 0.0);
}
#[test]
fn test_min_cache_size_formula() {
assert_eq!(min_cache_size_takens(1.0), 3); assert_eq!(min_cache_size_takens(2.0), 5); assert_eq!(min_cache_size_takens(64.0), 129); assert_eq!(min_cache_size_takens(1.5), 5); }
#[test]
fn test_min_cache_size_fractional_rounds_up() {
assert_eq!(min_cache_size_takens(0.7), 3);
assert_eq!(min_cache_size_takens(2.1), 7);
}
}