787 lines
23 KiB
Rust
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);
|
|
}
|
|
}
|