//! 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, /// 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, /// Granger causality X → Y at each frequency pub gc_x_to_y: Vec, /// Granger causality Y → X at each frequency pub gc_y_to_x: Vec, /// Instantaneous causality at each frequency pub gc_instantaneous: Vec, /// Total coherence-like measure pub gc_total: Vec, /// 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>, /// Residual covariance matrix residual_cov: DMatrix, /// 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], order: usize) -> ConnectivityResult { 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> { self.coefficients.get(lag) } /// Get residual covariance matrix pub fn residual_covariance(&self) -> &DMatrix { &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 { 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) -> ConnectivityResult> { 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) -> ConnectivityResult> { 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 { 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, ) -> ConnectivityResult { 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 = (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], // [n_channels][n_samples] order: usize, ) -> ConnectivityResult>> { 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], max_order: usize, criterion: OrderCriterion, ) -> ConnectivityResult { 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 { 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, Vec) { 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 = (0..500).map(|i| (i as f64 * 0.1).sin()).collect(); let y: Vec = (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); } }