pub fn phi_star(data: &[Vec<f64>], tau: usize) -> f64 {
let n = data.len();
if n < 2 || data[0].len() <= 2 * tau {
return 0.0;
}
let t = data[0].len();
let past: Vec<Vec<f64>> = data.iter().map(|ch| ch[..t - tau].to_vec()).collect();
let future: Vec<Vec<f64>> = data.iter().map(|ch| ch[tau..].to_vec()).collect();
let mi_whole = gaussian_mi(&past, &future);
let mut mi_parts_min = f64::INFINITY;
for k in 1..n {
let past_a: Vec<Vec<f64>> = past[..k].to_vec();
let fut_a: Vec<Vec<f64>> = future[..k].to_vec();
let past_b: Vec<Vec<f64>> = past[k..].to_vec();
let fut_b: Vec<Vec<f64>> = future[k..].to_vec();
let mi_parts = gaussian_mi(&past_a, &fut_a) + gaussian_mi(&past_b, &fut_b);
if mi_parts < mi_parts_min {
mi_parts_min = mi_parts;
}
}
(mi_whole - mi_parts_min).max(0.0)
}
fn gaussian_mi(x: &[Vec<f64>], y: &[Vec<f64>]) -> f64 {
let eps = 1e-10;
let cov_x = covariance_matrix(x, eps);
let cov_y = covariance_matrix(y, eps);
let mut xy: Vec<Vec<f64>> = x.to_vec();
xy.extend_from_slice(y);
let cov_xy = covariance_matrix(&xy, eps);
let det_x = determinant(&cov_x).max(1e-300);
let det_y = determinant(&cov_y).max(1e-300);
let det_xy = determinant(&cov_xy).max(1e-300);
let mi = 0.5 * (det_x * det_y / det_xy).ln();
mi.max(0.0)
}
fn covariance_matrix(data: &[Vec<f64>], eps: f64) -> Vec<Vec<f64>> {
let n = data.len();
if n == 0 {
return vec![];
}
let t = data[0].len();
if t == 0 {
return vec![vec![eps; n]; n];
}
let means: Vec<f64> = data
.iter()
.map(|ch| ch.iter().sum::<f64>() / t as f64)
.collect();
let mut cov = vec![vec![0.0; n]; n];
let t_f = t as f64;
let denom = (t_f - 1.0).max(1.0);
for i in 0..n {
for j in 0..=i {
let dot = crate::simd::dot_f64_dispatch(&data[i], &data[j]);
let val = (dot - t_f * means[i] * means[j]) / denom;
cov[i][j] = val;
cov[j][i] = val;
}
cov[i][i] += eps; }
cov
}
fn determinant(matrix: &[Vec<f64>]) -> f64 {
let n = matrix.len();
if n == 0 {
return 1.0;
}
if n == 1 {
return matrix[0][0];
}
if n == 2 {
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
}
let mut a: Vec<Vec<f64>> = matrix.to_vec();
let mut det = 1.0f64;
for col in 0..n {
let mut max_row = col;
let mut max_val = a[col][col].abs();
for row in (col + 1)..n {
if a[row][col].abs() > max_val {
max_val = a[row][col].abs();
max_row = row;
}
}
if max_val < 1e-300 {
return 0.0;
}
if max_row != col {
a.swap(col, max_row);
det = -det;
}
det *= a[col][col];
let pivot = a[col][col];
for row in (col + 1)..n {
let factor = a[row][col] / pivot;
for j in (col + 1)..n {
a[row][j] -= factor * a[col][j];
}
}
}
det
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_independent_channels_low_phi() {
let data = vec![
(0..200).map(|i| (i as f64 * 0.1).sin()).collect(),
(0..200).map(|i| (i as f64 * 0.3).cos()).collect(),
(0..200).map(|i| (i as f64 * 0.7).sin()).collect(),
];
let phi = phi_star(&data, 1);
assert!(phi < 0.5, "Expected low Phi for independent, got {phi}");
}
#[test]
fn test_correlated_channels_positive_phi() {
let data = vec![
(0..200)
.map(|i| (i as f64 * 0.1).sin() + (i as f64 * 0.05).cos() * 0.3)
.collect(),
(0..200)
.map(|i| (i as f64 * 0.1).sin() * 0.8 + (i as f64 * 0.2).sin() * 0.5)
.collect(),
(0..200)
.map(|i| (i as f64 * 0.1).sin() * 0.6 + (i as f64 * 0.15).cos() * 0.7)
.collect(),
];
let phi = phi_star(&data, 1);
assert!(phi >= 0.0, "Expected non-negative Phi, got {phi}");
}
#[test]
fn test_single_channel_zero() {
let data = vec![vec![1.0, 2.0, 3.0, 4.0, 5.0]];
assert_eq!(phi_star(&data, 1), 0.0);
}
#[test]
fn test_short_data_zero() {
let data = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
assert_eq!(phi_star(&data, 1), 0.0);
}
#[test]
fn test_determinant_2x2() {
let m = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let det = determinant(&m);
assert!((det - (-2.0)).abs() < 1e-10);
}
#[test]
fn test_determinant_3x3() {
let m = vec![
vec![1.0, 2.0, 3.0],
vec![0.0, 1.0, 4.0],
vec![5.0, 6.0, 0.0],
];
let det = determinant(&m);
assert!((det - 1.0).abs() < 1e-10);
}
}