pub fn cid_ce(series: &[f64], normalize: bool) -> f64 {
if series.len() < 2 {
return 0.0;
}
if normalize {
let n = series.len() as f64;
let m: f64 = series.iter().sum::<f64>() / n;
let std: f64 = (series.iter().map(|x| (x - m).powi(2)).sum::<f64>() / n).sqrt();
if std < 1e-10 {
return 0.0; }
let normalized: Vec<f64> = series.iter().map(|x| (x - m) / std).collect();
let sum_sq_diff: f64 = normalized.windows(2).map(|w| (w[1] - w[0]).powi(2)).sum();
sum_sq_diff.sqrt()
} else {
let sum_sq_diff: f64 = series.windows(2).map(|w| (w[1] - w[0]).powi(2)).sum();
sum_sq_diff.sqrt()
}
}
pub fn c3(series: &[f64], lag: usize) -> f64 {
if series.len() <= 2 * lag {
return f64::NAN;
}
let n = series.len() - 2 * lag;
let sum: f64 = (0..n)
.map(|i| series[i] * series[i + lag] * series[i + 2 * lag])
.sum();
sum / n as f64
}
pub fn lempel_ziv_complexity(series: &[f64], bins: usize) -> f64 {
if series.len() < 2 || bins == 0 {
return 0.0;
}
let n = series.len();
let min_val = series.iter().copied().fold(f64::INFINITY, f64::min);
let max_val = series.iter().copied().fold(f64::NEG_INFINITY, f64::max);
if (max_val - min_val).abs() < 1e-10 {
return 1.0 / n as f64;
}
let bin_width = (max_val - min_val) / bins as f64;
let sequence: Vec<usize> = series
.iter()
.map(|&x| {
let bin = ((x - min_val) / bin_width).floor() as usize;
bin.min(bins - 1)
})
.collect();
let mut sub_strings: std::collections::HashSet<Vec<usize>> = std::collections::HashSet::new();
let mut ind = 0;
let mut inc = 1;
while ind + inc <= n {
let sub_str: Vec<usize> = sequence[ind..ind + inc].to_vec();
if sub_strings.contains(&sub_str) {
inc += 1;
} else {
sub_strings.insert(sub_str);
ind += inc;
inc = 1;
}
}
sub_strings.len() as f64 / n as f64
}
pub fn lempel_ziv_complexity_binary(series: &[f64]) -> f64 {
if series.len() < 2 {
return 0.0;
}
let median = compute_median(series);
let binary: Vec<u8> = series
.iter()
.map(|&x| if x >= median { 1 } else { 0 })
.collect();
let n = binary.len();
let mut i = 0;
let mut c = 1; let mut k = 1;
let mut k_max = 1;
let mut l = 1;
while l + k <= n {
let substring: &[u8] = &binary[i..i + k];
let search_space: &[u8] = &binary[0..l];
let found = (0..search_space.len().saturating_sub(k - 1))
.any(|j| &search_space[j..j + k] == substring);
if found {
k += 1;
if k > k_max {
k_max = k;
}
} else {
c += 1;
l += k_max;
i = l;
k = 1;
k_max = 1;
}
}
let n_f = n as f64;
let b = 2.0; let max_complexity = n_f / n_f.log(b);
c as f64 / max_complexity
}
fn compute_median(series: &[f64]) -> f64 {
let mut sorted = series.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
if n % 2 == 0 {
(sorted[n / 2 - 1] + sorted[n / 2]) / 2.0
} else {
sorted[n / 2]
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn cid_ce_constant() {
let series = vec![5.0; 10];
assert_relative_eq!(cid_ce(&series, false), 0.0, epsilon = 1e-10);
assert_relative_eq!(cid_ce(&series, true), 0.0, epsilon = 1e-10);
}
#[test]
fn cid_ce_linear_unnormalized() {
let series: Vec<f64> = (0..10).map(|i| i as f64).collect();
assert_relative_eq!(cid_ce(&series, false), 3.0, epsilon = 1e-10);
}
#[test]
fn cid_ce_linear_normalized() {
let series: Vec<f64> = (0..10).map(|i| i as f64).collect();
let result = cid_ce(&series, true);
assert!(
result > 1.0 && result < 1.1,
"Expected ~1.044, got {}",
result
);
}
#[test]
fn cid_ce_complex() {
let simple: Vec<f64> = (0..20).map(|i| i as f64).collect();
let complex: Vec<f64> = (0..20)
.map(|i| if i % 2 == 0 { 0.0 } else { 10.0 })
.collect();
assert!(cid_ce(&complex, false) > cid_ce(&simple, false));
}
#[test]
fn cid_ce_short() {
assert_relative_eq!(cid_ce(&[], false), 0.0, epsilon = 1e-10);
assert_relative_eq!(cid_ce(&[1.0], false), 0.0, epsilon = 1e-10);
}
#[test]
fn c3_constant() {
let series = vec![2.0; 20];
assert_relative_eq!(c3(&series, 1), 8.0, epsilon = 1e-10);
}
#[test]
fn c3_linear() {
let series: Vec<f64> = (0..20).map(|i| i as f64).collect();
let result = c3(&series, 1);
assert!(!result.is_nan());
}
#[test]
fn c3_alternating() {
let series: Vec<f64> = (0..20)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
let result = c3(&series, 1);
assert!(!result.is_nan());
}
#[test]
fn c3_short() {
assert!(c3(&[], 1).is_nan());
assert!(c3(&[1.0, 2.0], 1).is_nan());
assert!(c3(&[1.0, 2.0, 3.0], 2).is_nan()); }
#[test]
fn c3_different_lags() {
let series: Vec<f64> = (0..50).map(|i| (i as f64 * 0.5).sin()).collect();
let c3_1 = c3(&series, 1);
let c3_5 = c3(&series, 5);
assert!(!c3_1.is_nan());
assert!(!c3_5.is_nan());
}
#[test]
fn lempel_ziv_constant() {
let series = vec![5.0; 20];
let lz = lempel_ziv_complexity(&series, 10);
assert_relative_eq!(lz, 1.0 / 20.0, epsilon = 1e-10);
}
#[test]
fn lempel_ziv_alternating() {
let series: Vec<f64> = (0..20)
.map(|i| if i % 2 == 0 { 0.0 } else { 1.0 })
.collect();
let lz = lempel_ziv_complexity(&series, 10);
assert!(!lz.is_nan());
assert!(lz > 0.0 && lz < 0.5);
}
#[test]
fn lempel_ziv_random_like() {
let series: Vec<f64> = (0..50).map(|i| ((i * 7 + 3) % 13) as f64).collect();
let lz = lempel_ziv_complexity(&series, 10);
assert!(!lz.is_nan());
assert!(lz > 0.0);
}
#[test]
fn lempel_ziv_short() {
assert_relative_eq!(lempel_ziv_complexity(&[], 10), 0.0, epsilon = 1e-10);
assert_relative_eq!(lempel_ziv_complexity(&[1.0], 10), 0.0, epsilon = 1e-10);
}
#[test]
fn lempel_ziv_zero_bins() {
let series = vec![1.0, 2.0, 3.0];
assert_relative_eq!(lempel_ziv_complexity(&series, 0), 0.0, epsilon = 1e-10);
}
#[test]
fn lempel_ziv_normalized_range() {
let series: Vec<f64> = (0..100).map(|i| ((i * 7 + 3) % 17) as f64).collect();
let lz = lempel_ziv_complexity(&series, 10);
assert!(
(0.0..=1.0).contains(&lz),
"LZ should be in [0, 1], got {}",
lz
);
}
#[test]
fn lempel_ziv_binary_works() {
let series: Vec<f64> = (0..50).map(|i| ((i * 7 + 3) % 13) as f64).collect();
let lz = lempel_ziv_complexity_binary(&series);
assert!(!lz.is_nan());
assert!(lz > 0.0);
}
}