Skip to content

Commit d0515bf

Browse files
authored
Merge pull request #86 from DynamicsAndNeuralSystems/jmoo2880-ids-spi
Addition of InterDependence score SPI
2 parents efd6206 + 1e4f17b commit d0515bf

File tree

8 files changed

+203
-1
lines changed

8 files changed

+203
-1
lines changed

pyspi/config.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,3 +1391,15 @@
13911391
- orth: True
13921392
log: True
13931393
absolute: True
1394+
1395+
# Interdependence score
1396+
InterDependenceScore:
1397+
labels:
1398+
- unsigned
1399+
- undirected
1400+
- nonlinear
1401+
dependencies:
1402+
configs: # default params
1403+
- terms: 6
1404+
pnorm: 'max'
1405+
bandwidth: 0.5

pyspi/fast_config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,3 +1291,16 @@
12911291
- orth: True
12921292
log: True
12931293
absolute: True
1294+
1295+
# Interdependence score
1296+
InterDependenceScore:
1297+
labels:
1298+
- unsigned
1299+
- undirected
1300+
- nonlinear
1301+
dependencies:
1302+
configs: # default params
1303+
- terms: 6
1304+
pnorm: 'max'
1305+
bandwidth: 0.5
1306+

pyspi/lib/ids/LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Adityanarayanan Radhakrishnan
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.

pyspi/lib/ids/__init__.py

Whitespace-only changes.

pyspi/lib/ids/dependence.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""
2+
Interdependence Score (IDS) computation.
3+
Based on the work by Adityanarayanan Radhakrishnan (MIT License)
4+
Original: https://github.com/aradha/interdependence_scores
5+
Modified for use in pyspi package.
6+
"""
7+
from .numpy_dependence import compute_IDS_numpy
8+
9+
def compute_IDS(X, Y=None, num_terms=6, p_norm='max',
10+
p_val=False, num_tests=100, bandwidth_term=1/2):
11+
"""Compute IDS between all pairs of variables in X (or between X and Y).
12+
13+
This is a modified version of the implementation from:
14+
https://github.com/aradha/interdependence_scores
15+
16+
Original author: Adityanarayanan Radhakrishnan
17+
License: MIT (see LICENSE.txt)
18+
19+
Parameters:
20+
X: np.ndarray or torch.Tensor
21+
Y: np.ndarray or torch.Tensor (optional)
22+
num_terms: Number of terms for Taylor series approximation (optional)
23+
p_norm: String 'max' if using IDS-max. 1 or 2 for IDS-1, IDS-2, respectively. (optional)
24+
p_val: Boolean. Indicates whether to compute p-values using permutation tests
25+
num_tests: Number of permutation tests if p_val=True
26+
bandwidth_term: Constant term in Gaussian kernel
27+
Returns:
28+
IDS matrix, p-value matrix (if p_val=True)
29+
"""
30+
return compute_IDS_numpy(X, Y=Y, num_terms=num_terms, p_norm=p_norm,
31+
p_val=p_val, num_tests=num_tests, bandwidth_term=bandwidth_term)

pyspi/lib/ids/numpy_dependence.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
Interdependence Score (IDS) computation.
3+
Based on the work by Adityanarayanan Radhakrishnan (MIT License)
4+
Original: https://github.com/aradha/interdependence_scores
5+
"""
6+
import numpy as np
7+
import math
8+
import sys
9+
from tqdm import tqdm
10+
11+
SEED = 1717
12+
np.random.seed(SEED)
13+
14+
EPSILON = sys.float_info.epsilon
15+
16+
def transform(y, num_terms=6, bandwidth_term=1/2):
17+
B = bandwidth_term
18+
exp = np.exp(-B * y**2)
19+
terms = []
20+
for i in range(num_terms):
21+
terms.append(exp * (y)**i / math.sqrt(math.factorial(i) *1.))
22+
y_ = np.concatenate(terms, axis=-1)
23+
return y_
24+
25+
def center(X):
26+
return X - np.mean(X, axis=0, keepdims=True)
27+
28+
29+
def compute_p_val(C, X, Y=None, num_terms=6, p_norm='max', n_tests=100, bandwidth_term=1/2):
30+
31+
gt = C
32+
count = 0
33+
34+
n, dx = X.shape
35+
for i in tqdm(range(n_tests)):
36+
37+
# Used to shuffle data
38+
random_noise = np.random.normal(size=(n, dx))
39+
permutations = np.argsort(random_noise, axis=0)
40+
X_permuted = X[permutations, np.arange(dx)[None, :]]
41+
42+
if Y is not None:
43+
n, dy = Y.shape
44+
random_noise = np.random.normal(size=(n, dy))
45+
permutations = np.argsort(random_noise, axis=0)
46+
Y_permuted = Y[permutations, np.arange(dy)[None, :]]
47+
null = compute_IDS_numpy(X_permuted, Y=Y_permuted, num_terms=num_terms,
48+
p_norm=p_norm, bandwidth_term=bandwidth_term)
49+
else:
50+
null = compute_IDS_numpy(X_permuted, Y=Y, num_terms=num_terms,
51+
p_norm=p_norm, bandwidth_term=bandwidth_term)
52+
53+
54+
count += np.where(null > gt, 1, 0)
55+
56+
p_vals = count / n_tests
57+
return p_vals
58+
59+
60+
def compute_IDS_numpy(X, Y=None, num_terms=6, p_norm='max',
61+
p_val=False, num_tests=100, bandwidth_term=1/2):
62+
n, dx = X.shape
63+
X_t = transform(X, num_terms=num_terms, bandwidth_term=bandwidth_term)
64+
X_t = center(X_t)
65+
66+
if Y is not None:
67+
_, dy = Y.shape
68+
Y_t = transform(Y, num_terms=num_terms, bandwidth_term=bandwidth_term)
69+
Y_t = center(Y_t)
70+
cov = X_t.T @ Y_t
71+
X_std = np.sqrt(np.sum(X_t**2, axis=0))
72+
Y_std = np.sqrt(np.sum(Y_t**2, axis=0))
73+
correlations = cov / (X_std.reshape(-1, 1) + EPSILON)
74+
C = correlations / (Y_std.reshape(1, -1) + EPSILON)
75+
C = C.reshape(num_terms, dx, num_terms, dy)
76+
else:
77+
C = np.corrcoef(X_t.T)
78+
C = C.reshape(num_terms, dx, num_terms, dx)
79+
80+
C = np.nan_to_num(C, nan=0, posinf=0, neginf=0)
81+
C = np.abs(C)
82+
83+
if p_norm == 'max':
84+
C = np.amax(C, axis=(0, 2))
85+
elif p_norm == 2:
86+
C = C**2
87+
C = np.mean(C, axis=0)
88+
C = np.mean(C, axis=1)
89+
C = np.sqrt(C)
90+
elif p_norm == 1:
91+
C = np.mean(C, axis=0)
92+
C = np.mean(C, axis=1)
93+
94+
if p_val:
95+
p_vals = compute_p_val(C, X, Y=Y, num_terms=num_terms, p_norm=p_norm,
96+
n_tests=num_tests, bandwidth_term=bandwidth_term)
97+
return C, p_vals
98+
else:
99+
return C

pyspi/statistics/misc.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sklearn.metrics import mean_squared_error
99
from sklearn import linear_model
1010
import mne.connectivity as mnec
11+
from pyspi.lib.ids.dependence import compute_IDS
1112

1213
from pyspi.base import (
1314
Directed,
@@ -147,7 +148,7 @@ def bivariate(self, data, i=None, j=None):
147148

148149

149150
class PowerEnvelopeCorrelation(Undirected, Unsigned):
150-
humanname = "Power envelope correlation"
151+
name = "Power envelope correlation"
151152
identifier = "pec"
152153
labels = ["unsigned", "misc", "undirected"]
153154

@@ -173,3 +174,28 @@ def multivariate(self, data):
173174
)
174175
np.fill_diagonal(adj, np.nan)
175176
return adj
177+
178+
class InterDependenceScore(Undirected, Unsigned):
179+
name = "Interdependence score"
180+
identifier = "ids"
181+
labels = ["unsigned", "misc", "undirected", "nonlinear"]
182+
183+
def __init__(
184+
self,
185+
terms=6,
186+
pnorm='max',
187+
bandwidth=0.5
188+
):
189+
self._num_terms = terms
190+
self._p_norm = pnorm
191+
self._bandwidth_term = bandwidth
192+
193+
194+
@parse_multivariate
195+
def multivariate(self, data):
196+
# reshape for the compute_IDS function which expects shape (obs, proc)
197+
z = np.squeeze(data.to_numpy(), axis=2).T
198+
ids = compute_IDS(z, num_terms=self._num_terms, p_norm=self._p_norm,
199+
bandwidth_term=self._bandwidth_term)
200+
return ids
201+

tests/CML7_benchmark_tables.pkl

878 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)