use super::gcmi::gcmi;
use super::knn_mi::knn_mutual_information;
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CmiBackend {
Linear,
}
pub fn ami_curve(series: &[f64], max_lag: usize) -> Vec<f64> {
ami_curve_with_k(series, max_lag, 8)
}
pub fn ami_curve_with_k(series: &[f64], max_lag: usize, k: usize) -> Vec<f64> {
let n = series.len();
#[cfg(feature = "parallel")]
{
(1..=max_lag)
.into_par_iter()
.map(|h| {
if n <= h + k {
0.0
} else {
knn_mutual_information(&series[..n - h], &series[h..], k)
}
})
.collect()
}
#[cfg(not(feature = "parallel"))]
{
(1..=max_lag)
.map(|h| {
if n <= h + k {
0.0
} else {
knn_mutual_information(&series[..n - h], &series[h..], k)
}
})
.collect()
}
}
pub fn gcmi_curve(series: &[f64], max_lag: usize) -> Vec<f64> {
let n = series.len();
let mut result = Vec::with_capacity(max_lag);
for h in 1..=max_lag {
if n <= h + 2 {
result.push(0.0);
continue;
}
let past = &series[..n - h];
let future = &series[h..];
result.push(gcmi(past, future));
}
result
}
pub fn pami_curve(series: &[f64], max_lag: usize, _backend: CmiBackend) -> Vec<f64> {
pami_curve_linear(series, max_lag)
}
fn pami_curve_linear(series: &[f64], max_lag: usize) -> Vec<f64> {
let n = series.len();
let k = 8;
let mut result = Vec::with_capacity(max_lag);
for h in 1..=max_lag {
if n <= h + k {
result.push(0.0);
continue;
}
let usable = n - h;
if h == 1 {
let past = &series[..usable];
let future = &series[h..h + usable];
result.push(knn_mutual_information(past, future, k));
continue;
}
let z_cols: Vec<Vec<f64>> = (1..h).map(|j| series[j..j + usable].to_vec()).collect();
let past = &series[..usable];
let future = &series[h..h + usable];
let past_resid = linear_residualize(past, &z_cols);
let future_resid = linear_residualize(future, &z_cols);
if past_resid.len() < k + 1 {
result.push(0.0);
continue;
}
result.push(knn_mutual_information(&past_resid, &future_resid, k));
}
result
}
pub(crate) fn linear_residualize(y: &[f64], z_cols: &[Vec<f64>]) -> Vec<f64> {
let n = y.len();
let p = z_cols.len();
if p == 0 || n < p + 1 {
return y.to_vec();
}
let mut ztz = vec![0.0; p * p];
let mut zty = vec![0.0; p];
for j in 0..p {
for k in j..p {
let dot: f64 = (0..n).map(|i| z_cols[j][i] * z_cols[k][i]).sum();
ztz[j * p + k] = dot;
ztz[k * p + j] = dot;
}
zty[j] = (0..n).map(|i| z_cols[j][i] * y[i]).sum();
}
let beta = match cholesky_solve(&ztz, &zty, p) {
Some(b) => b,
None => return y.to_vec(), };
let mut resid = Vec::with_capacity(n);
for i in 0..n {
let predicted: f64 = (0..p).map(|j| z_cols[j][i] * beta[j]).sum();
resid.push(y[i] - predicted);
}
resid
}
fn cholesky_solve(a: &[f64], b: &[f64], p: usize) -> Option<Vec<f64>> {
let mut l = vec![0.0; p * p];
for i in 0..p {
for j in 0..=i {
let mut sum = 0.0;
for k in 0..j {
sum += l[i * p + k] * l[j * p + k];
}
if i == j {
let diag = a[i * p + i] - sum;
if diag <= 1e-15 {
return None; }
l[i * p + j] = diag.sqrt();
} else {
l[i * p + j] = (a[i * p + j] - sum) / l[j * p + j];
}
}
}
let mut z = vec![0.0; p];
for i in 0..p {
let mut sum = 0.0;
for j in 0..i {
sum += l[i * p + j] * z[j];
}
z[i] = (b[i] - sum) / l[i * p + i];
}
let mut x = vec![0.0; p];
for i in (0..p).rev() {
let mut sum = 0.0;
for j in i + 1..p {
sum += l[j * p + i] * x[j];
}
x[i] = (z[i] - sum) / l[i * p + i];
}
Some(x)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ar1(n: usize, phi: f64, seed: u64) -> Vec<f64> {
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let mut series = Vec::with_capacity(n);
series.push(0.0);
for _ in 1..n {
let noise = (rng.gen::<f64>() - 0.5) * 2.0;
series.push(phi * *series.last().unwrap() + noise);
}
series
}
use rand::{Rng, SeedableRng};
#[test]
fn ami_curve_detects_ar1_structure() {
let series = make_ar1(500, 0.8, 42);
let curve = ami_curve(&series, 5);
assert_eq!(curve.len(), 5);
assert!(curve[0] > curve[4], "AMI should decay: {:?}", curve);
assert!(curve[0] > 0.1, "AMI(1) should be positive for AR(1)");
}
#[test]
fn gcmi_curve_length_correct() {
let series: Vec<f64> = (0..200).map(|i| (i as f64 * 0.1).sin()).collect();
let curve = gcmi_curve(&series, 10);
assert_eq!(curve.len(), 10);
}
#[test]
fn pami_lag1_equals_ami_lag1() {
let series = make_ar1(300, 0.6, 7);
let ami = ami_curve(&series, 1);
let pami = pami_curve(&series, 1, CmiBackend::Linear);
let ratio = if ami[0] > 0.01 { pami[0] / ami[0] } else { 1.0 };
assert!(
(0.8..1.2).contains(&ratio),
"pAMI(1) should match AMI(1): pami={:.4} ami={:.4}",
pami[0],
ami[0]
);
}
#[test]
fn pami_removes_indirect_dependence() {
let series = make_ar1(500, 0.8, 11);
let ami = ami_curve(&series, 3);
let pami = pami_curve(&series, 3, CmiBackend::Linear);
assert!(
pami[1] < ami[1] * 1.2,
"pAMI(2)={:.4} should be ≤ AMI(2)={:.4} for AR(1)",
pami[1],
ami[1]
);
}
}