pygranger 2.0.0__py3-none-any.whl
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.
- pygc/__init__.py +24 -0
- pygc/_jax_backend.py +200 -0
- pygc/ar_model.py +81 -0
- pygc/granger.py +493 -0
- pygc/misc/__init__.py +1 -0
- pygc/misc/smooth_spectra.py +15 -0
- pygc/non_parametric.py +107 -0
- pygc/output.py +115 -0
- pygc/parametric.py +130 -0
- pygc/spectral_analysis/__init__.py +24 -0
- pygc/spectral_analysis/fourier.py +79 -0
- pygc/spectral_analysis/time_frequency.py +196 -0
- pygranger-2.0.0.dist-info/METADATA +239 -0
- pygranger-2.0.0.dist-info/RECORD +17 -0
- pygranger-2.0.0.dist-info/WHEEL +5 -0
- pygranger-2.0.0.dist-info/licenses/LICENSE +21 -0
- pygranger-2.0.0.dist-info/top_level.txt +1 -0
pygc/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .parametric import YuleWalker, YuleWalker_multitrial, compute_transfer_function
|
|
2
|
+
from .non_parametric import wilson_factorization
|
|
3
|
+
from .granger import granger_causality, spectral_granger_causality, conditional_granger_causality, spectral_conditional_granger_causality #noqa
|
|
4
|
+
from ._jax_backend import JAX_AVAILABLE, JAX_FLOAT64, wilson_factorization_jax
|
|
5
|
+
from .output import build_granger_dataset, build_conditional_gc_dataset, build_conditional_spec_gc_dataset
|
|
6
|
+
|
|
7
|
+
__version__ = "2.0.0"
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"YuleWalker",
|
|
11
|
+
"YuleWalker_multitrial",
|
|
12
|
+
"compute_transfer_function",
|
|
13
|
+
"wilson_factorization",
|
|
14
|
+
"wilson_factorization_jax",
|
|
15
|
+
"JAX_AVAILABLE",
|
|
16
|
+
"JAX_FLOAT64",
|
|
17
|
+
"granger_causality",
|
|
18
|
+
"spectral_granger_causality",
|
|
19
|
+
"conditional_granger_causality",
|
|
20
|
+
"spectral_conditional_granger_causality",
|
|
21
|
+
"build_granger_dataset",
|
|
22
|
+
"build_conditional_gc_dataset",
|
|
23
|
+
"build_conditional_spec_gc_dataset"
|
|
24
|
+
]
|
pygc/_jax_backend.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Optional JAX backend for pygc.
|
|
3
|
+
|
|
4
|
+
Provides a JIT-compiled Wilson spectral factorization that uses XLA kernels
|
|
5
|
+
(CPU or GPU) for batched matrix operations. All loops — including the
|
|
6
|
+
convergence-checked iteration — are compiled with ``jax.lax.while_loop``
|
|
7
|
+
so no Python overhead remains after the first (tracing) call.
|
|
8
|
+
|
|
9
|
+
GPU support
|
|
10
|
+
-----------
|
|
11
|
+
* **Apple Silicon (Metal)**: ``pip install jax-metal``
|
|
12
|
+
Metal does not support float64, so the computation runs in complex64
|
|
13
|
+
(float32 precision). Accuracy is lower but throughput is higher.
|
|
14
|
+
* **NVIDIA (CUDA)**: install the appropriate ``jax[cuda]`` wheel.
|
|
15
|
+
Full float64 / complex128 precision is available.
|
|
16
|
+
|
|
17
|
+
Usage
|
|
18
|
+
-----
|
|
19
|
+
from pygc._jax_backend import wilson_factorization_jax, JAX_AVAILABLE, JAX_FLOAT64
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import functools
|
|
24
|
+
from typing import Any
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
# Pre-bind to None so the names are always defined; typed as Any so that
|
|
28
|
+
# attribute access inside `if JAX_AVAILABLE:` blocks is not flagged.
|
|
29
|
+
jax: Any = None
|
|
30
|
+
jnp: Any = None
|
|
31
|
+
lax: Any = None
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import jax
|
|
35
|
+
jax.config.update("jax_enable_x64", True) # no-op on Metal, active on CPU/CUDA
|
|
36
|
+
import jax.numpy as jnp
|
|
37
|
+
from jax import lax
|
|
38
|
+
JAX_AVAILABLE: bool = True
|
|
39
|
+
|
|
40
|
+
# Metal GPU supports only float32; CPU and CUDA support float64.
|
|
41
|
+
# Metal raises ValueError on float64 ops instead of silently downcasting.
|
|
42
|
+
try:
|
|
43
|
+
_probe = jnp.ones(1, dtype=jnp.float64)
|
|
44
|
+
JAX_FLOAT64: bool = bool(_probe.dtype == jnp.float64)
|
|
45
|
+
del _probe
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
JAX_FLOAT64 = False
|
|
48
|
+
|
|
49
|
+
# Choose working dtypes based on device capability.
|
|
50
|
+
_NP_CDTYPE = np.complex128 if JAX_FLOAT64 else np.complex64
|
|
51
|
+
_JNP_CDTYPE = jnp.complex128 if JAX_FLOAT64 else jnp.complex64
|
|
52
|
+
|
|
53
|
+
except ImportError: # pragma: no cover
|
|
54
|
+
JAX_AVAILABLE = False
|
|
55
|
+
JAX_FLOAT64 = False
|
|
56
|
+
_NP_CDTYPE = None
|
|
57
|
+
_JNP_CDTYPE = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Internal helpers (only defined when JAX is present)
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
if JAX_AVAILABLE:
|
|
65
|
+
|
|
66
|
+
def _plus_op(g: Any, freq_len: int) -> Any:
|
|
67
|
+
"""Plus-operator: batch IFFT → zero causal half → batch FFT."""
|
|
68
|
+
gam = jnp.fft.ifft(g, axis=2)
|
|
69
|
+
gamp = (gam
|
|
70
|
+
.at[:, :, 0].set(jnp.triu(0.5 * gam[:, :, 0]))
|
|
71
|
+
.at[:, :, freq_len:].set(0.0 + 0.0j))
|
|
72
|
+
return jnp.fft.fft(gamp, axis=2)
|
|
73
|
+
|
|
74
|
+
@functools.partial(jax.jit, static_argnames=["freq_len", "Niterations"])
|
|
75
|
+
def _wilson_loop(
|
|
76
|
+
psi: Any,
|
|
77
|
+
Sarr: Any,
|
|
78
|
+
I: Any,
|
|
79
|
+
freq_len: int,
|
|
80
|
+
Niterations: int,
|
|
81
|
+
tol: float,
|
|
82
|
+
) -> Any:
|
|
83
|
+
"""Full Wilson iteration loop compiled to XLA via lax.while_loop."""
|
|
84
|
+
|
|
85
|
+
def body(carry: tuple) -> tuple:
|
|
86
|
+
psi, _, itr = carry
|
|
87
|
+
psi_T = psi.transpose(2, 0, 1) # (2N, m, m)
|
|
88
|
+
psi_inv = jnp.linalg.inv(psi_T)
|
|
89
|
+
g_T = psi_inv @ Sarr.transpose(2, 0, 1) @ jnp.conj(psi_inv).swapaxes(-1, -2) + I
|
|
90
|
+
g = g_T.transpose(1, 2, 0)
|
|
91
|
+
|
|
92
|
+
gp = _plus_op(g, freq_len)
|
|
93
|
+
|
|
94
|
+
new_psi_T = psi_T @ gp.transpose(2, 0, 1)
|
|
95
|
+
diff = new_psi_T - psi_T
|
|
96
|
+
psierr = jnp.abs(diff).sum(axis=-2).max(axis=-1).mean()
|
|
97
|
+
|
|
98
|
+
return new_psi_T.transpose(1, 2, 0), psierr, itr + 1
|
|
99
|
+
|
|
100
|
+
def cond(carry: tuple) -> Any:
|
|
101
|
+
_, psierr, itr = carry
|
|
102
|
+
return (psierr >= tol) & (itr < Niterations)
|
|
103
|
+
|
|
104
|
+
init = (psi, jnp.array(jnp.inf), jnp.int32(0))
|
|
105
|
+
psi_final, _, _ = lax.while_loop(cond, body, init)
|
|
106
|
+
return psi_final
|
|
107
|
+
|
|
108
|
+
def wilson_factorization_jax(
|
|
109
|
+
S: np.ndarray,
|
|
110
|
+
freq: np.ndarray,
|
|
111
|
+
fs: float,
|
|
112
|
+
Niterations: int = 100,
|
|
113
|
+
tol: float = 1e-12,
|
|
114
|
+
verbose: bool = False,
|
|
115
|
+
ensure_stability: bool = True,
|
|
116
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
117
|
+
"""Wilson spectral factorization using a JAX JIT-compiled loop.
|
|
118
|
+
|
|
119
|
+
The first call incurs XLA compilation time (~1–5 s); subsequent calls
|
|
120
|
+
with the same array shapes reuse the compiled kernel.
|
|
121
|
+
|
|
122
|
+
Precision is chosen automatically:
|
|
123
|
+
- **complex128** on CPU and NVIDIA GPU (float64 available).
|
|
124
|
+
- **complex64** on Apple Metal GPU (float64 not supported).
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
S : ndarray (m, m, N+1) — cross-spectral matrix.
|
|
129
|
+
freq : ndarray (N+1,) — frequency axis.
|
|
130
|
+
fs : float — sampling rate (unused; kept for API compat).
|
|
131
|
+
Niterations : int.
|
|
132
|
+
tol : float.
|
|
133
|
+
verbose : bool — print JAX device / precision info.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
Snew, Hnew : ndarray (m, m, N+1)
|
|
138
|
+
Znew : ndarray (m, m)
|
|
139
|
+
"""
|
|
140
|
+
m = S.shape[0]
|
|
141
|
+
N = freq.shape[0] - 1
|
|
142
|
+
freq_len = len(freq)
|
|
143
|
+
|
|
144
|
+
if verbose:
|
|
145
|
+
prec = "complex128 (float64)" if JAX_FLOAT64 else "complex64 (float32, Metal GPU)"
|
|
146
|
+
print(f'[JAX] devices: {jax.devices()} precision: {prec}')
|
|
147
|
+
|
|
148
|
+
# Build Hermitian-extended Sarr (numpy; done once outside JIT)
|
|
149
|
+
Sarr_np = np.zeros([m, m, 2 * N], dtype=_NP_CDTYPE)
|
|
150
|
+
Sarr_np[:, :, :N + 1] = S
|
|
151
|
+
for k in range(1, N):
|
|
152
|
+
Sarr_np[:, :, 2 * N - k] = S[:, :, k].T
|
|
153
|
+
|
|
154
|
+
# Diagonal regularization fix
|
|
155
|
+
if ensure_stability:
|
|
156
|
+
eps = np.finfo(float).eps * np.abs(Sarr_np).max() * 100
|
|
157
|
+
for i in range(m):
|
|
158
|
+
Sarr_np[i, i, :] += eps
|
|
159
|
+
|
|
160
|
+
# Initialise psi from Cholesky of gam0.
|
|
161
|
+
# A small relative regularisation guards against float32 rounding that
|
|
162
|
+
# can make gam0 appear numerically indefinite on Metal GPU.
|
|
163
|
+
gam0 = np.fft.ifft(Sarr_np, axis=2).real[:, :, 0]
|
|
164
|
+
eps = np.finfo(gam0.dtype).eps * np.abs(np.diag(gam0)).max() * 10
|
|
165
|
+
gam0 += np.eye(m) * eps
|
|
166
|
+
h = np.linalg.cholesky(gam0).T
|
|
167
|
+
psi_np = np.tile(h[:, :, np.newaxis], (1, 1, 2 * N)).astype(_NP_CDTYPE)
|
|
168
|
+
|
|
169
|
+
# Move to JAX arrays and run compiled loop
|
|
170
|
+
psi = jnp.array(psi_np)
|
|
171
|
+
Sarr = jnp.array(Sarr_np)
|
|
172
|
+
I_jax = jnp.eye(m, dtype=_JNP_CDTYPE)[jnp.newaxis] # (1, m, m)
|
|
173
|
+
|
|
174
|
+
psi = _wilson_loop(psi, Sarr, I_jax, freq_len, Niterations, tol)
|
|
175
|
+
psi = np.array(psi) # back to NumPy for output
|
|
176
|
+
|
|
177
|
+
# Compute outputs (NumPy; trivially fast)
|
|
178
|
+
psi_T = psi.transpose(2, 0, 1)
|
|
179
|
+
Snew = (psi_T[:N + 1] @ np.conj(psi_T[:N + 1]).swapaxes(-1, -2)).transpose(1, 2, 0)
|
|
180
|
+
gamtmp = np.fft.ifft(psi, axis=2).real
|
|
181
|
+
A0 = gamtmp[:, :, 0]
|
|
182
|
+
A0inv = np.linalg.inv(A0)
|
|
183
|
+
Znew = (A0 @ A0.T).real
|
|
184
|
+
Hnew = (psi_T[:N + 1] @ A0inv[np.newaxis]).transpose(1, 2, 0)
|
|
185
|
+
|
|
186
|
+
return Snew, Hnew, Znew
|
|
187
|
+
|
|
188
|
+
else: # JAX not available — stub that raises a helpful error # pragma: no cover
|
|
189
|
+
|
|
190
|
+
def wilson_factorization_jax( # type: ignore[misc]
|
|
191
|
+
S: np.ndarray,
|
|
192
|
+
freq: np.ndarray,
|
|
193
|
+
fs: float,
|
|
194
|
+
Niterations: int = 100,
|
|
195
|
+
tol: float = 1e-12,
|
|
196
|
+
verbose: bool = False,
|
|
197
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
198
|
+
raise ImportError(
|
|
199
|
+
"JAX is not installed. Install it with: pip install jax"
|
|
200
|
+
)
|
pygc/ar_model.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def ar_model_dhamala(N=5000, Trials=10, Fs=200, C=0.2, t_start=0, t_stop=None, cov=None):
|
|
5
|
+
"""
|
|
6
|
+
Bivariate AR model from Dhamala et al. (2008).
|
|
7
|
+
|
|
8
|
+
Y drives X with coupling strength C in the interval [t_start, t_stop].
|
|
9
|
+
GC(Y->X) > 0, GC(X->Y) ≈ 0.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
N : int — number of time samples per trial.
|
|
14
|
+
Trials : int — number of independent trials.
|
|
15
|
+
Fs : float — sampling rate (Hz).
|
|
16
|
+
C : float — coupling coefficient from Y to X.
|
|
17
|
+
t_start, t_stop : float — time interval (seconds) during which coupling is active.
|
|
18
|
+
cov : ndarray, shape (2,2) — noise covariance matrix.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
Z : ndarray, shape (2, Trials, N)
|
|
23
|
+
"""
|
|
24
|
+
if cov is None:
|
|
25
|
+
cov = np.eye(2)
|
|
26
|
+
|
|
27
|
+
T = N / Fs
|
|
28
|
+
time = np.linspace(0, T, N)
|
|
29
|
+
|
|
30
|
+
X = np.random.random([Trials, N])
|
|
31
|
+
Y = np.random.random([Trials, N])
|
|
32
|
+
|
|
33
|
+
def _active(t):
|
|
34
|
+
if t_stop is None:
|
|
35
|
+
return t >= t_start
|
|
36
|
+
return (t >= t_start) & (t <= t_stop)
|
|
37
|
+
|
|
38
|
+
for i in range(Trials):
|
|
39
|
+
E = np.random.multivariate_normal(np.zeros(2), cov, size=(N,))
|
|
40
|
+
for t in range(2, N):
|
|
41
|
+
X[i, t] = (0.55 * X[i, t - 1] - 0.8 * X[i, t - 2]
|
|
42
|
+
+ _active(time[t]) * C * Y[i, t - 1] + E[t, 0])
|
|
43
|
+
Y[i, t] = 0.55 * Y[i, t - 1] - 0.8 * Y[i, t - 2] + E[t, 1]
|
|
44
|
+
|
|
45
|
+
Z = np.zeros([2, Trials, N])
|
|
46
|
+
Z[0] = X
|
|
47
|
+
Z[1] = Y
|
|
48
|
+
return Z
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ar_model_baccala(nvars=5, N=1000, ntrials=1):
|
|
52
|
+
"""
|
|
53
|
+
Five-variable AR model from Baccalá & Sameshima (2001).
|
|
54
|
+
|
|
55
|
+
Connectivity: X1 → X2, X1 → X3, X1 → X4, X1 → X5 (via X4).
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
nvars : int — number of variables (must be 5).
|
|
60
|
+
N : int — number of time samples per trial.
|
|
61
|
+
ntrials : int — number of trials.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
Y : ndarray, shape (nvars, N, ntrials)
|
|
66
|
+
"""
|
|
67
|
+
Y = np.random.uniform(size=(nvars, N, ntrials))
|
|
68
|
+
w = np.random.normal(0, 1, size=(nvars, N, ntrials))
|
|
69
|
+
|
|
70
|
+
for i in range(ntrials):
|
|
71
|
+
for t in range(5, N):
|
|
72
|
+
Y[0, t, i] = (0.95 * np.sqrt(2.0) * Y[0, t - 1, i]
|
|
73
|
+
- 0.9025 * Y[0, t - 2, i] + w[0, t, i])
|
|
74
|
+
Y[1, t, i] = 0.5 * Y[0, t - 2, i] + w[1, t, i]
|
|
75
|
+
Y[2, t, i] = -0.4 * Y[0, t - 3, i] + w[2, t, i]
|
|
76
|
+
Y[3, t, i] = (-0.5 * Y[0, t - 2, i]
|
|
77
|
+
+ 0.25 * np.sqrt(2.0) * Y[3, t - 1, i]
|
|
78
|
+
+ 0.25 * np.sqrt(2.0) * Y[4, t - 1, i] + w[3, t, i])
|
|
79
|
+
Y[4, t, i] = (-0.25 * np.sqrt(2.0) * Y[3, t - 1, i]
|
|
80
|
+
+ 0.25 * np.sqrt(2.0) * Y[4, t - 1, i] + w[4, t, i])
|
|
81
|
+
return Y
|