sc_neurocore_engine/
phi.rs1use nalgebra::DMatrix;
13
14pub fn phi_star(data: &[Vec<f64>], tau: usize) -> f64 {
17 let n = data.len();
18 if n < 2 || data[0].len() <= 2 * tau {
19 return 0.0;
20 }
21 let t = data[0].len();
22
23 let past: Vec<Vec<f64>> = data.iter().map(|ch| ch[..t - tau].to_vec()).collect();
25 let future: Vec<Vec<f64>> = data.iter().map(|ch| ch[tau..].to_vec()).collect();
26
27 let mi_whole = gaussian_mi(&past, &future);
28
29 let mut mi_parts_min = f64::INFINITY;
31 for k in 1..n {
32 let past_a: Vec<Vec<f64>> = past[..k].to_vec();
33 let fut_a: Vec<Vec<f64>> = future[..k].to_vec();
34 let past_b: Vec<Vec<f64>> = past[k..].to_vec();
35 let fut_b: Vec<Vec<f64>> = future[k..].to_vec();
36 let mi_parts = gaussian_mi(&past_a, &fut_a) + gaussian_mi(&past_b, &fut_b);
37 if mi_parts < mi_parts_min {
38 mi_parts_min = mi_parts;
39 }
40 }
41
42 (mi_whole - mi_parts_min).max(0.0)
43}
44
45fn gaussian_mi(x: &[Vec<f64>], y: &[Vec<f64>]) -> f64 {
52 let eps = 1e-10;
53 let cov_x = covariance_matrix(x, eps);
54 let cov_y = covariance_matrix(y, eps);
55
56 let mut xy: Vec<Vec<f64>> = x.to_vec();
57 xy.extend_from_slice(y);
58 let cov_xy = covariance_matrix(&xy, eps);
59
60 let mi = 0.5 * (logdet_spd(&cov_x) + logdet_spd(&cov_y) - logdet_spd(&cov_xy));
61 mi.max(0.0)
62}
63
64fn covariance_matrix(data: &[Vec<f64>], eps: f64) -> Vec<Vec<f64>> {
66 let n = data.len();
67 if n == 0 {
68 return vec![];
69 }
70 let t = data[0].len();
71 if t == 0 {
72 return vec![vec![eps; n]; n];
73 }
74
75 let means: Vec<f64> = data
77 .iter()
78 .map(|ch| ch.iter().sum::<f64>() / t as f64)
79 .collect();
80
81 let mut cov = vec![vec![0.0; n]; n];
83 let t_f = t as f64;
84 let denom = (t_f - 1.0).max(1.0);
85
86 for i in 0..n {
87 for j in 0..=i {
88 let dot = crate::simd::dot_f64_dispatch(&data[i], &data[j]);
89 let val = (dot - t_f * means[i] * means[j]) / denom;
90 cov[i][j] = val;
91 cov[j][i] = val;
92 }
93 cov[i][i] += eps; }
95 cov
96}
97
98fn logdet_spd(matrix: &[Vec<f64>]) -> f64 {
103 let n = matrix.len();
104 if n == 0 {
105 return 0.0;
106 }
107 let flat: Vec<f64> = matrix.iter().flatten().copied().collect();
108 let chol = DMatrix::<f64>::from_row_slice(n, n, &flat)
109 .cholesky()
110 .expect("phi covariance must be symmetric positive-definite");
111 let l = chol.l();
112 2.0 * (0..n).map(|i| l[(i, i)].ln()).sum::<f64>()
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn test_independent_channels_low_phi() {
121 let data = vec![
123 (0..200).map(|i| (i as f64 * 0.1).sin()).collect(),
124 (0..200).map(|i| (i as f64 * 0.3).cos()).collect(),
125 (0..200).map(|i| (i as f64 * 0.7).sin()).collect(),
126 ];
127 let phi = phi_star(&data, 1);
128 assert!(phi < 0.5, "Expected low Phi for independent, got {phi}");
129 }
130
131 #[test]
132 fn test_correlated_channels_positive_phi() {
133 let data = vec![
135 (0..200)
136 .map(|i| (i as f64 * 0.1).sin() + (i as f64 * 0.05).cos() * 0.3)
137 .collect(),
138 (0..200)
139 .map(|i| (i as f64 * 0.1).sin() * 0.8 + (i as f64 * 0.2).sin() * 0.5)
140 .collect(),
141 (0..200)
142 .map(|i| (i as f64 * 0.1).sin() * 0.6 + (i as f64 * 0.15).cos() * 0.7)
143 .collect(),
144 ];
145 let phi = phi_star(&data, 1);
146 assert!(phi >= 0.0, "Expected non-negative Phi, got {phi}");
147 }
148
149 #[test]
150 fn test_single_channel_zero() {
151 let data = vec![vec![1.0, 2.0, 3.0, 4.0, 5.0]];
152 assert_eq!(phi_star(&data, 1), 0.0);
153 }
154
155 #[test]
156 fn test_short_data_zero() {
157 let data = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
158 assert_eq!(phi_star(&data, 1), 0.0);
159 }
160
161 #[test]
162 fn test_logdet_spd_diagonal() {
163 let m = vec![vec![2.0, 0.0], vec![0.0, 8.0]];
165 assert!((logdet_spd(&m) - 16.0_f64.ln()).abs() < 1e-10);
166 }
167
168 #[test]
169 fn test_logdet_spd_matches_dense() {
170 let m = vec![
172 vec![4.0, 1.0, 0.5],
173 vec![1.0, 3.0, 0.2],
174 vec![0.5, 0.2, 2.0],
175 ];
176 let det: f64 = 4.0 * (3.0 * 2.0 - 0.2 * 0.2) - 1.0 * (1.0 * 2.0 - 0.2 * 0.5)
177 + 0.5 * (1.0 * 0.2 - 3.0 * 0.5);
178 assert!((logdet_spd(&m) - det.ln()).abs() < 1e-10);
179 }
180}