pytorch-mvgc 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytorch_mvgc-0.1.0/LICENSE +21 -0
- pytorch_mvgc-0.1.0/PKG-INFO +64 -0
- pytorch_mvgc-0.1.0/README.md +47 -0
- pytorch_mvgc-0.1.0/fast_mvgc/__init__.py +4 -0
- pytorch_mvgc-0.1.0/fast_mvgc/core.py +169 -0
- pytorch_mvgc-0.1.0/fast_mvgc/cpu_engine.py +120 -0
- pytorch_mvgc-0.1.0/fast_mvgc/utils.py +20 -0
- pytorch_mvgc-0.1.0/pytorch_mvgc.egg-info/PKG-INFO +64 -0
- pytorch_mvgc-0.1.0/pytorch_mvgc.egg-info/SOURCES.txt +13 -0
- pytorch_mvgc-0.1.0/pytorch_mvgc.egg-info/dependency_links.txt +1 -0
- pytorch_mvgc-0.1.0/pytorch_mvgc.egg-info/requires.txt +3 -0
- pytorch_mvgc-0.1.0/pytorch_mvgc.egg-info/top_level.txt +1 -0
- pytorch_mvgc-0.1.0/setup.cfg +4 -0
- pytorch_mvgc-0.1.0/setup.py +24 -0
- pytorch_mvgc-0.1.0/tests/test.py +49 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytorch-mvgc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A GPU-accelerated Multivariate Granger Causality implementation using PyTorch.
|
|
5
|
+
Home-page: https://github.com/yourusername/pytorch-mvgc
|
|
6
|
+
Author: nju_sit_hubc
|
|
7
|
+
Author-email: 2515094316@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: torch>=1.9.0
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
|
|
18
|
+
# PyTorch-MVGC
|
|
19
|
+
|
|
20
|
+
[](https://badge.fury.io/py/pytorch-mvgc)
|
|
21
|
+
[](https://opensource.org/licenses/MIT)
|
|
22
|
+
|
|
23
|
+
**PyTorch-MVGC** is a GPU-accelerated Python implementation of **Multivariate Granger Causality (MVGC)** analysis.
|
|
24
|
+
|
|
25
|
+
It is designed for high-dimensional time-series data (e.g., EEG, fMRI, MEA) where traditional CPU-based implementations are too slow. By leveraging PyTorch tensors and batched matrix operations, this library achieves significant speedups compared to standard NumPy or Matlab implementations.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **🚀 GPU Acceleration**: Fully implemented in PyTorch, supporting CUDA acceleration out-of-the-box.
|
|
30
|
+
- **⚡ Batch Processing**: Optimized for processing multiple trials or sliding windows simultaneously.
|
|
31
|
+
- **🔧 Numerical Stability**: Implements Ridge regularization and Pseudo-inverse solvers to handle ill-conditioned matrices common in high-channel neural recordings (e.g., 128-channel EEG).
|
|
32
|
+
- **🐍 Python Native**: A seamless replacement for the Matlab MVGC toolbox for Python pipelines.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pytorch-mvgc
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import numpy as np
|
|
44
|
+
import torch
|
|
45
|
+
from pytorch_mvgc import compute_mvgc
|
|
46
|
+
|
|
47
|
+
# 1. Generate sample data (3 channels, 1000 timepoints)
|
|
48
|
+
# System: Node 0 drives Node 1
|
|
49
|
+
n_time = 1000
|
|
50
|
+
X = np.zeros((3, n_time))
|
|
51
|
+
noise = 0.5 * np.random.randn(3, n_time)
|
|
52
|
+
for t in range(1, n_time):
|
|
53
|
+
X[0, t] = 0.8 * X[0, t-1] + noise[0, t]
|
|
54
|
+
X[1, t] = 0.5 * X[1, t-1] + 0.5 * X[0, t-1] + noise[1, t] # 0->1 Causal
|
|
55
|
+
X[2, t] = 0.7 * X[2, t-1] + noise[2, t]
|
|
56
|
+
|
|
57
|
+
# 2. Compute MVGC
|
|
58
|
+
# Returns matrix F where F[i, j] denotes causality j -> i
|
|
59
|
+
F_matrix = compute_mvgc(X, p=1, device='cuda')
|
|
60
|
+
|
|
61
|
+
print("MVGC Matrix:\n", np.round(F_matrix, 4))
|
|
62
|
+
# Expected: F[1, 0] should be significant (> 0)
|
|
63
|
+
```
|
|
64
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# PyTorch-MVGC
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/pytorch-mvgc)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**PyTorch-MVGC** is a GPU-accelerated Python implementation of **Multivariate Granger Causality (MVGC)** analysis.
|
|
7
|
+
|
|
8
|
+
It is designed for high-dimensional time-series data (e.g., EEG, fMRI, MEA) where traditional CPU-based implementations are too slow. By leveraging PyTorch tensors and batched matrix operations, this library achieves significant speedups compared to standard NumPy or Matlab implementations.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **🚀 GPU Acceleration**: Fully implemented in PyTorch, supporting CUDA acceleration out-of-the-box.
|
|
13
|
+
- **⚡ Batch Processing**: Optimized for processing multiple trials or sliding windows simultaneously.
|
|
14
|
+
- **🔧 Numerical Stability**: Implements Ridge regularization and Pseudo-inverse solvers to handle ill-conditioned matrices common in high-channel neural recordings (e.g., 128-channel EEG).
|
|
15
|
+
- **🐍 Python Native**: A seamless replacement for the Matlab MVGC toolbox for Python pipelines.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install pytorch-mvgc
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import numpy as np
|
|
27
|
+
import torch
|
|
28
|
+
from pytorch_mvgc import compute_mvgc
|
|
29
|
+
|
|
30
|
+
# 1. Generate sample data (3 channels, 1000 timepoints)
|
|
31
|
+
# System: Node 0 drives Node 1
|
|
32
|
+
n_time = 1000
|
|
33
|
+
X = np.zeros((3, n_time))
|
|
34
|
+
noise = 0.5 * np.random.randn(3, n_time)
|
|
35
|
+
for t in range(1, n_time):
|
|
36
|
+
X[0, t] = 0.8 * X[0, t-1] + noise[0, t]
|
|
37
|
+
X[1, t] = 0.5 * X[1, t-1] + 0.5 * X[0, t-1] + noise[1, t] # 0->1 Causal
|
|
38
|
+
X[2, t] = 0.7 * X[2, t-1] + noise[2, t]
|
|
39
|
+
|
|
40
|
+
# 2. Compute MVGC
|
|
41
|
+
# Returns matrix F where F[i, j] denotes causality j -> i
|
|
42
|
+
F_matrix = compute_mvgc(X, p=1, device='cuda')
|
|
43
|
+
|
|
44
|
+
print("MVGC Matrix:\n", np.round(F_matrix, 4))
|
|
45
|
+
# Expected: F[1, 0] should be significant (> 0)
|
|
46
|
+
```
|
|
47
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
def check_backend(device: str = 'cuda'):
|
|
6
|
+
if device == 'cuda' and not torch.cuda.is_available():
|
|
7
|
+
warnings.warn("CUDA not available, falling back to CPU.")
|
|
8
|
+
return torch.device('cpu')
|
|
9
|
+
return torch.device(device)
|
|
10
|
+
|
|
11
|
+
def compute_mvgc(X: np.ndarray, p: int, alpha: float = 0.01, device: str = 'cuda') -> np.ndarray:
|
|
12
|
+
"""
|
|
13
|
+
计算 MVGC 矩阵(用户主入口)。
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
X: (n_channels, n_time) 时间序列数据
|
|
17
|
+
p: VAR 滞后阶数 (Lag order)
|
|
18
|
+
alpha: 岭回归正则化系数 (Ridge regularization),解决 NaN 的关键!
|
|
19
|
+
建议值 0.01 ~ 0.1。如果数据很干净,可以用 1e-4。
|
|
20
|
+
device: 'cuda' 或 'cpu'
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
F: (n_channels, n_channels) 格兰杰因果矩阵。F[i, j] 代表 j -> i 的因果强度。
|
|
24
|
+
"""
|
|
25
|
+
X = np.asarray(X)
|
|
26
|
+
|
|
27
|
+
# 维度检查与修正
|
|
28
|
+
if X.ndim != 2:
|
|
29
|
+
raise ValueError(f"Input must be 2D (n_channels, n_time), got {X.shape}")
|
|
30
|
+
|
|
31
|
+
n_channels, n_time = X.shape
|
|
32
|
+
if n_channels > n_time:
|
|
33
|
+
warnings.warn(f"Warning: Channels ({n_channels}) > Time points ({n_time}). Did you transpose the input?")
|
|
34
|
+
|
|
35
|
+
# 视作 Batch = 1 进行处理
|
|
36
|
+
X_batch = X[np.newaxis, :, :] # (1, n, T)
|
|
37
|
+
|
|
38
|
+
F_batch = compute_mvgc_batch(X_batch, p, alpha, device)
|
|
39
|
+
|
|
40
|
+
return F_batch[0] # 返回 (n, n)
|
|
41
|
+
|
|
42
|
+
def compute_mvgc_batch(X_batch: np.ndarray, p: int, alpha: float = 0.01, device: str = 'cuda') -> np.ndarray:
|
|
43
|
+
"""
|
|
44
|
+
批量 MVGC 计算函数。
|
|
45
|
+
"""
|
|
46
|
+
dev = check_backend(device)
|
|
47
|
+
X_t = torch.from_numpy(X_batch).to(dev, dtype=torch.float64) # 使用 float64 提高精度
|
|
48
|
+
|
|
49
|
+
# 调用内部 Torch 内核
|
|
50
|
+
with torch.no_grad():
|
|
51
|
+
F_res = _mvgc_batch_kernel(X_t, p, alpha, dev)
|
|
52
|
+
|
|
53
|
+
return F_res.cpu().numpy()
|
|
54
|
+
|
|
55
|
+
def _mvgc_batch_kernel(segments: torch.Tensor, p: int, alpha: float, dev: torch.device) -> torch.Tensor:
|
|
56
|
+
"""
|
|
57
|
+
PyTorch 核心计算逻辑:向量化 OLS 回归 + 岭回归正则化
|
|
58
|
+
segments: (B, n, T)
|
|
59
|
+
return: (B, n, n)
|
|
60
|
+
"""
|
|
61
|
+
B, n, T = segments.shape
|
|
62
|
+
if T <= p:
|
|
63
|
+
raise ValueError(f'Time length {T} must be > order {p}')
|
|
64
|
+
|
|
65
|
+
# 1. 去均值 (Centering)
|
|
66
|
+
seg_t = segments - segments.mean(dim=2, keepdim=True)
|
|
67
|
+
|
|
68
|
+
# 2. 构建 Y (响应变量) 和 Z (设计矩阵)
|
|
69
|
+
# Y: (B, N, n), N = T-p
|
|
70
|
+
# Z: (B, N, n*p)
|
|
71
|
+
N = T - p
|
|
72
|
+
Y = seg_t[:, :, p:].permute(0, 2, 1)
|
|
73
|
+
|
|
74
|
+
Z_list = []
|
|
75
|
+
for k in range(1, p + 1):
|
|
76
|
+
# 滞后 k 个时间点
|
|
77
|
+
lag = seg_t[:, :, p - k : T - k].permute(0, 2, 1)
|
|
78
|
+
Z_list.append(lag)
|
|
79
|
+
Zs = torch.cat(Z_list, dim=2) # (B, N, n*p)
|
|
80
|
+
|
|
81
|
+
D = n * p # 特征维度
|
|
82
|
+
|
|
83
|
+
# 3. 计算 Full Model (全模型) 的残差方差
|
|
84
|
+
# 求解 Y = Z * Beta + E
|
|
85
|
+
# Beta = (Z^T Z + alpha*I)^-1 Z^T Y
|
|
86
|
+
|
|
87
|
+
ZsT = Zs.permute(0, 2, 1) # (B, D, N)
|
|
88
|
+
ZTZ = torch.bmm(ZsT, Zs) # (B, D, D)
|
|
89
|
+
ZTY = torch.bmm(ZsT, Y) # (B, D, n)
|
|
90
|
+
|
|
91
|
+
# === 关键:正则化 (Ridge) 防止奇异矩阵导致的 NaN ===
|
|
92
|
+
# 添加 alpha 到对角线
|
|
93
|
+
eye = torch.eye(D, device=dev, dtype=ZTZ.dtype).unsqueeze(0).expand(B, D, D)
|
|
94
|
+
ZTZ_reg = ZTZ + alpha * eye
|
|
95
|
+
|
|
96
|
+
# 使用 cholesky 或 solve 求解比求逆更稳定
|
|
97
|
+
# beta_full: (B, D, n)
|
|
98
|
+
try:
|
|
99
|
+
# 尝试更快的 Cholesky
|
|
100
|
+
L = torch.linalg.cholesky(ZTZ_reg)
|
|
101
|
+
beta_full = torch.cholesky_solve(ZTY, L)
|
|
102
|
+
except:
|
|
103
|
+
# 回退到标准求逆 (伪逆)
|
|
104
|
+
beta_full = torch.linalg.solve(ZTZ_reg, ZTY)
|
|
105
|
+
|
|
106
|
+
# 计算 Full Model 残差方差 sigma_full
|
|
107
|
+
resid_full = Y - torch.bmm(Zs, beta_full)
|
|
108
|
+
# 只需要对角线部分的方差 (Generalized Variance 近似为变量方差的乘积,在MVGC中简化为逐变量比较)
|
|
109
|
+
sigma_full = resid_full.pow(2).sum(dim=1) # (B, n)
|
|
110
|
+
|
|
111
|
+
# 4. 计算 Reduced Models (逐个剔除源变量 j)
|
|
112
|
+
# 结果矩阵 F (B, target, source)
|
|
113
|
+
F_batch = torch.zeros((B, n, n), dtype=torch.float64, device=dev)
|
|
114
|
+
|
|
115
|
+
# 预先生成所有列的索引
|
|
116
|
+
all_cols = torch.arange(D, device=dev)
|
|
117
|
+
|
|
118
|
+
for j in range(n): # j 是 Source (因)
|
|
119
|
+
# 剔除 j 的所有滞后项 (columns: j*p, j*p+1, ..., (j+1)*p-1)
|
|
120
|
+
# 保留 Target i 和其他变量 Z
|
|
121
|
+
|
|
122
|
+
# 确定要保留的列索引
|
|
123
|
+
# 这是一个简单的掩码操作
|
|
124
|
+
mask = torch.ones(D, dtype=torch.bool, device=dev)
|
|
125
|
+
mask[j*p : (j+1)*p] = False
|
|
126
|
+
keep_idx = all_cols[mask]
|
|
127
|
+
|
|
128
|
+
# 提取 Reduced 设计矩阵 Z_red
|
|
129
|
+
Zs_red = Zs.index_select(2, keep_idx) # (B, N, D_reduced)
|
|
130
|
+
D_red = Zs_red.shape[2]
|
|
131
|
+
|
|
132
|
+
# OLS for Reduced Model
|
|
133
|
+
Zs_red_T = Zs_red.permute(0, 2, 1)
|
|
134
|
+
ZTZ_r = torch.bmm(Zs_red_T, Zs_red)
|
|
135
|
+
ZTY_r = torch.bmm(Zs_red_T, Y)
|
|
136
|
+
|
|
137
|
+
# Regularization again
|
|
138
|
+
eye_r = torch.eye(D_red, device=dev, dtype=ZTZ_r.dtype).unsqueeze(0).expand(B, D_red, D_red)
|
|
139
|
+
ZTZ_r_reg = ZTZ_r + alpha * eye_r
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
L_r = torch.linalg.cholesky(ZTZ_r_reg)
|
|
143
|
+
beta_r = torch.cholesky_solve(ZTY_r, L_r)
|
|
144
|
+
except:
|
|
145
|
+
beta_r = torch.linalg.solve(ZTZ_r_reg, ZTY_r)
|
|
146
|
+
|
|
147
|
+
resid_r = Y - torch.bmm(Zs_red, beta_r)
|
|
148
|
+
sigma_red = resid_r.pow(2).sum(dim=1) # (B, n) - 每个变量作为 Target 时的 Reduced 方差
|
|
149
|
+
|
|
150
|
+
# 5. 计算 Granger Causality
|
|
151
|
+
# F_{j->i} = ln(var_reduced_i / var_full_i)
|
|
152
|
+
# 注意:这里 sigma_red 和 sigma_full 都是 (B, n),表示 n 个变量作为 Target 时的方差
|
|
153
|
+
# 当前循环 j 是 Source。
|
|
154
|
+
# 所以 ratio[b, i] 代表:Source j 被剔除后,Target i 的方差变化
|
|
155
|
+
|
|
156
|
+
# 加上极小值防止除以0
|
|
157
|
+
ratio = (sigma_red + 1e-12) / (sigma_full + 1e-12)
|
|
158
|
+
|
|
159
|
+
# 结果填入 F 的第 j 列 (j -> all targets)
|
|
160
|
+
F_batch[:, :, j] = torch.log(ratio)
|
|
161
|
+
|
|
162
|
+
# 6. 后处理:对角线置 0 (无自因果)
|
|
163
|
+
idx = torch.arange(n, device=dev)
|
|
164
|
+
F_batch[:, idx, idx] = 0.0
|
|
165
|
+
|
|
166
|
+
# 修正可能的负值 (由于数值误差,完全无关时 log 可能出现微小负值)
|
|
167
|
+
F_batch = torch.relu(F_batch)
|
|
168
|
+
|
|
169
|
+
return F_batch
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
def compute_mvgc_cpu(X: np.ndarray, p: int, alpha: float = 0.01) -> np.ndarray:
|
|
5
|
+
"""
|
|
6
|
+
CPU implementation of MVGC using NumPy (OLS with Ridge Regression).
|
|
7
|
+
Acts as a fallback or reference implementation.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
X: (n_channels, n_time)
|
|
11
|
+
p: Lag order
|
|
12
|
+
alpha: Ridge regularization parameter
|
|
13
|
+
"""
|
|
14
|
+
X = np.asarray(X)
|
|
15
|
+
n, T = X.shape
|
|
16
|
+
if T <= p:
|
|
17
|
+
raise ValueError(f'Time length {T} must be > order {p}')
|
|
18
|
+
|
|
19
|
+
# 1. Prepare Data
|
|
20
|
+
# Center the data
|
|
21
|
+
X_mean = X - X.mean(axis=1, keepdims=True)
|
|
22
|
+
|
|
23
|
+
# Build Y (Target) and Z (Design Matrix)
|
|
24
|
+
# Y: (n, N) where N = T - p
|
|
25
|
+
N = T - p
|
|
26
|
+
Y = X_mean[:, p:]
|
|
27
|
+
|
|
28
|
+
Z_list = []
|
|
29
|
+
for k in range(1, p + 1):
|
|
30
|
+
lag = X_mean[:, p-k : T-k]
|
|
31
|
+
Z_list.append(lag)
|
|
32
|
+
# Stack lags: Z shape (n*p, N)
|
|
33
|
+
Z = np.vstack(Z_list)
|
|
34
|
+
|
|
35
|
+
# 2. Full Model Regression
|
|
36
|
+
# Beta = (Z Z.T + alpha I)^-1 Z Y.T
|
|
37
|
+
# We solve (Z Z.T + alpha I) Beta = Z Y.T
|
|
38
|
+
|
|
39
|
+
ZZT = Z @ Z.T
|
|
40
|
+
ZYT = Z @ Y.T
|
|
41
|
+
|
|
42
|
+
# Ridge Regularization
|
|
43
|
+
D = n * p
|
|
44
|
+
ZZT_reg = ZZT + alpha * np.eye(D)
|
|
45
|
+
|
|
46
|
+
# Solve linear system
|
|
47
|
+
try:
|
|
48
|
+
# utilize symmetry for speed (equivalent to cholesky solve)
|
|
49
|
+
beta_full = np.linalg.solve(ZZT_reg, ZYT)
|
|
50
|
+
except np.linalg.LinAlgError:
|
|
51
|
+
# Fallback to pseudoinverse if somehow still singular
|
|
52
|
+
beta_full = np.linalg.pinv(ZZT_reg) @ ZYT
|
|
53
|
+
|
|
54
|
+
# Residuals
|
|
55
|
+
# Note: Beta shape is (n*p, n), so Z.T @ Beta gives (N, n), transpose to (n, N)
|
|
56
|
+
preds_full = beta_full.T @ Z
|
|
57
|
+
res_full = Y - preds_full
|
|
58
|
+
# Variance of residuals (diagonal of covariance matrix)
|
|
59
|
+
sig_full = np.var(res_full, axis=1, ddof=0)
|
|
60
|
+
|
|
61
|
+
# 3. Reduced Models (Loop over source j)
|
|
62
|
+
F = np.zeros((n, n))
|
|
63
|
+
|
|
64
|
+
# Cache ZZT parts to avoid recomputing?
|
|
65
|
+
# For CPU simple implementation, we just slice Z.
|
|
66
|
+
|
|
67
|
+
for j in range(n):
|
|
68
|
+
# We want to calculate causality j -> i (for all i)
|
|
69
|
+
# So we remove source j from the predictors Z
|
|
70
|
+
|
|
71
|
+
# Identify rows in Z to keep (remove lag rows corresponding to channel j)
|
|
72
|
+
# Z rows: 0..n-1 (lag 1), n..2n-1 (lag 2)...
|
|
73
|
+
keep_indices = []
|
|
74
|
+
for k in range(p):
|
|
75
|
+
# The row index for channel j at lag k+1 is: j + k*n
|
|
76
|
+
# Wait, construction of Z above:
|
|
77
|
+
# Z_list[0] is shape (n, N). It contains ch0_lag1, ch1_lag1, ...
|
|
78
|
+
# So Z structure is: [ch0_l1, ch1_l1, ..., ch0_l2, ...].T
|
|
79
|
+
# Correct index for channel c at lag k (1-based) is: c + (k-1)*n
|
|
80
|
+
|
|
81
|
+
# Let's verify standard flattening.
|
|
82
|
+
# Z = vstack(Z_list). Z_list[0] has n rows.
|
|
83
|
+
# Row x corresponds to channel (x % n)
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Easier way: mask indices where (idx % n) != j
|
|
87
|
+
all_indices = np.arange(D)
|
|
88
|
+
mask = (all_indices % n) != j
|
|
89
|
+
idx_reduced = all_indices[mask]
|
|
90
|
+
|
|
91
|
+
Z_red = Z[idx_reduced, :]
|
|
92
|
+
D_red = len(idx_reduced)
|
|
93
|
+
|
|
94
|
+
# OLS Reduced
|
|
95
|
+
ZZT_r = Z_red @ Z_red.T
|
|
96
|
+
ZYT_r = Z_red @ Y.T
|
|
97
|
+
|
|
98
|
+
ZZT_r_reg = ZZT_r + alpha * np.eye(D_red)
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
beta_r = np.linalg.solve(ZZT_r_reg, ZYT_r)
|
|
102
|
+
except np.linalg.LinAlgError:
|
|
103
|
+
beta_r = np.linalg.pinv(ZZT_r_reg) @ ZYT_r
|
|
104
|
+
|
|
105
|
+
preds_r = beta_r.T @ Z_red
|
|
106
|
+
res_r = Y - preds_r
|
|
107
|
+
sig_r = np.var(res_r, axis=1, ddof=0)
|
|
108
|
+
|
|
109
|
+
# 4. Granger Causality
|
|
110
|
+
# F_{j->i} = ln(sig_r_i / sig_full_i)
|
|
111
|
+
ratio = (sig_r + 1e-12) / (sig_full + 1e-12)
|
|
112
|
+
F[:, j] = np.log(ratio)
|
|
113
|
+
|
|
114
|
+
# Zero diagonal
|
|
115
|
+
np.fill_diagonal(F, 0.0)
|
|
116
|
+
|
|
117
|
+
# Clean up small negative values
|
|
118
|
+
F[F < 0] = 0.0
|
|
119
|
+
|
|
120
|
+
return F
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# fast_mvgc/utils.py
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
def segment_data(X: np.ndarray, window_size: int, step_size: int = None) -> np.ndarray:
|
|
5
|
+
"""
|
|
6
|
+
Generic segmentation function.
|
|
7
|
+
Args:
|
|
8
|
+
X: (n_channels, total_time)
|
|
9
|
+
window_size: Length of each segment
|
|
10
|
+
step_size: Stride (default = window_size, i.e., no overlap)
|
|
11
|
+
Returns:
|
|
12
|
+
segments: (n_segments, n_channels, window_size)
|
|
13
|
+
"""
|
|
14
|
+
if step_size is None:
|
|
15
|
+
step_size = window_size
|
|
16
|
+
|
|
17
|
+
n_channels, n_time = X.shape
|
|
18
|
+
starts = range(0, n_time - window_size + 1, step_size)
|
|
19
|
+
segments = [X[:, s:s+window_size] for s in starts]
|
|
20
|
+
return np.stack(segments, axis=0)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pytorch-mvgc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A GPU-accelerated Multivariate Granger Causality implementation using PyTorch.
|
|
5
|
+
Home-page: https://github.com/yourusername/pytorch-mvgc
|
|
6
|
+
Author: nju_sit_hubc
|
|
7
|
+
Author-email: 2515094316@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: numpy
|
|
15
|
+
Requires-Dist: torch>=1.9.0
|
|
16
|
+
Requires-Dist: tqdm
|
|
17
|
+
|
|
18
|
+
# PyTorch-MVGC
|
|
19
|
+
|
|
20
|
+
[](https://badge.fury.io/py/pytorch-mvgc)
|
|
21
|
+
[](https://opensource.org/licenses/MIT)
|
|
22
|
+
|
|
23
|
+
**PyTorch-MVGC** is a GPU-accelerated Python implementation of **Multivariate Granger Causality (MVGC)** analysis.
|
|
24
|
+
|
|
25
|
+
It is designed for high-dimensional time-series data (e.g., EEG, fMRI, MEA) where traditional CPU-based implementations are too slow. By leveraging PyTorch tensors and batched matrix operations, this library achieves significant speedups compared to standard NumPy or Matlab implementations.
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **🚀 GPU Acceleration**: Fully implemented in PyTorch, supporting CUDA acceleration out-of-the-box.
|
|
30
|
+
- **⚡ Batch Processing**: Optimized for processing multiple trials or sliding windows simultaneously.
|
|
31
|
+
- **🔧 Numerical Stability**: Implements Ridge regularization and Pseudo-inverse solvers to handle ill-conditioned matrices common in high-channel neural recordings (e.g., 128-channel EEG).
|
|
32
|
+
- **🐍 Python Native**: A seamless replacement for the Matlab MVGC toolbox for Python pipelines.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pytorch-mvgc
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import numpy as np
|
|
44
|
+
import torch
|
|
45
|
+
from pytorch_mvgc import compute_mvgc
|
|
46
|
+
|
|
47
|
+
# 1. Generate sample data (3 channels, 1000 timepoints)
|
|
48
|
+
# System: Node 0 drives Node 1
|
|
49
|
+
n_time = 1000
|
|
50
|
+
X = np.zeros((3, n_time))
|
|
51
|
+
noise = 0.5 * np.random.randn(3, n_time)
|
|
52
|
+
for t in range(1, n_time):
|
|
53
|
+
X[0, t] = 0.8 * X[0, t-1] + noise[0, t]
|
|
54
|
+
X[1, t] = 0.5 * X[1, t-1] + 0.5 * X[0, t-1] + noise[1, t] # 0->1 Causal
|
|
55
|
+
X[2, t] = 0.7 * X[2, t-1] + noise[2, t]
|
|
56
|
+
|
|
57
|
+
# 2. Compute MVGC
|
|
58
|
+
# Returns matrix F where F[i, j] denotes causality j -> i
|
|
59
|
+
F_matrix = compute_mvgc(X, p=1, device='cuda')
|
|
60
|
+
|
|
61
|
+
print("MVGC Matrix:\n", np.round(F_matrix, 4))
|
|
62
|
+
# Expected: F[1, 0] should be significant (> 0)
|
|
63
|
+
```
|
|
64
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
fast_mvgc/__init__.py
|
|
5
|
+
fast_mvgc/core.py
|
|
6
|
+
fast_mvgc/cpu_engine.py
|
|
7
|
+
fast_mvgc/utils.py
|
|
8
|
+
pytorch_mvgc.egg-info/PKG-INFO
|
|
9
|
+
pytorch_mvgc.egg-info/SOURCES.txt
|
|
10
|
+
pytorch_mvgc.egg-info/dependency_links.txt
|
|
11
|
+
pytorch_mvgc.egg-info/requires.txt
|
|
12
|
+
pytorch_mvgc.egg-info/top_level.txt
|
|
13
|
+
tests/test.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fast_mvgc
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="pytorch-mvgc",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
author="nju_sit_hubc",
|
|
7
|
+
author_email="2515094316@qq.com",
|
|
8
|
+
description="A GPU-accelerated Multivariate Granger Causality implementation using PyTorch.",
|
|
9
|
+
long_description=open("README.md").read(),
|
|
10
|
+
long_description_content_type="text/markdown",
|
|
11
|
+
url="https://github.com/yourusername/pytorch-mvgc",
|
|
12
|
+
packages=find_packages(),
|
|
13
|
+
classifiers=[
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
],
|
|
18
|
+
python_requires='>=3.6',
|
|
19
|
+
install_requires=[
|
|
20
|
+
"numpy",
|
|
21
|
+
"torch>=1.9.0", # 指定最低版本
|
|
22
|
+
"tqdm"
|
|
23
|
+
],
|
|
24
|
+
)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import torch
|
|
3
|
+
from fast_mvgc.core import compute_mvgc # 确保引用了上面的新文件
|
|
4
|
+
|
|
5
|
+
def test_causality_fix():
|
|
6
|
+
print("=== 开始 MVGC 正确性测试 ===")
|
|
7
|
+
|
|
8
|
+
# 1. 生成标准的 VAR(1) 3通道模型
|
|
9
|
+
# 0 -> 1 (强因果)
|
|
10
|
+
# 2 独立
|
|
11
|
+
np.random.seed(42)
|
|
12
|
+
n_time = 2000
|
|
13
|
+
X = np.zeros((3, n_time))
|
|
14
|
+
noise = np.random.randn(3, n_time) # 标准正态噪声
|
|
15
|
+
|
|
16
|
+
print("生成数据中: Node 0 -> Node 1 ...")
|
|
17
|
+
for t in range(1, n_time):
|
|
18
|
+
X[0, t] = 0.8 * X[0, t-1] + noise[0, t]
|
|
19
|
+
# X[1] 依赖 X[0] 的过去值
|
|
20
|
+
X[1, t] = 0.5 * X[1, t-1] + 0.5 * X[0, t-1] + noise[1, t]
|
|
21
|
+
X[2, t] = 0.7 * X[2, t-1] + noise[2, t]
|
|
22
|
+
|
|
23
|
+
# 2. 计算 MVGC
|
|
24
|
+
# 注意:alpha (正则化) 对于数值稳定性至关重要
|
|
25
|
+
try:
|
|
26
|
+
F = compute_mvgc(X, p=1, alpha=0.01, device='cuda')
|
|
27
|
+
except Exception as e:
|
|
28
|
+
print(f"GPU计算失败,尝试CPU... 错误: {e}")
|
|
29
|
+
F = compute_mvgc(X, p=1, alpha=0.01, device='cpu')
|
|
30
|
+
|
|
31
|
+
print("\n计算结果 F (F[i, j] 表示 j->i):")
|
|
32
|
+
print(np.round(F, 4))
|
|
33
|
+
|
|
34
|
+
# 3. 验证
|
|
35
|
+
# 我们期望 F[1, 0] (0->1) 是显著的
|
|
36
|
+
val_0_to_1 = F[1, 0]
|
|
37
|
+
val_1_to_0 = F[0, 1]
|
|
38
|
+
|
|
39
|
+
print(f"\n关键路径检查:")
|
|
40
|
+
print(f"0 -> 1 (预期 > 0.05): {val_0_to_1:.4f}", "✅ PASS" if val_0_to_1 > 0.05 else "❌ FAIL")
|
|
41
|
+
print(f"1 -> 0 (预期 < 0.05): {val_1_to_0:.4f}", "✅ PASS" if val_1_to_0 < 0.05 else "❌ FAIL (False Positive)")
|
|
42
|
+
|
|
43
|
+
if np.isnan(F).any():
|
|
44
|
+
print("\n❌ 警告: 结果中包含 NaN!")
|
|
45
|
+
else:
|
|
46
|
+
print("\n✅ 结果数值正常 (无 NaN).")
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
test_causality_fix()
|