fn digamma(mut x: f64) -> f64 {
let mut result = 0.0;
while x < 8.0 {
result -= 1.0 / x;
x += 1.0;
}
let inv_x = 1.0 / x;
let inv_x2 = inv_x * inv_x;
result += x.ln()
- 0.5 * inv_x
- inv_x2 * (1.0 / 12.0 - inv_x2 * (1.0 / 120.0 - inv_x2 * (1.0 / 252.0)));
result
}
fn count_within_eps(sorted: &[f64], center: f64, eps: f64) -> usize {
if eps <= 0.0 {
return 0;
}
let lo = center - eps;
let hi = center + eps;
let left = sorted.partition_point(|&v| v <= lo);
let right = sorted.partition_point(|&v| v < hi);
right.saturating_sub(left)
}
fn kth_neighbor_distances(x: &[f64], y: &[f64], k: usize) -> Vec<(f64, f64)> {
let n = x.len();
let mut result = Vec::with_capacity(n);
for i in 0..n {
let mut dists: Vec<(f64, usize)> = (0..n)
.filter(|&j| j != i)
.map(|j| {
let dx = (x[i] - x[j]).abs();
let dy = (y[i] - y[j]).abs();
(dx.max(dy), j)
})
.collect();
dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let kth_idx = k - 1; let eps = dists[kth_idx].0;
result.push((eps, eps));
}
result
}
pub fn knn_mutual_information(x: &[f64], y: &[f64], k: usize) -> f64 {
let n = x.len();
assert_eq!(n, y.len(), "x and y must have the same length");
assert!(k > 0 && k < n, "k must satisfy 0 < k < N");
if n < k + 1 {
return 0.0;
}
let mut x_sorted: Vec<f64> = x.to_vec();
x_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let mut y_sorted: Vec<f64> = y.to_vec();
y_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
let eps_pairs = kth_neighbor_distances(x, y, k);
let mut sum_psi = 0.0;
for (i, &(eps_x, eps_y)) in eps_pairs.iter().enumerate() {
let n_x = count_within_eps(&x_sorted, x[i], eps_x).saturating_sub(1);
let n_y = count_within_eps(&y_sorted, y[i], eps_y).saturating_sub(1);
sum_psi += digamma((n_x + 1) as f64) + digamma((n_y + 1) as f64);
let _ = i;
}
let avg_psi = sum_psi / n as f64;
let mi = digamma(k as f64) - avg_psi + digamma(n as f64);
mi.max(0.0) }
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn digamma_at_1_is_neg_euler() {
assert_relative_eq!(digamma(1.0), -0.5772156649, epsilon = 1e-6);
}
#[test]
fn digamma_at_integers() {
assert_relative_eq!(digamma(2.0), 0.42278, epsilon = 1e-4);
assert_relative_eq!(digamma(5.0), 1.5061, epsilon = 1e-4);
}
#[test]
fn mi_of_identical_variables_is_positive() {
let x: Vec<f64> = (0..200).map(|i| (i as f64 * 0.1).sin()).collect();
let mi = knn_mutual_information(&x, &x, 5);
assert!(mi > 0.5, "I(X;X) should be large, got {}", mi);
}
#[test]
fn mi_of_independent_variables_is_near_zero() {
let x: Vec<f64> = (0..300).map(|i| (i as f64 * 0.07).sin()).collect();
let y: Vec<f64> = (0..300)
.map(|i| ((i * 13 + 7) % 97) as f64 / 97.0)
.collect();
let mi = knn_mutual_information(&x, &y, 8);
assert!(mi < 0.3, "I(independent X, Y) should be near 0, got {}", mi);
}
#[test]
fn mi_detects_linear_dependence() {
let x: Vec<f64> = (0..300)
.map(|i| {
let base = (i as f64 * 0.05).sin();
base + ((i * 7 + 3) % 11) as f64 * 0.02 - 0.11
})
.collect();
let y: Vec<f64> = x
.iter()
.enumerate()
.map(|(i, &xi)| 2.0 * xi + ((i * 13 + 5) % 17) as f64 * 0.02 - 0.17)
.collect();
let mi = knn_mutual_information(&x, &y, 8);
assert!(mi > 0.3, "linear dependence MI should be > 0.3, got {}", mi);
}
}