use crate::compute::{ComputeEngine, CpuEngine};
use crate::multivariate::context::MultiSeriesContext;
use crate::multivariate::error::MultivariateError;
#[derive(Debug, Clone)]
pub struct GrangerResult {
pub f_statistic: f64,
pub p_value: f64,
pub significant: bool,
pub lag_order: usize,
pub cause_idx: usize,
pub effect_idx: usize,
}
pub fn granger_causality_test(
ctx: &MultiSeriesContext,
cause_idx: usize,
effect_idx: usize,
lag_order: usize,
significance: f64,
) -> Result<GrangerResult, MultivariateError> {
let k = ctx.matrix.n_series();
let n = ctx.matrix.n_timestamps();
let p = lag_order;
if cause_idx >= k {
return Err(MultivariateError::SeriesNotFound(format!(
"cause_idx {cause_idx} >= n_series {k}"
)));
}
if effect_idx >= k {
return Err(MultivariateError::SeriesNotFound(format!(
"effect_idx {effect_idx} >= n_series {k}"
)));
}
if cause_idx == effect_idx {
return Err(MultivariateError::InvalidParameter(
"cause and effect must be different series".into(),
));
}
if p == 0 {
return Err(MultivariateError::InvalidParameter(
"lag_order must be >= 1".into(),
));
}
let t = n.saturating_sub(p); let dim_u = 1 + p * k; let _dim_r = 1 + p * (k - 1); let df_num = p as f64; let df_den = t as f64 - dim_u as f64;
if t < dim_u + 1 {
return Err(MultivariateError::InsufficientData {
min: dim_u + p + 1,
got: n,
});
}
if df_den <= 0.0 {
return Err(MultivariateError::InsufficientData {
min: dim_u + p + 1,
got: n,
});
}
let engine = CpuEngine::default();
let rss_u = ols_rss(ctx, effect_idx, None, p, &engine)?;
let rss_r = ols_rss(ctx, effect_idx, Some(cause_idx), p, &engine)?;
let f_stat = if rss_u.abs() < 1e-30 {
0.0
} else {
((rss_r - rss_u) / df_num) / (rss_u / df_den)
};
let p_value = f_distribution_sf(f_stat.max(0.0), df_num, df_den);
let significant = p_value < significance;
Ok(GrangerResult {
f_statistic: f_stat.max(0.0),
p_value,
significant,
lag_order: p,
cause_idx,
effect_idx,
})
}
pub fn granger_causality_test_robust(
ctx: &MultiSeriesContext,
cause_idx: usize,
effect_idx: usize,
lag_order: usize,
significance: f64,
) -> Result<GrangerResult, MultivariateError> {
let k = ctx.matrix.n_series();
let n = ctx.matrix.n_timestamps();
let p = lag_order;
if cause_idx >= k {
return Err(MultivariateError::SeriesNotFound(format!(
"cause_idx {cause_idx} >= n_series {k}"
)));
}
if effect_idx >= k {
return Err(MultivariateError::SeriesNotFound(format!(
"effect_idx {effect_idx} >= n_series {k}"
)));
}
if cause_idx == effect_idx {
return Err(MultivariateError::InvalidParameter(
"cause and effect must be different series".into(),
));
}
if p == 0 {
return Err(MultivariateError::InvalidParameter(
"lag_order must be >= 1".into(),
));
}
let t = n.saturating_sub(p);
let dim = 1 + p * k; let df_den = t as f64 - dim as f64;
if t < dim + 1 {
return Err(MultivariateError::InsufficientData {
min: dim + p + 1,
got: n,
});
}
if df_den <= 0.0 {
return Err(MultivariateError::InsufficientData {
min: dim + p + 1,
got: n,
});
}
let engine = CpuEngine::default();
let mut x_mat = vec![0.0_f64; t * dim];
let mut y_vec = vec![0.0_f64; t];
for (row, obs) in (p..n).enumerate() {
y_vec[row] = ctx.matrix.data[effect_idx][obs];
x_mat[row * dim] = 1.0; let mut col = 1;
for l in 1..=p {
for s in 0..k {
x_mat[row * dim + col] = ctx.matrix.data[s][obs - l];
col += 1;
}
}
}
let mut xtx = vec![0.0_f64; dim * dim];
for row in 0..t {
for i in 0..dim {
let xi = x_mat[row * dim + i];
for j in i..dim {
let v = xi * x_mat[row * dim + j];
xtx[i * dim + j] += v;
if i != j {
xtx[j * dim + i] += v;
}
}
}
}
let mut xty = vec![0.0_f64; dim];
for row in 0..t {
let y = y_vec[row];
for i in 0..dim {
xty[i] += x_mat[row * dim + i] * y;
}
}
for i in 0..dim {
xtx[i * dim + i] += 1e-8;
}
let beta = engine
.batch_matrix_solve(&xtx, &xty, dim)
.map_err(|e| MultivariateError::Compute(e.to_string()))?;
let mut residuals = vec![0.0_f64; t];
for row in 0..t {
let mut fitted = 0.0;
for j in 0..dim {
fitted += x_mat[row * dim + j] * beta[j];
}
residuals[row] = y_vec[row] - fitted;
}
let mut xtx_inv = vec![0.0_f64; dim * dim];
for col in 0..dim {
let mut rhs = vec![0.0_f64; dim];
rhs[col] = 1.0;
let sol = engine
.batch_matrix_solve(&xtx, &rhs, dim)
.map_err(|e| MultivariateError::Compute(e.to_string()))?;
for row in 0..dim {
xtx_inv[row * dim + col] = sol[row];
}
}
let mut hat_diag = vec![0.0_f64; t];
for row in 0..t {
let mut h = 0.0;
for i in 0..dim {
let mut inner = 0.0;
for j in 0..dim {
inner += xtx_inv[i * dim + j] * x_mat[row * dim + j];
}
h += x_mat[row * dim + i] * inner;
}
hat_diag[row] = h.clamp(0.0, 1.0 - 1e-12);
}
let mut meat = vec![0.0_f64; dim * dim];
for row in 0..t {
let denom = (1.0 - hat_diag[row]).powi(2);
let w = residuals[row].powi(2) / denom;
for i in 0..dim {
let xi_w = x_mat[row * dim + i] * w;
for j in i..dim {
let v = xi_w * x_mat[row * dim + j];
meat[i * dim + j] += v;
if i != j {
meat[j * dim + i] += v;
}
}
}
}
let mut tmp = vec![0.0_f64; dim * dim];
for i in 0..dim {
for j in 0..dim {
let mut s = 0.0;
for c in 0..dim {
s += xtx_inv[i * dim + c] * meat[c * dim + j];
}
tmp[i * dim + j] = s;
}
}
let mut v_hc3 = vec![0.0_f64; dim * dim];
for i in 0..dim {
for j in 0..dim {
let mut s = 0.0;
for c in 0..dim {
s += tmp[i * dim + c] * xtx_inv[c * dim + j];
}
v_hc3[i * dim + j] = s;
}
}
let mut cause_cols = Vec::with_capacity(p);
for l in 1..=p {
cause_cols.push(1 + (l - 1) * k + cause_idx);
}
let r_beta: Vec<f64> = cause_cols.iter().map(|&c| beta[c]).collect();
let mut r_v_rt = vec![0.0_f64; p * p];
for (i, &ci) in cause_cols.iter().enumerate() {
for (j, &cj) in cause_cols.iter().enumerate() {
r_v_rt[i * p + j] = v_hc3[ci * dim + cj];
}
}
for i in 0..p {
r_v_rt[i * p + i] += 1e-12;
}
let inv_r_beta = engine
.batch_matrix_solve(&r_v_rt, &r_beta, p)
.map_err(|e| MultivariateError::Compute(e.to_string()))?;
let wald: f64 = r_beta
.iter()
.zip(inv_r_beta.iter())
.map(|(a, b)| a * b)
.sum();
let f_stat = (wald / p as f64).max(0.0);
let p_value = f_distribution_sf(f_stat, p as f64, df_den);
let significant = p_value < significance;
Ok(GrangerResult {
f_statistic: f_stat,
p_value,
significant,
lag_order: p,
cause_idx,
effect_idx,
})
}
fn ols_rss(
ctx: &MultiSeriesContext,
effect_idx: usize,
exclude_idx: Option<usize>,
p: usize,
engine: &CpuEngine,
) -> Result<f64, MultivariateError> {
let k = ctx.matrix.n_series();
let n = ctx.matrix.n_timestamps();
let _t = n - p;
let dim = if exclude_idx.is_some() {
1 + p * (k - 1)
} else {
1 + p * k
};
let mut xtx = vec![0.0; dim * dim];
let mut xty = vec![0.0; dim];
let mut yty = 0.0;
for obs in p..n {
let y = ctx.matrix.data[effect_idx][obs];
yty += y * y;
let mut row = Vec::with_capacity(dim);
row.push(1.0); for l in 1..=p {
for s in 0..k {
if exclude_idx == Some(s) {
continue;
}
row.push(ctx.matrix.data[s][obs - l]);
}
}
debug_assert_eq!(row.len(), dim);
for i in 0..dim {
for j in 0..dim {
xtx[i * dim + j] += row[i] * row[j];
}
xty[i] += row[i] * y;
}
}
for i in 0..dim {
xtx[i * dim + i] += 1e-8;
}
let beta = engine
.batch_matrix_solve(&xtx, &xty, dim)
.map_err(|e| MultivariateError::Compute(e.to_string()))?;
let beta_xty: f64 = beta.iter().zip(xty.iter()).map(|(b, xy)| b * xy).sum();
let rss = (yty - beta_xty).max(0.0);
Ok(rss)
}
fn f_distribution_sf(x: f64, d1: f64, d2: f64) -> f64 {
if x <= 0.0 || d1 <= 0.0 || d2 <= 0.0 {
return 1.0;
}
let z = d2 / (d2 + d1 * x);
regularized_incomplete_beta(z, d2 / 2.0, d1 / 2.0)
}
fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
if x > (a + 1.0) / (a + b + 2.0) {
return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
}
let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b) - a.ln();
let prefix = ln_prefix.exp();
let max_iter = 200;
let eps = 1e-14;
let tiny = 1e-30;
let mut c = 1.0;
let mut d = 1.0 - (a + b) * x / (a + 1.0);
if d.abs() < tiny {
d = tiny;
}
d = 1.0 / d;
let mut h = d;
for m in 1..=max_iter {
let m_f64 = m as f64;
let num_even = m_f64 * (b - m_f64) * x / ((a + 2.0 * m_f64 - 1.0) * (a + 2.0 * m_f64));
d = 1.0 + num_even * d;
if d.abs() < tiny {
d = tiny;
}
c = 1.0 + num_even / c;
if c.abs() < tiny {
c = tiny;
}
d = 1.0 / d;
h *= d * c;
let num_odd =
-((a + m_f64) * (a + b + m_f64)) * x / ((a + 2.0 * m_f64) * (a + 2.0 * m_f64 + 1.0));
d = 1.0 + num_odd * d;
if d.abs() < tiny {
d = tiny;
}
c = 1.0 + num_odd / c;
if c.abs() < tiny {
c = tiny;
}
d = 1.0 / d;
let delta = d * c;
h *= delta;
if (delta - 1.0).abs() < eps {
break;
}
}
prefix * h
}
fn ln_beta(a: f64, b: f64) -> f64 {
ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
}
fn ln_gamma(z: f64) -> f64 {
const COEFFS: [f64; 7] = [
1.000000000190015,
76.18009172947146,
-86.50532032941678,
24.01409824083091,
-1.231739572450155,
0.1208650973866179e-2,
-0.5395239384953e-5,
];
let x = z;
let tmp = x + 5.5;
let tmp = (x + 0.5) * tmp.ln() - tmp;
let mut ser = COEFFS[0];
for (i, &c) in COEFFS[1..].iter().enumerate() {
ser += c / (x + i as f64 + 1.0);
}
tmp + (2.5066282746310005_f64 * ser / x).ln()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::multivariate::context::MultiSeriesContext;
fn causal_ctx() -> MultiSeriesContext {
let n = 200;
let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
let mut x = vec![0.0; n];
let mut rng_state: u64 = 42;
for i in 1..n {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let noise = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5;
x[i] = 0.7 * x[i - 1] + noise;
}
let mut y = vec![0.0; n];
for i in 1..n {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let noise = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.2;
y[i] = 0.8 * x[i - 1] + noise;
}
MultiSeriesContext::build(vec![("x".into(), ts.clone(), x), ("y".into(), ts, y)], None)
.unwrap()
}
fn independent_ctx() -> MultiSeriesContext {
let n = 200;
let ts: Vec<i64> = (0..n as i64).map(|i| i * 1_000_000_000).collect();
let mut x = vec![0.0; n];
let mut y = vec![0.0; n];
let mut rng_state: u64 = 12345;
for i in 1..n {
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let noise_x = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5;
x[i] = 0.5 * x[i - 1] + noise_x;
rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
let noise_y = ((rng_state >> 33) as f64 / (1u64 << 31) as f64 - 0.5) * 0.5;
y[i] = 0.5 * y[i - 1] + noise_y;
}
MultiSeriesContext::build(vec![("x".into(), ts.clone(), x), ("y".into(), ts, y)], None)
.unwrap()
}
#[test]
fn granger_detects_causality() {
let ctx = causal_ctx();
let result = granger_causality_test(&ctx, 0, 1, 1, 0.05).unwrap();
assert!(
result.significant,
"x should Granger-cause y: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
assert!(result.f_statistic > 10.0, "F-stat should be large");
assert!(result.p_value < 0.01, "p-value should be very small");
}
#[test]
fn granger_no_reverse_causality() {
let ctx = causal_ctx();
let result = granger_causality_test(&ctx, 1, 0, 1, 0.05).unwrap();
assert!(
!result.significant,
"y should not Granger-cause x: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
}
#[test]
fn granger_independent_series_not_significant() {
let ctx = independent_ctx();
let result = granger_causality_test(&ctx, 0, 1, 1, 0.05).unwrap();
assert!(
!result.significant,
"independent series should not show Granger causality: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
}
#[test]
fn granger_rejects_same_series() {
let ctx = causal_ctx();
let err = granger_causality_test(&ctx, 0, 0, 1, 0.05).unwrap_err();
assert!(
matches!(err, MultivariateError::InvalidParameter(_)),
"same cause and effect should be rejected"
);
}
#[test]
fn granger_rejects_zero_lag() {
let ctx = causal_ctx();
let err = granger_causality_test(&ctx, 0, 1, 0, 0.05).unwrap_err();
assert!(matches!(err, MultivariateError::InvalidParameter(_)));
}
#[test]
fn granger_rejects_out_of_range_index() {
let ctx = causal_ctx();
let err = granger_causality_test(&ctx, 5, 1, 1, 0.05).unwrap_err();
assert!(matches!(err, MultivariateError::SeriesNotFound(_)));
}
#[test]
fn granger_higher_lag_order() {
let ctx = causal_ctx();
let result = granger_causality_test(&ctx, 0, 1, 2, 0.05).unwrap();
assert!(
result.significant,
"x should Granger-cause y at lag 2: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
}
#[test]
fn f_distribution_sf_basic() {
let p = f_distribution_sf(3.94, 1.0, 100.0);
assert!((p - 0.05).abs() < 0.01, "p={p} should be near 0.05");
assert!((f_distribution_sf(0.0, 1.0, 100.0) - 1.0).abs() < 1e-10);
let p = f_distribution_sf(100.0, 1.0, 100.0);
assert!(p < 0.001, "large F should give p near 0: p={p}");
}
#[test]
fn granger_robust_detects_causality() {
let ctx = causal_ctx();
let result = granger_causality_test_robust(&ctx, 0, 1, 1, 0.05).unwrap();
assert!(
result.significant,
"robust: x should Granger-cause y: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
}
#[test]
fn granger_robust_independent_not_significant() {
let ctx = independent_ctx();
let result = granger_causality_test_robust(&ctx, 0, 1, 1, 0.05).unwrap();
assert!(
!result.significant,
"robust: independent series should not show causality: F={:.2}, p={:.6}",
result.f_statistic, result.p_value
);
}
}