extern crate alloc;
use alloc::boxed::Box;
use alloc::vec;
#[derive(Debug, Clone)]
pub struct TransferEntropyF64 {
joint_xy: Box<[u64]>,
joint_yx: Box<[u64]>,
marginal_y: Box<[u64]>,
marginal_x: Box<[u64]>,
hist_x: Box<[usize]>,
hist_y: Box<[usize]>,
head: usize,
filled: usize,
bins: usize,
lag: usize,
total: u64,
}
#[derive(Debug, Clone)]
pub struct TransferEntropyF64Builder {
bins: Option<usize>,
lag: Option<usize>,
}
impl TransferEntropyF64 {
#[inline]
#[must_use]
pub fn builder() -> TransferEntropyF64Builder {
TransferEntropyF64Builder {
bins: Option::None,
lag: Option::None,
}
}
#[inline]
fn idx3(&self, a: usize, b: usize, c: usize) -> usize {
a * self.bins * self.bins + b * self.bins + c
}
#[inline]
fn idx2(&self, a: usize, b: usize) -> usize {
a * self.bins + b
}
#[inline]
pub fn update(&mut self, x_bin: usize, y_bin: usize) {
let bins = self.bins;
assert!(x_bin < bins, "x_bin {x_bin} out of range (bins={bins})");
assert!(y_bin < bins, "y_bin {y_bin} out of range (bins={bins})");
if self.filled >= self.lag {
let xp = self.hist_x[self.head];
let yp = self.hist_y[self.head];
let i = self.idx3(xp, yp, y_bin);
self.joint_xy[i] += 1;
let i = self.idx3(yp, xp, x_bin);
self.joint_yx[i] += 1;
let i = self.idx2(yp, y_bin);
self.marginal_y[i] += 1;
let i = self.idx2(xp, x_bin);
self.marginal_x[i] += 1;
self.total += 1;
}
self.hist_x[self.head] = x_bin;
self.hist_y[self.head] = y_bin;
self.head = (self.head + 1) % self.lag;
if self.filled < self.lag {
self.filled += 1;
}
}
#[must_use]
pub fn te_x_to_y(&self) -> Option<f64> {
if self.total == 0 {
return Option::None;
}
Option::Some(self.compute_te(&self.joint_xy, &self.marginal_y))
}
#[must_use]
pub fn te_y_to_x(&self) -> Option<f64> {
if self.total == 0 {
return Option::None;
}
Option::Some(self.compute_te(&self.joint_yx, &self.marginal_x))
}
#[must_use]
pub fn net_flow(&self) -> Option<f64> {
let te_xy = self.te_x_to_y()?;
let te_yx = self.te_y_to_x()?;
Option::Some(te_xy - te_yx)
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.total
}
#[inline]
#[must_use]
pub fn bins(&self) -> usize {
self.bins
}
#[inline]
#[must_use]
pub fn lag(&self) -> usize {
self.lag
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.total > 0
}
pub fn reset(&mut self) {
self.joint_xy.fill(0);
self.joint_yx.fill(0);
self.marginal_y.fill(0);
self.marginal_x.fill(0);
self.hist_x.fill(0);
self.hist_y.fill(0);
self.head = 0;
self.filled = 0;
self.total = 0;
}
fn compute_te(&self, joint: &[u64], marginal: &[u64]) -> f64 {
let n = self.total as f64;
let bins = self.bins;
let mut te = 0.0;
let mut marginal_b_sums = [0u64; 32]; for b in 0..bins {
let mut sum = 0u64;
for c in 0..bins {
sum += marginal[self.idx2(b, c)];
}
marginal_b_sums[b] = sum;
}
for a in 0..bins {
for b in 0..bins {
let mut joint_ab = 0u64;
for c in 0..bins {
joint_ab += joint[self.idx3(a, b, c)];
}
if joint_ab == 0 {
continue;
}
let marginal_b = marginal_b_sums[b];
if marginal_b == 0 {
continue;
}
for c in 0..bins {
let joint_abc = joint[self.idx3(a, b, c)];
if joint_abc == 0 {
continue;
}
let marginal_bc = marginal[self.idx2(b, c)];
if marginal_bc == 0 {
continue;
}
let p_abc = joint_abc as f64 / n;
let ratio = (joint_abc as f64 * marginal_b as f64)
/ (marginal_bc as f64 * joint_ab as f64);
te += p_abc * nexus_stats_core::math::ln(ratio);
}
}
}
te
}
}
impl TransferEntropyF64Builder {
#[inline]
#[must_use]
pub fn bins(mut self, bins: usize) -> Self {
self.bins = Option::Some(bins);
self
}
#[inline]
#[must_use]
pub fn lag(mut self, lag: usize) -> Self {
self.lag = Option::Some(lag);
self
}
pub fn build(self) -> Result<TransferEntropyF64, nexus_stats_core::ConfigError> {
let bins = self
.bins
.ok_or(nexus_stats_core::ConfigError::Missing("bins"))?;
let lag = self
.lag
.ok_or(nexus_stats_core::ConfigError::Missing("lag"))?;
if bins < 2 {
return Err(nexus_stats_core::ConfigError::Invalid("bins must be >= 2"));
}
if bins > 32 {
return Err(nexus_stats_core::ConfigError::Invalid(
"bins must be <= 32 (bins^3 table growth; 16 or fewer recommended)",
));
}
if lag < 1 {
return Err(nexus_stats_core::ConfigError::Invalid("lag must be >= 1"));
}
let bins_sq = bins
.checked_mul(bins)
.ok_or(nexus_stats_core::ConfigError::Invalid(
"bins too large (bins² overflows)",
))?;
let bins_cu = bins_sq
.checked_mul(bins)
.ok_or(nexus_stats_core::ConfigError::Invalid(
"bins too large (bins³ overflows)",
))?;
Ok(TransferEntropyF64 {
joint_xy: vec![0u64; bins_cu].into_boxed_slice(),
joint_yx: vec![0u64; bins_cu].into_boxed_slice(),
marginal_y: vec![0u64; bins_sq].into_boxed_slice(),
marginal_x: vec![0u64; bins_sq].into_boxed_slice(),
hist_x: vec![0usize; lag].into_boxed_slice(),
hist_y: vec![0usize; lag].into_boxed_slice(),
head: 0,
filled: 0,
bins,
lag,
total: 0,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn te_builder(bins: usize, lag: usize) -> TransferEntropyF64 {
TransferEntropyF64::builder()
.bins(bins)
.lag(lag)
.build()
.unwrap()
}
#[test]
fn x_causes_y_lag1() {
let mut te = te_builder(4, 1);
let mut prev_x = 0usize;
let mut rng = 12345u64;
for _ in 0..20000 {
rng = rng
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let x_bin = ((rng >> 62) as usize) % 4;
let y_bin = prev_x;
te.update(x_bin, y_bin);
prev_x = x_bin;
}
let te_xy = te.te_x_to_y().unwrap();
let te_yx = te.te_y_to_x().unwrap();
assert!(
te_xy > 0.5,
"TE(X→Y) should be large when X causes Y, got {te_xy}"
);
assert!(
te_xy > te_yx * 2.0,
"TE(X→Y)={te_xy} should dominate TE(Y→X)={te_yx}"
);
}
#[test]
fn x_causes_y_lag3() {
let mut te = te_builder(4, 3);
let mut hist = [0usize; 3]; let mut hpos = 0usize;
let mut rng = 99999u64;
for i in 0..30000u32 {
rng = rng
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let x_bin = ((rng >> 62) as usize) % 4;
let y_bin = if i >= 3 { hist[hpos] } else { 0 };
te.update(x_bin, y_bin);
hist[hpos] = x_bin;
hpos = (hpos + 1) % 3;
}
let te_xy = te.te_x_to_y().unwrap();
let te_yx = te.te_y_to_x().unwrap();
assert!(
te_xy > 0.3,
"lag-3 TE(X→Y) should be significant, got {te_xy}"
);
assert!(
te_xy > te_yx,
"TE(X→Y)={te_xy} should exceed TE(Y→X)={te_yx} at lag=3"
);
}
#[test]
fn independent_streams_near_zero() {
let mut te = te_builder(4, 1);
for i in 0..10000u32 {
let x_bin = (i as usize) % 4;
let y_bin = ((i as usize) * 3 + 1) % 4;
te.update(x_bin, y_bin);
}
let te_xy = te.te_x_to_y().unwrap();
let te_yx = te.te_y_to_x().unwrap();
assert!(
te_xy.abs() < 0.1,
"independent TE(X→Y) should be near 0, got {te_xy}"
);
assert!(
te_yx.abs() < 0.1,
"independent TE(Y→X) should be near 0, got {te_yx}"
);
}
#[test]
fn net_flow_sign() {
let mut te = te_builder(4, 1);
let mut prev_x = 0usize;
let mut rng = 54321u64;
for _ in 0..20000 {
rng = rng
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let x_bin = ((rng >> 62) as usize) % 4;
let y_bin = prev_x;
te.update(x_bin, y_bin);
prev_x = x_bin;
}
let nf = te.net_flow().unwrap();
assert!(nf > 0.0, "X→Y should have positive net flow, got {nf}");
}
#[test]
fn symmetric_coupling() {
let mut te = te_builder(2, 1);
let mut x = 0usize;
let mut y = 0usize;
for _ in 0..10000 {
let new_x = y;
let new_y = x;
te.update(x, y);
x = new_x % 2;
y = new_y % 2;
}
let nf = te.net_flow().unwrap();
assert!(
nf.abs() < 0.3,
"symmetric coupling should have near-zero net flow, got {nf}"
);
}
#[test]
fn empty_returns_none() {
let te = te_builder(4, 1);
assert!(te.te_x_to_y().is_none());
assert!(te.te_y_to_x().is_none());
assert!(te.net_flow().is_none());
assert!(!te.is_primed());
}
#[test]
fn not_primed_until_lag_plus_1() {
let mut te = te_builder(4, 3);
te.update(0, 0);
assert!(!te.is_primed());
te.update(1, 1);
assert!(!te.is_primed());
te.update(2, 2);
assert!(!te.is_primed());
te.update(3, 3); assert!(te.is_primed());
assert_eq!(te.count(), 1);
}
#[test]
#[should_panic(expected = "out of range")]
fn x_out_of_range_panics() {
let mut te = te_builder(4, 1);
te.update(4, 0);
}
#[test]
#[should_panic(expected = "out of range")]
fn y_out_of_range_panics() {
let mut te = te_builder(4, 1);
te.update(0, 4);
}
#[test]
fn builder_rejects_bins_below_2() {
let r = TransferEntropyF64::builder().bins(1).lag(1).build();
assert!(r.is_err());
}
#[test]
fn builder_rejects_lag_zero() {
let r = TransferEntropyF64::builder().bins(4).lag(0).build();
assert!(r.is_err());
}
#[test]
fn builder_rejects_missing_bins() {
let r = TransferEntropyF64::builder().lag(1).build();
assert!(r.is_err());
}
#[test]
fn builder_rejects_missing_lag() {
let r = TransferEntropyF64::builder().bins(4).build();
assert!(r.is_err());
}
#[test]
fn accessors() {
let te = te_builder(8, 3);
assert_eq!(te.bins(), 8);
assert_eq!(te.lag(), 3);
assert_eq!(te.count(), 0);
}
#[test]
fn reset_clears_state() {
let mut te = te_builder(4, 1);
let mut prev_x = 0usize;
for i in 0..1000u32 {
let x_bin = (i as usize) % 4;
te.update(x_bin, prev_x);
prev_x = x_bin;
}
te.reset();
assert_eq!(te.count(), 0);
assert!(!te.is_primed());
assert!(te.te_x_to_y().is_none());
}
}