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 digamma_table(max_arg: usize) -> Vec<f64> {
(0..=max_arg).map(|i| digamma((i + 1) as f64)).collect()
}
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)
}
#[derive(Clone)]
struct KdNode {
idx: usize,
xy: [f64; 2],
}
struct KdTree {
tree: Vec<KdNode>,
}
impl KdTree {
fn build(x: &[f64], y: &[f64]) -> Self {
let n = x.len();
let mut nodes: Vec<KdNode> = (0..n)
.map(|i| KdNode {
idx: i,
xy: [x[i], y[i]],
})
.collect();
let mut tree = vec![
KdNode {
idx: 0,
xy: [0.0, 0.0]
};
n
];
Self::build_recursive(&mut nodes, &mut tree, 0, n, 0);
Self { tree }
}
fn build_recursive(
nodes: &mut [KdNode],
tree: &mut [KdNode],
start: usize,
end: usize,
depth: usize,
) {
if start >= end {
return;
}
let dim = depth % 2;
let mid = (start + end) / 2;
nodes[start..end]
.select_nth_unstable_by(mid - start, |a, b| a.xy[dim].total_cmp(&b.xy[dim]));
tree[mid] = nodes[mid].clone();
if mid > start {
Self::build_recursive(nodes, tree, start, mid, depth + 1);
}
if mid + 1 < end {
Self::build_recursive(nodes, tree, mid + 1, end, depth + 1);
}
}
fn kth_neighbor_chebyshev(&self, qx: f64, qy: f64, exclude_idx: usize, k: usize) -> f64 {
let n = self.tree.len();
if n == 0 {
return f64::INFINITY;
}
let mut heap = KnnHeap::new(k);
self.search_recursive(qx, qy, exclude_idx, 0, n, 0, &mut heap);
heap.worst()
}
fn search_recursive(
&self,
qx: f64,
qy: f64,
exclude_idx: usize,
start: usize,
end: usize,
depth: usize,
heap: &mut KnnHeap,
) {
if start >= end {
return;
}
let mid = (start + end) / 2;
let node = &self.tree[mid];
let dist = (qx - node.xy[0]).abs().max((qy - node.xy[1]).abs());
if node.idx != exclude_idx {
heap.push(dist);
}
let dim = depth % 2;
let q_dim = if dim == 0 { qx } else { qy };
let split = node.xy[dim];
let diff = q_dim - split;
let (first_start, first_end, second_start, second_end) = if diff <= 0.0 {
(start, mid, mid + 1, end)
} else {
(mid + 1, end, start, mid)
};
self.search_recursive(qx, qy, exclude_idx, first_start, first_end, depth + 1, heap);
if diff.abs() < heap.worst() {
self.search_recursive(
qx,
qy,
exclude_idx,
second_start,
second_end,
depth + 1,
heap,
);
}
}
}
struct KnnHeap {
data: Vec<f64>,
k: usize,
}
impl KnnHeap {
fn new(k: usize) -> Self {
Self {
data: Vec::with_capacity(k),
k,
}
}
#[inline]
fn worst(&self) -> f64 {
if self.data.len() < self.k {
f64::INFINITY
} else {
self.data[self.k - 1]
}
}
#[inline]
fn push(&mut self, dist: f64) {
if self.data.len() < self.k {
let pos = self.data.partition_point(|&d| d < dist);
self.data.insert(pos, dist);
} else if dist < self.data[self.k - 1] {
self.data.pop();
let pos = self.data.partition_point(|&d| d < dist);
self.data.insert(pos, dist);
}
}
}
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_unstable_by(|a, b| a.total_cmp(b));
let mut y_sorted: Vec<f64> = y.to_vec();
y_sorted.sort_unstable_by(|a, b| a.total_cmp(b));
let psi = digamma_table(n);
let tree = KdTree::build(x, y);
let mut sum_psi = 0.0;
for i in 0..n {
let eps = tree.kth_neighbor_chebyshev(x[i], y[i], i, k);
let n_x = count_within_eps(&x_sorted, x[i], eps).saturating_sub(1);
let n_y = count_within_eps(&y_sorted, y[i], eps).saturating_sub(1);
sum_psi += psi[n_x.min(n)] + psi[n_y.min(n)];
}
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);
}
}