use crate::error::TimeSeriesError;
use scirs2_core::ndarray::{s, Array1, Array2, Axis};
use scirs2_core::validation::checkarray_finite;
use super::{
chi_squared_p_value, compute_regression_likelihood, compute_regression_rss,
f_distribution_p_value, solve_linear_system, CausalityResult,
};
#[derive(Debug, Clone)]
pub struct GrangerCausalityResult {
pub f_statistic: f64,
pub p_value: f64,
pub is_causal: bool,
pub significance_level: f64,
pub degrees_of_freedom: (usize, usize),
pub ll_restricted: f64,
pub ll_unrestricted: f64,
pub chi2_statistic: f64,
pub chi2_p_value: f64,
pub lag: usize,
}
#[derive(Debug, Clone)]
pub struct GrangerConfig {
pub max_lag: usize,
pub significance_level: f64,
pub include_trend: bool,
pub include_constant: bool,
pub auto_lag: bool,
pub lag_criterion: LagSelectionCriterion,
}
impl Default for GrangerConfig {
fn default() -> Self {
Self {
max_lag: 4,
significance_level: 0.05,
include_trend: false,
include_constant: true,
auto_lag: false,
lag_criterion: LagSelectionCriterion::BIC,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LagSelectionCriterion {
AIC,
BIC,
HQIC,
}
#[derive(Debug, Clone)]
pub struct LagSelectionResult {
pub optimal_lag_aic: usize,
pub optimal_lag_bic: usize,
pub optimal_lag_hqic: usize,
pub aic_values: Vec<f64>,
pub bic_values: Vec<f64>,
pub hqic_values: Vec<f64>,
pub recommended_lag: usize,
pub criterion: LagSelectionCriterion,
}
#[derive(Debug, Clone)]
pub struct MultivariateCausalityResult {
pub f_statistic: f64,
pub p_value: f64,
pub is_causal: bool,
pub log_likelihood_ratio: f64,
pub n_conditioning: usize,
pub lag: usize,
}
#[derive(Debug, Clone)]
pub struct SpectralGrangerResult {
pub frequencies: Vec<f64>,
pub causality_spectrum: Vec<f64>,
pub total_causality: f64,
pub peak_frequency: f64,
pub peak_causality: f64,
pub lag: usize,
}
pub fn granger_test(
x: &Array1<f64>,
y: &Array1<f64>,
config: &GrangerConfig,
) -> CausalityResult<GrangerCausalityResult> {
checkarray_finite(x, "x")?;
checkarray_finite(y, "y")?;
if x.len() != y.len() {
return Err(TimeSeriesError::InvalidInput(
"Time series must have the same length".to_string(),
));
}
let lag = if config.auto_lag {
let lag_result = select_optimal_lag(x, y, config.max_lag, config.lag_criterion)?;
lag_result.recommended_lag.max(1)
} else {
config.max_lag
};
if x.len() <= lag + 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series too short for the specified lag".to_string(),
));
}
let n = x.len() - lag;
let n_extra =
if config.include_constant { 1 } else { 0 } + if config.include_trend { 1 } else { 0 };
let unrestricted_cols = 2 * lag + n_extra;
let restricted_cols = lag + n_extra;
let mut data = Array2::zeros((n, unrestricted_cols));
let mut y_vec = Array1::zeros(n);
for i in 0..n {
let row_idx = lag + i;
y_vec[i] = y[row_idx];
for l in 1..=lag {
data[[i, l - 1]] = y[row_idx - l];
}
for l in 1..=lag {
data[[i, lag + l - 1]] = x[row_idx - l];
}
let mut col = 2 * lag;
if config.include_constant {
data[[i, col]] = 1.0;
col += 1;
}
if config.include_trend {
data[[i, col]] = i as f64;
}
}
let unrestricted_rss = compute_regression_rss(&data, &y_vec)?;
let unrestricted_ll = compute_regression_likelihood(&data, &y_vec)?;
let restricted_data = if n_extra > 0 {
let mut rd = Array2::zeros((n, restricted_cols));
for i in 0..n {
for j in 0..lag {
rd[[i, j]] = data[[i, j]];
}
for j in 0..n_extra {
rd[[i, lag + j]] = data[[i, 2 * lag + j]];
}
}
rd
} else {
data.slice(s![.., ..lag]).to_owned()
};
let restricted_rss = compute_regression_rss(&restricted_data, &y_vec)?;
let restricted_ll = compute_regression_likelihood(&restricted_data, &y_vec)?;
let df_num = lag;
let df_den = if n > unrestricted_cols {
n - unrestricted_cols
} else {
1
};
let f_statistic = if df_den > 0 && unrestricted_rss > 0.0 {
((restricted_rss - unrestricted_rss) / df_num as f64) / (unrestricted_rss / df_den as f64)
} else {
0.0
};
let p_value = f_distribution_p_value(f_statistic, df_num, df_den);
let chi2_statistic = if unrestricted_rss > 0.0 && restricted_rss > unrestricted_rss {
n as f64 * (restricted_rss / unrestricted_rss).ln()
} else {
0.0
};
let chi2_p_value = chi_squared_p_value(chi2_statistic, lag);
Ok(GrangerCausalityResult {
f_statistic,
p_value,
is_causal: p_value < config.significance_level,
significance_level: config.significance_level,
degrees_of_freedom: (df_num, df_den),
ll_restricted: restricted_ll,
ll_unrestricted: unrestricted_ll,
chi2_statistic,
chi2_p_value,
lag,
})
}
pub fn select_optimal_lag(
x: &Array1<f64>,
y: &Array1<f64>,
max_lag: usize,
criterion: LagSelectionCriterion,
) -> CausalityResult<LagSelectionResult> {
checkarray_finite(x, "x")?;
checkarray_finite(y, "y")?;
if x.len() != y.len() {
return Err(TimeSeriesError::InvalidInput(
"Time series must have the same length".to_string(),
));
}
if max_lag == 0 {
return Err(TimeSeriesError::InvalidInput(
"max_lag must be at least 1".to_string(),
));
}
let n_total = x.len();
let effective_max_lag = max_lag.min(n_total / 3);
if effective_max_lag == 0 {
return Err(TimeSeriesError::InvalidInput(
"Time series too short for lag selection".to_string(),
));
}
let mut aic_values = Vec::with_capacity(effective_max_lag);
let mut bic_values = Vec::with_capacity(effective_max_lag);
let mut hqic_values = Vec::with_capacity(effective_max_lag);
for lag in 1..=effective_max_lag {
let n = n_total - lag;
if n <= 2 * lag + 2 {
break;
}
let n_params = 2 * lag + 1; let mut design = Array2::zeros((n, n_params));
let mut response = Array1::zeros(n);
for i in 0..n {
let row_idx = lag + i;
response[i] = y[row_idx];
for l in 1..=lag {
design[[i, l - 1]] = y[row_idx - l];
design[[i, lag + l - 1]] = x[row_idx - l];
}
design[[i, 2 * lag]] = 1.0; }
let rss = compute_regression_rss(&design, &response)?;
let n_f = n as f64;
let k_f = n_params as f64;
let sigma2 = rss / n_f;
if sigma2 <= 0.0 {
continue;
}
let log_sigma2 = sigma2.ln();
let aic = n_f * log_sigma2 + 2.0 * k_f;
let bic = n_f * log_sigma2 + k_f * n_f.ln();
let hqic = n_f * log_sigma2 + 2.0 * k_f * n_f.ln().ln();
aic_values.push(aic);
bic_values.push(bic);
hqic_values.push(hqic);
}
if aic_values.is_empty() {
return Err(TimeSeriesError::InvalidInput(
"Could not compute information criteria for any lag".to_string(),
));
}
let optimal_lag_aic = find_min_index(&aic_values) + 1;
let optimal_lag_bic = find_min_index(&bic_values) + 1;
let optimal_lag_hqic = find_min_index(&hqic_values) + 1;
let recommended_lag = match criterion {
LagSelectionCriterion::AIC => optimal_lag_aic,
LagSelectionCriterion::BIC => optimal_lag_bic,
LagSelectionCriterion::HQIC => optimal_lag_hqic,
};
Ok(LagSelectionResult {
optimal_lag_aic,
optimal_lag_bic,
optimal_lag_hqic,
aic_values,
bic_values,
hqic_values,
recommended_lag,
criterion,
})
}
pub fn conditional_granger_test(
x: &Array1<f64>,
y: &Array1<f64>,
z: &Array2<f64>,
lag: usize,
significance_level: f64,
) -> CausalityResult<MultivariateCausalityResult> {
checkarray_finite(x, "x")?;
checkarray_finite(y, "y")?;
let n_total = x.len();
if n_total != y.len() || n_total != z.nrows() {
return Err(TimeSeriesError::InvalidInput(
"All time series must have the same length".to_string(),
));
}
if lag == 0 {
return Err(TimeSeriesError::InvalidInput(
"Lag must be at least 1".to_string(),
));
}
let n_cond = z.ncols();
let n = n_total - lag;
if n <= (2 + n_cond) * lag + 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series too short for conditional Granger causality with given lag and conditioning variables".to_string(),
));
}
let unrestricted_params = (1 + 1 + n_cond) * lag + 1;
let mut unrestricted_design = Array2::zeros((n, unrestricted_params));
let mut response = Array1::zeros(n);
for i in 0..n {
let row_idx = lag + i;
response[i] = y[row_idx];
let mut col = 0;
for l in 1..=lag {
unrestricted_design[[i, col]] = y[row_idx - l];
col += 1;
}
for l in 1..=lag {
unrestricted_design[[i, col]] = x[row_idx - l];
col += 1;
}
for c in 0..n_cond {
for l in 1..=lag {
unrestricted_design[[i, col]] = z[[row_idx - l, c]];
col += 1;
}
}
unrestricted_design[[i, col]] = 1.0;
}
let restricted_params = (1 + n_cond) * lag + 1;
let mut restricted_design = Array2::zeros((n, restricted_params));
for i in 0..n {
let row_idx = lag + i;
let mut col = 0;
for l in 1..=lag {
restricted_design[[i, col]] = y[row_idx - l];
col += 1;
}
for c in 0..n_cond {
for l in 1..=lag {
restricted_design[[i, col]] = z[[row_idx - l, c]];
col += 1;
}
}
restricted_design[[i, col]] = 1.0;
}
let unrestricted_rss = compute_regression_rss(&unrestricted_design, &response)?;
let restricted_rss = compute_regression_rss(&restricted_design, &response)?;
let unrestricted_ll = compute_regression_likelihood(&unrestricted_design, &response)?;
let restricted_ll = compute_regression_likelihood(&restricted_design, &response)?;
let df_num = lag; let df_den = if n > unrestricted_params {
n - unrestricted_params
} else {
1
};
let f_statistic = if df_den > 0 && unrestricted_rss > 0.0 {
((restricted_rss - unrestricted_rss) / df_num as f64) / (unrestricted_rss / df_den as f64)
} else {
0.0
};
let p_value = f_distribution_p_value(f_statistic, df_num, df_den);
let log_likelihood_ratio = if unrestricted_rss > 0.0 && restricted_rss > unrestricted_rss {
n as f64 * (restricted_rss / unrestricted_rss).ln()
} else {
0.0
};
Ok(MultivariateCausalityResult {
f_statistic,
p_value,
is_causal: p_value < significance_level,
log_likelihood_ratio,
n_conditioning: n_cond,
lag,
})
}
pub fn spectral_granger_causality(
x: &Array1<f64>,
y: &Array1<f64>,
lag: usize,
n_freqs: usize,
) -> CausalityResult<SpectralGrangerResult> {
checkarray_finite(x, "x")?;
checkarray_finite(y, "y")?;
if x.len() != y.len() {
return Err(TimeSeriesError::InvalidInput(
"Time series must have the same length".to_string(),
));
}
let n_total = x.len();
if n_total <= lag + 2 {
return Err(TimeSeriesError::InvalidInput(
"Time series too short for spectral Granger causality".to_string(),
));
}
let n = n_total - lag;
let n_params = 2 * lag + 1;
let mut design = Array2::zeros((n, n_params));
let mut resp_y = Array1::zeros(n);
let mut resp_x = Array1::zeros(n);
for i in 0..n {
let row_idx = lag + i;
resp_y[i] = y[row_idx];
resp_x[i] = x[row_idx];
for l in 1..=lag {
design[[i, l - 1]] = y[row_idx - l];
design[[i, lag + l - 1]] = x[row_idx - l];
}
design[[i, 2 * lag]] = 1.0;
}
let xt = design.t();
let xtx = xt.dot(&design);
let xty = xt.dot(&resp_y);
let beta_y = solve_linear_system(&xtx, &xty)?;
let xtx2 = xt.dot(&design);
let xtx_x = xt.dot(&resp_x);
let beta_x = solve_linear_system(&xtx2, &xtx_x)?;
let fitted_y = design.dot(&beta_y);
let fitted_x = design.dot(&beta_x);
let resid_y = &resp_y - &fitted_y;
let resid_x = &resp_x - &fitted_x;
let sigma_yy = resid_y.mapv(|v| v * v).sum() / n as f64;
let sigma_xx = resid_x.mapv(|v| v * v).sum() / n as f64;
let sigma_yx: f64 = resid_y
.iter()
.zip(resid_x.iter())
.map(|(a, b)| a * b)
.sum::<f64>()
/ n as f64;
let mut restricted_design = Array2::zeros((n, lag + 1));
for i in 0..n {
let row_idx = lag + i;
for l in 1..=lag {
restricted_design[[i, l - 1]] = y[row_idx - l];
}
restricted_design[[i, lag]] = 1.0;
}
let restricted_rss = compute_regression_rss(&restricted_design, &resp_y)?;
let sigma_yy_restricted = restricted_rss / n as f64;
let mut frequencies = Vec::with_capacity(n_freqs);
let mut causality_spectrum = Vec::with_capacity(n_freqs);
for k in 0..n_freqs {
let freq = k as f64 / (2.0 * n_freqs as f64); let omega = 2.0 * std::f64::consts::PI * freq;
let mut h_yy_re = 1.0;
let mut h_yy_im = 0.0;
let mut h_yx_re = 0.0;
let mut h_yx_im = 0.0;
for l in 1..=lag {
let phase = omega * l as f64;
let cos_phase = phase.cos();
let sin_phase = phase.sin();
let a_yy = beta_y[l - 1]; let a_yx = beta_y[lag + l - 1];
h_yy_re -= a_yy * cos_phase;
h_yy_im += a_yy * sin_phase;
h_yx_re -= a_yx * cos_phase;
h_yx_im += a_yx * sin_phase;
}
let h_yy_mag2 = h_yy_re * h_yy_re + h_yy_im * h_yy_im;
let h_yx_mag2 = h_yx_re * h_yx_re + h_yx_im * h_yx_im;
let spectral_full = if h_yy_mag2 > 1e-15 {
sigma_yy + (h_yx_mag2 / h_yy_mag2) * sigma_xx
} else {
sigma_yy
};
let spectral_gc = if spectral_full > 1e-15 && sigma_yy > 1e-15 {
(sigma_yy_restricted / spectral_full).ln().max(0.0)
} else {
0.0
};
frequencies.push(freq);
causality_spectrum.push(spectral_gc);
}
let total_causality = if n_freqs > 1 {
let df = 0.5 / n_freqs as f64;
causality_spectrum.iter().sum::<f64>() * df
} else {
causality_spectrum.first().copied().unwrap_or(0.0)
};
let (peak_idx, &peak_causality) = causality_spectrum
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or((0, &0.0));
let peak_frequency = frequencies.get(peak_idx).copied().unwrap_or(0.0);
Ok(SpectralGrangerResult {
frequencies,
causality_spectrum,
total_causality,
peak_frequency,
peak_causality,
lag,
})
}
pub fn granger_causality_test(
x: &Array1<f64>,
y: &Array1<f64>,
max_lag: usize,
) -> CausalityResult<GrangerCausalityResult> {
let config = GrangerConfig {
max_lag,
significance_level: 0.05,
include_trend: false,
include_constant: true,
auto_lag: false,
lag_criterion: LagSelectionCriterion::BIC,
};
granger_test(x, y, &config)
}
pub fn granger_causality_test_with_alpha(
x: &Array1<f64>,
y: &Array1<f64>,
max_lag: usize,
significance_level: f64,
) -> CausalityResult<GrangerCausalityResult> {
let config = GrangerConfig {
max_lag,
significance_level,
include_trend: false,
include_constant: true,
auto_lag: false,
lag_criterion: LagSelectionCriterion::BIC,
};
granger_test(x, y, &config)
}
pub fn granger_causality_bidirectional(
x: &Array1<f64>,
y: &Array1<f64>,
max_lag: usize,
) -> CausalityResult<(GrangerCausalityResult, GrangerCausalityResult)> {
let x_causes_y = granger_causality_test(x, y, max_lag)?;
let y_causes_x = granger_causality_test(y, x, max_lag)?;
Ok((x_causes_y, y_causes_x))
}
pub fn granger_causality_auto(
x: &Array1<f64>,
y: &Array1<f64>,
max_lag: usize,
) -> CausalityResult<GrangerCausalityResult> {
let config = GrangerConfig {
max_lag,
significance_level: 0.05,
include_trend: false,
include_constant: true,
auto_lag: true,
lag_criterion: LagSelectionCriterion::BIC,
};
granger_test(x, y, &config)
}
fn find_min_index(values: &[f64]) -> usize {
values
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
fn generate_causal_data(n: usize) -> (Array1<f64>, Array1<f64>) {
let mut x = Array1::zeros(n);
let mut y = Array1::zeros(n);
for i in 1..n {
x[i] = 0.5 * x[i - 1] + 0.3 * (i as f64 * 0.1).sin();
y[i] = 0.3 * y[i - 1] + 0.4 * x[i - 1] + 0.1 * (i as f64 * 0.2).cos();
}
(x, y)
}
#[test]
fn test_granger_causality() {
let (x, y) = generate_causal_data(100);
let config = GrangerConfig::default();
let result = granger_test(&x, &y, &config).expect("Granger test failed");
assert!(result.f_statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.chi2_statistic >= 0.0);
assert!(result.chi2_p_value >= 0.0 && result.chi2_p_value <= 1.0);
assert_eq!(result.lag, 4);
}
#[test]
fn test_granger_causality_convenience() {
let (x, y) = generate_causal_data(100);
let result = granger_causality_test(&x, &y, 4).expect("Convenience test failed");
assert!(result.f_statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert_eq!(result.degrees_of_freedom.0, 4);
}
#[test]
fn test_granger_bidirectional() {
let (x, y) = generate_causal_data(100);
let (x_causes_y, y_causes_x) =
granger_causality_bidirectional(&x, &y, 2).expect("Bidirectional test failed");
assert!(x_causes_y.f_statistic >= 0.0);
assert!(y_causes_x.f_statistic >= 0.0);
assert!(x_causes_y.p_value >= 0.0);
assert!(y_causes_x.p_value >= 0.0);
}
#[test]
fn test_granger_causality_no_effect() {
let n = 100;
let x = Array1::from_vec((0..n).map(|i| (i as f64 * 0.1).sin()).collect());
let y = Array1::from_vec((0..n).map(|i| (i as f64 * 0.3 + 2.0).cos()).collect());
let result = granger_causality_test(&x, &y, 2).expect("Test failed");
assert!(result.f_statistic.is_finite());
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
}
#[test]
fn test_granger_causality_mismatched_lengths() {
let x = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let y = Array1::from_vec(vec![1.0, 2.0]);
let result = granger_causality_test(&x, &y, 1);
assert!(result.is_err(), "Mismatched lengths should fail");
}
#[test]
fn test_granger_causality_with_alpha() {
let (x, y) = generate_causal_data(100);
let result_strict =
granger_causality_test_with_alpha(&x, &y, 2, 0.01).expect("Test failed");
assert_eq!(result_strict.significance_level, 0.01);
let result_loose = granger_causality_test_with_alpha(&x, &y, 2, 0.10).expect("Test failed");
assert_eq!(result_loose.significance_level, 0.10);
assert!(
(result_strict.f_statistic - result_loose.f_statistic).abs() < 1e-10,
"F-statistic should be the same regardless of alpha"
);
}
#[test]
fn test_granger_causality_series_too_short() {
let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let y = Array1::from_vec(vec![2.0, 3.0, 4.0, 5.0, 6.0]);
let result = granger_causality_test(&x, &y, 4);
assert!(result.is_err(), "Too-short series should fail");
}
#[test]
fn test_lag_selection() {
let (x, y) = generate_causal_data(200);
let result = select_optimal_lag(&x, &y, 10, LagSelectionCriterion::BIC)
.expect("Lag selection failed");
assert!(result.optimal_lag_aic >= 1);
assert!(result.optimal_lag_bic >= 1);
assert!(result.optimal_lag_hqic >= 1);
assert!(result.recommended_lag >= 1);
assert!(!result.aic_values.is_empty());
assert!(!result.bic_values.is_empty());
assert!(!result.hqic_values.is_empty());
}
#[test]
fn test_lag_selection_criteria() {
let (x, y) = generate_causal_data(200);
let aic_result = select_optimal_lag(&x, &y, 8, LagSelectionCriterion::AIC)
.expect("AIC selection failed");
let bic_result = select_optimal_lag(&x, &y, 8, LagSelectionCriterion::BIC)
.expect("BIC selection failed");
let hqic_result = select_optimal_lag(&x, &y, 8, LagSelectionCriterion::HQIC)
.expect("HQIC selection failed");
assert!(aic_result.recommended_lag >= 1 && aic_result.recommended_lag <= 8);
assert!(bic_result.recommended_lag >= 1 && bic_result.recommended_lag <= 8);
assert!(hqic_result.recommended_lag >= 1 && hqic_result.recommended_lag <= 8);
assert!(bic_result.optimal_lag_bic <= aic_result.optimal_lag_aic + 2);
}
#[test]
fn test_granger_auto_lag() {
let (x, y) = generate_causal_data(200);
let result = granger_causality_auto(&x, &y, 10).expect("Auto lag test failed");
assert!(result.f_statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert!(result.lag >= 1 && result.lag <= 10);
}
#[test]
fn test_conditional_granger() {
let n = 200;
let mut x = Array1::zeros(n);
let mut y = Array1::zeros(n);
let mut z1 = Array1::zeros(n);
for i in 1..n {
z1[i] = 0.5 * z1[i - 1] + 0.2 * (i as f64 * 0.05).sin();
x[i] = 0.4 * x[i - 1] + 0.3 * z1[i - 1] + 0.2 * (i as f64 * 0.1).sin();
y[i] =
0.3 * y[i - 1] + 0.4 * x[i - 1] + 0.1 * z1[i - 1] + 0.05 * (i as f64 * 0.2).cos();
}
let z = Array2::from_shape_vec((n, 1), z1.to_vec()).expect("Shape creation failed");
let result =
conditional_granger_test(&x, &y, &z, 2, 0.05).expect("Conditional Granger test failed");
assert!(result.f_statistic >= 0.0);
assert!(result.p_value >= 0.0 && result.p_value <= 1.0);
assert_eq!(result.n_conditioning, 1);
assert_eq!(result.lag, 2);
}
#[test]
fn test_spectral_granger() {
let (x, y) = generate_causal_data(200);
let result = spectral_granger_causality(&x, &y, 4, 64).expect("Spectral Granger failed");
assert_eq!(result.frequencies.len(), 64);
assert_eq!(result.causality_spectrum.len(), 64);
assert!(result.total_causality >= 0.0);
assert!(result.peak_frequency >= 0.0 && result.peak_frequency <= 0.5);
assert!(result.peak_causality >= 0.0);
assert_eq!(result.lag, 4);
}
#[test]
fn test_spectral_granger_independent() {
let n = 200;
let x = Array1::from_vec((0..n).map(|i| (i as f64 * 0.1).sin()).collect());
let y = Array1::from_vec((0..n).map(|i| (i as f64 * 0.37 + 1.5).cos()).collect());
let result = spectral_granger_causality(&x, &y, 2, 32).expect("Spectral Granger failed");
assert_eq!(result.frequencies.len(), 32);
assert!(result.total_causality.is_finite());
}
#[test]
fn test_chi_squared_statistic() {
let (x, y) = generate_causal_data(100);
let result = granger_causality_test(&x, &y, 2).expect("Test failed");
assert!(result.chi2_statistic >= 0.0);
assert!(result.chi2_p_value >= 0.0 && result.chi2_p_value <= 1.0);
}
}