use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use statrs::distribution::{ContinuousCDF, Normal};
pub fn gcmi(x: &[f64], y: &[f64]) -> f64 {
let n = x.len();
assert_eq!(n, y.len(), "x and y must have the same length");
if n < 3 {
return 0.0;
}
let needs_jitter = has_significant_ties(x) || has_significant_ties(y);
let (gx, gy) = if needs_jitter {
let seed = deterministic_seed(x, y);
let mut rng = StdRng::seed_from_u64(seed);
let xj: Vec<f64> = x.iter().map(|&v| v + rng.gen_range(-0.5..0.5)).collect();
let yj: Vec<f64> = y.iter().map(|&v| v + rng.gen_range(-0.5..0.5)).collect();
(rank_to_probit(&xj), rank_to_probit(&yj))
} else {
(rank_to_probit(x), rank_to_probit(y))
};
let rho = pearson(&gx, &gy);
let rho2 = rho * rho;
if rho2 >= 1.0 {
return f64::INFINITY; }
-0.5 * (1.0 - rho2).log2()
}
fn has_significant_ties(values: &[f64]) -> bool {
let n = values.len();
if n < 10 {
return false;
}
let mut sorted: Vec<f64> = values.to_vec();
sorted.sort_unstable_by(|a, b| a.total_cmp(b));
let mut tie_count = 0usize;
for w in sorted.windows(2) {
if (w[1] - w[0]).abs() < 1e-10 {
tie_count += 1;
}
}
tie_count as f64 / n as f64 > 0.10
}
fn deterministic_seed(x: &[f64], y: &[f64]) -> u64 {
let mut h: u64 = 0xcbf29ce484222325;
for &v in x.iter().take(8).chain(x.iter().rev().take(4)) {
h ^= v.to_bits();
h = h.wrapping_mul(0x100000001b3);
}
for &v in y.iter().take(8).chain(y.iter().rev().take(4)) {
h ^= v.to_bits();
h = h.wrapping_mul(0x100000001b3);
}
h ^= (x.len() as u64).wrapping_mul(0x9e3779b97f4a7c15);
h
}
fn precompute_probit_table(n: usize) -> Vec<f64> {
let normal = Normal::new(0.0, 1.0).unwrap();
let scale = 1.0 / (n as f64 + 1.0);
(0..n)
.map(|k| normal.inverse_cdf((k as f64 + 1.0) * scale))
.collect()
}
fn rank_to_probit(values: &[f64]) -> Vec<f64> {
let n = values.len();
let probit_table = precompute_probit_table(n);
let mut indexed: Vec<(f64, usize)> = values.iter().copied().zip(0..).collect();
indexed.sort_unstable_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut result = vec![0.0; n];
let mut i = 0;
while i < n {
let mut j = i + 1;
while j < n && (indexed[j].0 - indexed[i].0).abs() < 1e-15 {
j += 1;
}
if j == i + 1 {
result[indexed[i].1] = probit_table[i];
} else {
let normal = Normal::new(0.0, 1.0).unwrap();
let avg_rank = (i + j) as f64 / 2.0 + 0.5;
let scale = 1.0 / (n as f64 + 1.0);
let probit = normal.inverse_cdf(avg_rank * scale);
for item in indexed.iter().take(j).skip(i) {
result[item.1] = probit;
}
}
i = j;
}
result
}
fn pearson(x: &[f64], y: &[f64]) -> f64 {
let n = x.len() as f64;
let mx = x.iter().sum::<f64>() / n;
let my = y.iter().sum::<f64>() / n;
let mut sxy = 0.0;
let mut sxx = 0.0;
let mut syy = 0.0;
for (&xi, &yi) in x.iter().zip(y.iter()) {
let dx = xi - mx;
let dy = yi - my;
sxy += dx * dy;
sxx += dx * dx;
syy += dy * dy;
}
if sxx < 1e-30 || syy < 1e-30 {
return 0.0;
}
sxy / (sxx * syy).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn independent_variables_gcmi_near_zero() {
let x: Vec<f64> = (0..200).map(|i| (i as f64 * 0.07).sin()).collect();
let y: Vec<f64> = (0..200)
.map(|i| ((i * 13 + 7) % 97) as f64 / 97.0)
.collect();
let mi = gcmi(&x, &y);
assert!(
mi.abs() < 0.15,
"independent GCMI should be near 0, got {}",
mi
);
}
#[test]
fn linearly_dependent_gcmi_is_positive() {
let x: Vec<f64> = (0..300).map(|i| i as f64 * 0.1).collect();
let noise: Vec<f64> = (0..300)
.map(|i| ((i * 7 + 3) % 11) as f64 * 0.1 - 0.55)
.collect();
let y: Vec<f64> = x
.iter()
.zip(noise.iter())
.map(|(&xi, &ni)| 3.0 * xi + ni)
.collect();
let mi = gcmi(&x, &y);
assert!(
mi > 1.0,
"strong linear dependence GCMI should be >> 0, got {}",
mi
);
}
#[test]
fn gcmi_deterministic_same_input_same_output() {
let x: Vec<f64> = (0..100).map(|i| (i as f64 * 0.3).sin()).collect();
let y: Vec<f64> = (0..100).map(|i| (i as f64 * 0.3).cos()).collect();
let a = gcmi(&x, &y);
let b = gcmi(&x, &y);
assert_relative_eq!(a, b, epsilon = 1e-15);
}
#[test]
fn gcmi_integer_data_with_jitter_detects_linear() {
let x: Vec<f64> = (0..300).map(|i| (i % 10) as f64).collect();
let y: Vec<f64> = x
.iter()
.enumerate()
.map(|(i, &xi)| (2.0 * xi + ((i * 3 + 1) % 5) as f64).round())
.collect();
let mi = gcmi(&x, &y);
assert!(
mi > 0.1,
"GCMI on integer data with linear structure should be > 0.1 (jitter breaks ties), got {}",
mi
);
}
#[test]
fn gcmi_integer_data_independent_near_zero() {
let x: Vec<f64> = (0..300).map(|i| (i % 7) as f64).collect();
let y: Vec<f64> = (0..300).map(|i| ((i * 13 + 5) % 11) as f64).collect();
let mi = gcmi(&x, &y);
assert!(
mi < 0.3,
"GCMI on independent integers should be small, got {}",
mi
);
}
#[test]
fn gcmi_integer_data_deterministic() {
let x: Vec<f64> = (0..200).map(|i| (i % 5) as f64).collect();
let y: Vec<f64> = (0..200).map(|i| (i % 3) as f64).collect();
let a = gcmi(&x, &y);
let b = gcmi(&x, &y);
assert_relative_eq!(a, b, epsilon = 1e-15);
}
#[test]
fn has_significant_ties_detects_integers() {
let integers: Vec<f64> = (0..100).map(|i| (i % 5) as f64).collect();
assert!(has_significant_ties(&integers));
let continuous: Vec<f64> = (0..100).map(|i| i as f64 * 0.1 + 0.001).collect();
assert!(!has_significant_ties(&continuous));
}
}