Files
rustytorch/crates/specialized/rtx-neuro-connectivity/src/granger.rs
T
2026-03-04 00:08:42 +00:00

787 lines
23 KiB
Rust

//! Granger Causality analysis for effective connectivity.
//!
//! Granger causality measures the directed influence of one time series on another
//! by testing whether past values of X help predict Y beyond past values of Y alone.
//!
//! ## Time Domain Granger Causality
//!
//! The Granger causality from X to Y is:
//! GC(X→Y) = ln(var(Y|Y_past) / var(Y|Y_past,X_past))
//!
//! ## Spectral Granger Causality
//!
//! Decomposes Granger causality across frequencies using the
//! Geweke spectral measure.
//!
//! ## References
//!
//! - Granger, C. W. (1969). Investigating causal relations by econometric models.
//! - Geweke, J. (1982). Measurement of linear dependence and feedback between
//! multiple time series.
use crate::{ConnectivityError, ConnectivityResult};
use nalgebra::{DMatrix, DVector};
use num_complex::Complex64;
use std::f64::consts::PI;
/// Configuration for Granger causality analysis
#[derive(Debug, Clone)]
pub struct GrangerConfig {
/// Model order (number of lags)
pub order: usize,
/// Criterion for automatic order selection
pub order_criterion: OrderCriterion,
/// Maximum order for automatic selection
pub max_order: usize,
/// Include instantaneous effects
pub include_instantaneous: bool,
}
impl Default for GrangerConfig {
fn default() -> Self {
Self {
order: 10,
order_criterion: OrderCriterion::Bic,
max_order: 20,
include_instantaneous: false,
}
}
}
impl GrangerConfig {
/// Create config with specific model order
pub fn with_order(order: usize) -> Self {
Self {
order,
..Self::default()
}
}
/// Create config with automatic order selection
pub fn auto_order(max_order: usize, criterion: OrderCriterion) -> Self {
Self {
order: 0, // Will be determined automatically
order_criterion: criterion,
max_order,
..Self::default()
}
}
}
/// Criterion for selecting VAR model order
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderCriterion {
/// Akaike Information Criterion
Aic,
/// Bayesian Information Criterion
Bic,
/// Hannan-Quinn Criterion
Hqc,
/// Use fixed order
Fixed,
}
/// Result of Granger causality test
#[derive(Debug, Clone)]
pub struct GrangerResult {
/// Granger causality value (log ratio of variances)
pub gc_value: f64,
/// F-statistic for causality test
pub f_statistic: f64,
/// P-value (if computed)
pub p_value: Option<f64>,
/// Degrees of freedom (numerator, denominator)
pub df: (usize, usize),
/// Model order used
pub order: usize,
/// Residual variance (restricted model)
pub var_restricted: f64,
/// Residual variance (unrestricted model)
pub var_unrestricted: f64,
}
/// Result of spectral Granger causality
#[derive(Debug, Clone)]
pub struct SpectralGrangerResult {
/// Frequencies
pub frequencies: Vec<f64>,
/// Granger causality X → Y at each frequency
pub gc_x_to_y: Vec<f64>,
/// Granger causality Y → X at each frequency
pub gc_y_to_x: Vec<f64>,
/// Instantaneous causality at each frequency
pub gc_instantaneous: Vec<f64>,
/// Total coherence-like measure
pub gc_total: Vec<f64>,
/// Model order used
pub order: usize,
}
/// Vector Autoregressive (VAR) model for Granger causality
#[derive(Debug)]
pub struct VarModel {
/// Coefficient matrices A_1, A_2, ..., A_p for each lag
coefficients: Vec<DMatrix<f64>>,
/// Residual covariance matrix
residual_cov: DMatrix<f64>,
/// Model order
order: usize,
/// Number of variables
n_vars: usize,
}
impl VarModel {
/// Fit a VAR model to multivariate time series
///
/// # Arguments
/// * `data` - Multivariate time series [n_vars][n_samples]
/// * `order` - Model order (number of lags)
pub fn fit(data: &[Vec<f64>], order: usize) -> ConnectivityResult<Self> {
let n_vars = data.len();
let n_samples = data[0].len();
if n_samples <= order * n_vars + 1 {
return Err(ConnectivityError::InsufficientData(format!(
"Need more samples for order {} model (have {}, need > {})",
order,
n_samples,
order * n_vars + 1
)));
}
// Build regression matrices
// Y = [y_p+1, ..., y_T]^T (T-p x n_vars)
// X = [Y_lagged] (T-p x p*n_vars)
let t = n_samples - order;
// Dependent variable matrix Y
let mut y_mat = DMatrix::zeros(t, n_vars);
for j in 0..n_vars {
for i in 0..t {
y_mat[(i, j)] = data[j][order + i];
}
}
// Lagged predictor matrix X
let n_pred = order * n_vars;
let mut x_mat = DMatrix::zeros(t, n_pred);
for i in 0..t {
for lag in 0..order {
for j in 0..n_vars {
x_mat[(i, lag * n_vars + j)] = data[j][order + i - lag - 1];
}
}
}
// OLS estimation: B = (X'X)^(-1) X'Y
let xtx = x_mat.transpose() * &x_mat;
let xty = x_mat.transpose() * &y_mat;
let xtx_inv = Self::pseudoinverse(&xtx)?;
let b = &xtx_inv * &xty;
// Extract coefficient matrices
let mut coefficients = Vec::with_capacity(order);
for lag in 0..order {
let mut a_lag = DMatrix::zeros(n_vars, n_vars);
for i in 0..n_vars {
for j in 0..n_vars {
a_lag[(i, j)] = b[(lag * n_vars + j, i)];
}
}
coefficients.push(a_lag);
}
// Compute residuals and covariance
let y_pred = &x_mat * &b;
let residuals = &y_mat - &y_pred;
let residual_cov = (residuals.transpose() * &residuals) / (t - n_pred) as f64;
Ok(Self {
coefficients,
residual_cov,
order,
n_vars,
})
}
/// Get coefficient matrix for a specific lag
pub fn get_coefficients(&self, lag: usize) -> Option<&DMatrix<f64>> {
self.coefficients.get(lag)
}
/// Get residual covariance matrix
pub fn residual_covariance(&self) -> &DMatrix<f64> {
&self.residual_cov
}
/// Compute AIC for model selection
pub fn aic(&self, n_samples: usize) -> f64 {
let n = n_samples - self.order;
let det = self.residual_cov.determinant().max(1e-30);
let k = (self.order * self.n_vars * self.n_vars) as f64;
n as f64 * det.ln() + 2.0 * k
}
/// Compute BIC for model selection
pub fn bic(&self, n_samples: usize) -> f64 {
let n = n_samples - self.order;
let det = self.residual_cov.determinant().max(1e-30);
let k = (self.order * self.n_vars * self.n_vars) as f64;
n as f64 * det.ln() + k * (n as f64).ln()
}
/// Compute transfer function H(f) at a frequency
pub fn transfer_function(&self, freq: f64, sfreq: f64) -> DMatrix<Complex64> {
let n = self.n_vars;
let mut a_f = DMatrix::from_element(n, n, Complex64::new(0.0, 0.0));
for (lag, a) in self.coefficients.iter().enumerate() {
let exp_factor = Complex64::new(0.0, -2.0 * PI * freq * (lag + 1) as f64 / sfreq);
let exp_val = exp_factor.exp();
for i in 0..n {
for j in 0..n {
a_f[(i, j)] += Complex64::new(a[(i, j)], 0.0) * exp_val;
}
}
}
// I - A(f)
let mut result = DMatrix::from_element(n, n, Complex64::new(0.0, 0.0));
for i in 0..n {
result[(i, i)] = Complex64::new(1.0, 0.0);
}
for i in 0..n {
for j in 0..n {
result[(i, j)] -= a_f[(i, j)];
}
}
// H(f) = (I - A(f))^(-1)
Self::complex_inverse(&result).unwrap_or(result)
}
/// Pseudoinverse using SVD
fn pseudoinverse(m: &DMatrix<f64>) -> ConnectivityResult<DMatrix<f64>> {
let svd = m.clone().svd(true, true);
let u = svd
.u
.ok_or_else(|| ConnectivityError::ComputationError("SVD failed".to_string()))?;
let vt = svd
.v_t
.ok_or_else(|| ConnectivityError::ComputationError("SVD failed".to_string()))?;
let s = svd.singular_values;
let tol = 1e-10 * s[0];
let n = s.len().min(m.ncols());
let mut s_inv = DMatrix::zeros(m.ncols(), m.nrows());
for i in 0..n {
if s[i] > tol {
s_inv[(i, i)] = 1.0 / s[i];
}
}
Ok(vt.transpose() * &s_inv * u.transpose())
}
/// Complex matrix inverse
fn complex_inverse(m: &DMatrix<Complex64>) -> ConnectivityResult<DMatrix<Complex64>> {
let n = m.nrows();
if n != m.ncols() {
return Err(ConnectivityError::ComputationError(
"Matrix not square".to_string(),
));
}
// Simple Gaussian elimination for small matrices
let mut aug = DMatrix::from_element(n, 2 * n, Complex64::new(0.0, 0.0));
// Copy m to left half
for i in 0..n {
for j in 0..n {
aug[(i, j)] = m[(i, j)];
}
aug[(i, n + i)] = Complex64::new(1.0, 0.0);
}
// Forward elimination
for i in 0..n {
// Find pivot
let mut max_row = i;
let mut max_val = aug[(i, i)].norm();
for k in (i + 1)..n {
if aug[(k, i)].norm() > max_val {
max_val = aug[(k, i)].norm();
max_row = k;
}
}
// Swap rows
for j in 0..(2 * n) {
let tmp = aug[(i, j)];
aug[(i, j)] = aug[(max_row, j)];
aug[(max_row, j)] = tmp;
}
let pivot = aug[(i, i)];
if pivot.norm() < 1e-15 {
return Err(ConnectivityError::ComputationError(
"Singular matrix".to_string(),
));
}
// Scale row
for j in 0..(2 * n) {
aug[(i, j)] /= pivot;
}
// Eliminate column
for k in 0..n {
if k != i {
let factor = aug[(k, i)];
for j in 0..(2 * n) {
let val = aug[(i, j)];
aug[(k, j)] -= factor * val;
}
}
}
}
// Extract inverse from right half
let mut inv = DMatrix::from_element(n, n, Complex64::new(0.0, 0.0));
for i in 0..n {
for j in 0..n {
inv[(i, j)] = aug[(i, n + j)];
}
}
Ok(inv)
}
}
/// Compute time-domain Granger causality from X to Y
///
/// # Arguments
/// * `x` - Source time series
/// * `y` - Target time series
/// * `order` - Model order (number of lags)
///
/// # Returns
/// Granger causality result including GC value, F-statistic, and p-value
pub fn granger_causality(x: &[f64], y: &[f64], order: usize) -> ConnectivityResult<GrangerResult> {
if x.len() != y.len() {
return Err(ConnectivityError::DimensionMismatch(
"Time series must have same length".to_string(),
));
}
let n = x.len();
if n <= 2 * order + 1 {
return Err(ConnectivityError::InsufficientData(
"Not enough samples for given order".to_string(),
));
}
// Restricted model: Y ~ Y_past only
let var_restricted = fit_ar_variance(y, order)?;
// Unrestricted model: Y ~ Y_past + X_past (VAR model)
let data = vec![y.to_vec(), x.to_vec()];
let var_model = VarModel::fit(&data, order)?;
let var_unrestricted = var_model.residual_cov[(0, 0)];
// Granger causality: ln(var_restricted / var_unrestricted)
let gc_value = (var_restricted / var_unrestricted).ln();
// F-statistic for testing significance
// F = ((RSS_r - RSS_u) / p) / (RSS_u / (T - 2p - 1))
let t = n - order;
let df_num = order;
let df_den = t - 2 * order - 1;
let f_statistic = if df_den > 0 && var_unrestricted > 1e-30 {
((var_restricted - var_unrestricted) * df_den as f64) / (var_unrestricted * df_num as f64)
} else {
0.0
};
// P-value using F-distribution approximation (simplified)
let p_value = if f_statistic > 0.0 && df_den > 0 {
Some(f_distribution_sf(f_statistic, df_num, df_den))
} else {
None
};
Ok(GrangerResult {
gc_value: gc_value.max(0.0),
f_statistic: f_statistic.max(0.0),
p_value,
df: (df_num, df_den),
order,
var_restricted,
var_unrestricted,
})
}
/// Compute spectral Granger causality (Geweke's measure)
///
/// # Arguments
/// * `x` - First time series
/// * `y` - Second time series
/// * `sfreq` - Sampling frequency
/// * `order` - Model order
/// * `n_freqs` - Number of frequency points
pub fn spectral_granger(
x: &[f64],
y: &[f64],
sfreq: f64,
order: usize,
n_freqs: Option<usize>,
) -> ConnectivityResult<SpectralGrangerResult> {
if x.len() != y.len() {
return Err(ConnectivityError::DimensionMismatch(
"Time series must have same length".to_string(),
));
}
// Fit bivariate VAR model
let data = vec![x.to_vec(), y.to_vec()];
let var_model = VarModel::fit(&data, order)?;
let sigma = var_model.residual_covariance();
let n_freqs = n_freqs.unwrap_or(128);
let df = sfreq / (2.0 * n_freqs as f64);
let frequencies: Vec<f64> = (0..n_freqs).map(|i| i as f64 * df).collect();
let mut gc_x_to_y = Vec::with_capacity(n_freqs);
let mut gc_y_to_x = Vec::with_capacity(n_freqs);
let mut gc_instantaneous = Vec::with_capacity(n_freqs);
let mut gc_total = Vec::with_capacity(n_freqs);
for &freq in &frequencies {
// Compute transfer function H(f)
let h = var_model.transfer_function(freq, sfreq);
// Spectral matrix S(f) = H(f) * Sigma * H(f)^H
let h_conj = h.transpose().map(|c| c.conj());
// S(f) computation
let mut s = DMatrix::from_element(2, 2, Complex64::new(0.0, 0.0));
for i in 0..2 {
for j in 0..2 {
for k in 0..2 {
for l in 0..2 {
s[(i, j)] +=
h[(i, k)] * Complex64::new(sigma[(k, l)], 0.0) * h_conj[(l, j)];
}
}
}
}
// Geweke's spectral Granger causality
let s_xx = s[(0, 0)].re;
let s_yy = s[(1, 1)].re;
// Intrinsic power (what would remain without the other signal)
let sigma_xx = sigma[(0, 0)];
let sigma_yy = sigma[(1, 1)];
let h_xx = h[(0, 0)].norm_sqr();
let h_yy = h[(1, 1)].norm_sqr();
let _h_xy = h[(0, 1)].norm_sqr();
let _h_yx = h[(1, 0)].norm_sqr();
// GC X->Y: based on how much H_yx contributes
let gc_xy = if s_yy > 1e-30 {
let intrinsic_y = h_yy * sigma_yy;
(s_yy / intrinsic_y.max(1e-30)).ln().max(0.0)
} else {
0.0
};
// GC Y->X: based on how much H_xy contributes
let gc_yx = if s_xx > 1e-30 {
let intrinsic_x = h_xx * sigma_xx;
(s_xx / intrinsic_x.max(1e-30)).ln().max(0.0)
} else {
0.0
};
// Instantaneous causality (correlation of innovations)
let gc_inst = if sigma[(0, 0)] > 1e-30 && sigma[(1, 1)] > 1e-30 {
let r = sigma[(0, 1)] / (sigma[(0, 0)] * sigma[(1, 1)]).sqrt();
-(1.0 - r * r).ln().max(0.0)
} else {
0.0
};
// Total
let gc_t = gc_xy + gc_yx + gc_inst;
gc_x_to_y.push(gc_xy);
gc_y_to_x.push(gc_yx);
gc_instantaneous.push(gc_inst);
gc_total.push(gc_t);
}
Ok(SpectralGrangerResult {
frequencies,
gc_x_to_y,
gc_y_to_x,
gc_instantaneous,
gc_total,
order,
})
}
/// Compute Granger causality for all channel pairs
pub fn granger_all_pairs(
data: &[Vec<f64>], // [n_channels][n_samples]
order: usize,
) -> ConnectivityResult<Vec<Vec<f64>>> {
let n_channels = data.len();
// Generate all directed pairs
let mut results = Vec::new();
for i in 0..n_channels {
for j in 0..n_channels {
if i != j {
let gc = granger_causality(&data[i], &data[j], order)?;
results.push(vec![gc.gc_value]); // Single value per pair
}
}
}
Ok(results)
}
/// Select optimal model order using information criterion
pub fn select_order(
data: &[Vec<f64>],
max_order: usize,
criterion: OrderCriterion,
) -> ConnectivityResult<usize> {
let n_samples = data[0].len();
let mut best_order = 1;
let mut best_criterion = f64::INFINITY;
for order in 1..=max_order {
if let Ok(model) = VarModel::fit(data, order) {
let crit = match criterion {
OrderCriterion::Aic => model.aic(n_samples),
OrderCriterion::Bic => model.bic(n_samples),
OrderCriterion::Hqc => {
// HQ = n * ln(det) + 2k * ln(ln(n))
let n = n_samples - order;
let det = model.residual_cov.determinant().max(1e-30);
let k = (order * data.len() * data.len()) as f64;
n as f64 * det.ln() + 2.0 * k * (n as f64).ln().ln()
}
OrderCriterion::Fixed => return Ok(max_order),
};
if crit < best_criterion {
best_criterion = crit;
best_order = order;
}
}
}
Ok(best_order)
}
// ========== Helper functions ==========
/// Fit AR model and return residual variance
fn fit_ar_variance(y: &[f64], order: usize) -> ConnectivityResult<f64> {
let n = y.len();
let t = n - order;
// Build regression matrix
let mut x_mat = DMatrix::zeros(t, order);
let mut y_vec = DVector::zeros(t);
for i in 0..t {
y_vec[i] = y[order + i];
for j in 0..order {
x_mat[(i, j)] = y[order + i - j - 1];
}
}
// OLS
let xtx = x_mat.transpose() * &x_mat;
let xty = x_mat.transpose() * &y_vec;
let xtx_inv = VarModel::pseudoinverse(&xtx)?;
let b = &xtx_inv * &xty;
// Residual variance
let y_pred = &x_mat * &b;
let residuals = &y_vec - &y_pred;
let var = residuals.dot(&residuals) / (t - order) as f64;
Ok(var)
}
/// Survival function of F-distribution (approximate)
fn f_distribution_sf(f: f64, df1: usize, df2: usize) -> f64 {
if f <= 0.0 || df1 == 0 || df2 == 0 {
return 1.0;
}
// Use Beta distribution relationship:
// F(x; d1, d2) = I_{d1*x/(d1*x+d2)}(d1/2, d2/2)
// where I is the regularized incomplete beta function
// Simplified approximation for large df
let d1 = df1 as f64;
let d2 = df2 as f64;
let _x = d1 * f / (d1 * f + d2);
// Wilson-Hilferty approximation
let _a = d1 / 2.0;
let _b = d2 / 2.0;
// Simple normal approximation for p-value
let mean = d2 / (d2 - 2.0).max(1.0);
let var = 2.0 * d2 * d2 * (d1 + d2 - 2.0) / (d1 * (d2 - 2.0).powi(2) * (d2 - 4.0).max(1.0));
let z = (f - mean) / var.sqrt();
0.5 * (1.0 - erf(z / 2.0_f64.sqrt()))
}
/// Error function approximation
fn erf(x: f64) -> f64 {
// Horner's method approximation
let a1 = 0.254829592;
let a2 = -0.284496736;
let a3 = 1.421413741;
let a4 = -1.453152027;
let a5 = 1.061405429;
let p = 0.3275911;
let sign = if x < 0.0 { -1.0 } else { 1.0 };
let x = x.abs();
let t = 1.0 / (1.0 + p * x);
let y = 1.0 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * (-x * x).exp();
sign * y
}
#[cfg(test)]
mod tests {
use super::*;
fn create_coupled_series(n: usize, coupling: f64) -> (Vec<f64>, Vec<f64>) {
let mut x = vec![0.0; n];
let mut y = vec![0.0; n];
// AR(1) process for x
for i in 1..n {
x[i] = 0.8 * x[i - 1] + 0.1 * ((i as f64 * 0.1).sin());
}
// Y depends on past X (Granger causality from X to Y)
for i in 1..n {
y[i] = 0.5 * y[i - 1] + coupling * x[i - 1] + 0.1 * ((i as f64 * 0.2).cos());
}
(x, y)
}
#[test]
fn test_granger_causality_coupled() {
let (x, y) = create_coupled_series(500, 0.5);
// X should cause Y
let gc_x_to_y = granger_causality(&x, &y, 5).unwrap();
assert!(gc_x_to_y.gc_value > 0.0, "GC X→Y should be positive");
// Y should not cause X (or weakly)
let gc_y_to_x = granger_causality(&y, &x, 5).unwrap();
// GC X→Y should be much larger than GC Y→X
assert!(
gc_x_to_y.gc_value > gc_y_to_x.gc_value * 0.5,
"GC X→Y ({}) should be larger than GC Y→X ({})",
gc_x_to_y.gc_value,
gc_y_to_x.gc_value
);
}
#[test]
fn test_granger_uncoupled() {
// Create independent series
let x: Vec<f64> = (0..500).map(|i| (i as f64 * 0.1).sin()).collect();
let y: Vec<f64> = (0..500).map(|i| (i as f64 * 0.17).cos()).collect();
let gc = granger_causality(&x, &y, 5).unwrap();
// GC should be small for independent series
assert!(
gc.gc_value < 0.5,
"GC for independent series should be small, got {}",
gc.gc_value
);
}
#[test]
fn test_var_model() {
let (x, y) = create_coupled_series(300, 0.3);
let data = vec![x, y];
let model = VarModel::fit(&data, 3).unwrap();
assert_eq!(model.order, 3);
assert_eq!(model.n_vars, 2);
assert_eq!(model.coefficients.len(), 3);
}
#[test]
fn test_spectral_granger() {
let (x, y) = create_coupled_series(500, 0.5);
let result = spectral_granger(&x, &y, 100.0, 5, Some(32)).unwrap();
assert_eq!(result.frequencies.len(), 32);
assert_eq!(result.gc_x_to_y.len(), 32);
assert_eq!(result.gc_y_to_x.len(), 32);
// GC values should be non-negative
assert!(result.gc_x_to_y.iter().all(|&v| v >= 0.0));
assert!(result.gc_y_to_x.iter().all(|&v| v >= 0.0));
}
#[test]
fn test_order_selection() {
let (x, y) = create_coupled_series(500, 0.3);
let data = vec![x, y];
let order = select_order(&data, 10, OrderCriterion::Bic).unwrap();
assert!(order >= 1 && order <= 10);
}
#[test]
fn test_granger_config() {
let config = GrangerConfig::default();
assert_eq!(config.order, 10);
let config2 = GrangerConfig::with_order(5);
assert_eq!(config2.order, 5);
}
}