surd 1.1.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.
- surd/__init__.py +77 -0
- surd/analytic_eqs.py +39 -0
- surd/core.py +658 -0
- surd/it_tools.py +166 -0
- surd/transport_map.py +86 -0
- surd-1.1.0.dist-info/METADATA +168 -0
- surd-1.1.0.dist-info/RECORD +9 -0
- surd-1.1.0.dist-info/WHEEL +4 -0
- surd-1.1.0.dist-info/licenses/LICENSE +21 -0
surd/__init__.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""SURD: Synergistic-Unique-Redundant Decomposition of causality.
|
|
2
|
+
|
|
3
|
+
SURD measures the information that past observations of a set of source
|
|
4
|
+
variables carry about the future of a target variable and splits it into
|
|
5
|
+
redundant, unique and synergistic contributions. The remaining uncertainty
|
|
6
|
+
is the causality leak from unobserved variables.
|
|
7
|
+
|
|
8
|
+
Reference: Martínez-Sánchez, Arranz & Lozano-Durán, "Decomposing causality
|
|
9
|
+
into its synergistic, unique, and redundant components", Nature
|
|
10
|
+
Communications 15, 9296 (2024). https://doi.org/10.1038/s41467-024-53373-4
|
|
11
|
+
|
|
12
|
+
Typical use::
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import surd
|
|
16
|
+
|
|
17
|
+
hist, _ = np.histogramdd(Y.T, bins) # Y[0] target, Y[1:] sources
|
|
18
|
+
I_R, I_S, MI, info_leak = surd.surd(hist)
|
|
19
|
+
surd.nice_print(I_R, I_S, MI, info_leak)
|
|
20
|
+
|
|
21
|
+
Submodules:
|
|
22
|
+
|
|
23
|
+
- ``surd.it_tools``: entropies, mutual information and transfer entropy
|
|
24
|
+
on discrete distributions.
|
|
25
|
+
- ``surd.analytic_eqs``: the stochastic benchmark systems of the paper
|
|
26
|
+
(mediator, confounder, synergistic and redundant colliders).
|
|
27
|
+
- ``surd.transport_map``: density estimation with triangular transport
|
|
28
|
+
maps. Imported on first access and needs the optional ``mpart`` package.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import importlib
|
|
32
|
+
|
|
33
|
+
from . import it_tools
|
|
34
|
+
from .core import (
|
|
35
|
+
nice_print,
|
|
36
|
+
plot,
|
|
37
|
+
plot_multiple_lags,
|
|
38
|
+
plot_nlabels,
|
|
39
|
+
run,
|
|
40
|
+
run_multiple_lags,
|
|
41
|
+
run_parallel,
|
|
42
|
+
surd,
|
|
43
|
+
surd_hd,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__version__ = "1.1.0"
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"surd",
|
|
50
|
+
"surd_hd",
|
|
51
|
+
"run",
|
|
52
|
+
"run_parallel",
|
|
53
|
+
"run_multiple_lags",
|
|
54
|
+
"plot",
|
|
55
|
+
"plot_nlabels",
|
|
56
|
+
"plot_multiple_lags",
|
|
57
|
+
"nice_print",
|
|
58
|
+
"it_tools",
|
|
59
|
+
"analytic_eqs",
|
|
60
|
+
"transport_map",
|
|
61
|
+
"__version__",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Submodules loaded on first access. ``analytic_eqs`` seeds NumPy's global
|
|
65
|
+
# random generator when imported, and ``transport_map`` needs ``mpart``,
|
|
66
|
+
# so neither is imported by a plain ``import surd``.
|
|
67
|
+
_LAZY_SUBMODULES = ("analytic_eqs", "transport_map")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __getattr__(name):
|
|
71
|
+
if name in _LAZY_SUBMODULES:
|
|
72
|
+
return importlib.import_module(f"{__name__}.{name}")
|
|
73
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def __dir__():
|
|
77
|
+
return sorted(set(globals()) | set(_LAZY_SUBMODULES))
|
surd/analytic_eqs.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
np.random.seed(10)
|
|
3
|
+
|
|
4
|
+
def mediator(N):
|
|
5
|
+
q1, q2, q3 = np.zeros(N), np.zeros(N), np.zeros(N)
|
|
6
|
+
W1, W2, W3 = np.random.normal(0, 1, N), np.random.normal(0, 1, N), np.random.normal(0, 1, N)
|
|
7
|
+
for n in range(N-1):
|
|
8
|
+
q1[n+1] = np.sin(q2[n]) + 0.001*W1[n]
|
|
9
|
+
q2[n+1] = np.cos(q3[n]) + 0.01*W2[n]
|
|
10
|
+
q3[n+1] = 0.5*q3[n] + 0.1*W3[n]
|
|
11
|
+
return q1, q2, q3
|
|
12
|
+
|
|
13
|
+
def confounder(N):
|
|
14
|
+
q1, q2, q3 = np.zeros(N), np.zeros(N), np.zeros(N)
|
|
15
|
+
W1, W2, W3 = np.random.normal(0, 1, N), np.random.normal(0, 1, N), np.random.normal(0, 1, N)
|
|
16
|
+
for n in range(N-1):
|
|
17
|
+
q1[n+1] = np.sin(q1[n] + q3[n]) + 0.01*W1[n]
|
|
18
|
+
q2[n+1] = np.cos(q2[n] - q3[n]) + 0.01*W2[n]
|
|
19
|
+
q3[n+1] = 0.5*q3[n] + 0.1*W3[n]
|
|
20
|
+
return q1, q2, q3
|
|
21
|
+
|
|
22
|
+
def synergistic_collider(N):
|
|
23
|
+
q1, q2, q3 = np.zeros(N), np.zeros(N), np.zeros(N)
|
|
24
|
+
W1, W2, W3 = np.random.normal(0, 1, N), np.random.normal(0, 1, N), np.random.normal(0, 1, N)
|
|
25
|
+
for n in range(N-1):
|
|
26
|
+
q1[n+1] = np.sin(q2[n] * q3[n]) + 0.001*W1[n]
|
|
27
|
+
q2[n+1] = 0.5*q2[n] + 0.1*W2[n]
|
|
28
|
+
q3[n+1] = 0.5*q3[n] + 0.1*W3[n]
|
|
29
|
+
return q1, q2, q3
|
|
30
|
+
|
|
31
|
+
def redundant_collider(N):
|
|
32
|
+
q1, q2, q3 = np.zeros(N), np.zeros(N), np.zeros(N)
|
|
33
|
+
W1, W2, W3 = np.random.normal(0, 1, N), np.random.normal(0, 1, N), np.random.normal(0, 1, N)
|
|
34
|
+
for n in range(N-1):
|
|
35
|
+
# q1[n+1] = 0.3*q1[n] + 0.7*(np.sin(q2[n]*q3[n]) + 0.1*W1[n])
|
|
36
|
+
q1[n+1] = 0.3*q1[n] + (np.sin(q2[n]*q3[n]) + 0.001*W1[n])
|
|
37
|
+
q2[n+1] = 0.5*q2[n] + 0.1*W2[n]
|
|
38
|
+
q3[n+1] = q2[n+1]
|
|
39
|
+
return q1, q2, q3
|
surd/core.py
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
import matplotlib.colors as mcolors
|
|
4
|
+
import itertools
|
|
5
|
+
from itertools import combinations as icmb
|
|
6
|
+
from itertools import chain as ichain
|
|
7
|
+
from typing import Tuple, Dict
|
|
8
|
+
from . import it_tools as it
|
|
9
|
+
import warnings
|
|
10
|
+
try:
|
|
11
|
+
import pymp # optional: parallel loop in run_parallel (Linux and macOS)
|
|
12
|
+
except ImportError:
|
|
13
|
+
pymp = None
|
|
14
|
+
# Suppress all UserWarnings
|
|
15
|
+
warnings.filterwarnings("ignore", category=UserWarning)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def surd(p: np.ndarray) -> Tuple[Dict, Dict, Dict, float]:
|
|
19
|
+
'''
|
|
20
|
+
Decompose the mutual information between a target variable and a set
|
|
21
|
+
of agent variables into three terms: Redundancy (I_R), Synergy (I_S),
|
|
22
|
+
and Unique (I_U) information.
|
|
23
|
+
|
|
24
|
+
The surd function is designed to compute a decomposition of
|
|
25
|
+
the mutual information between a target variable T (signal in the future)
|
|
26
|
+
and agent variables A (signals in the present). This decomposition results
|
|
27
|
+
in terms related to redundancy (overlapping information), synergy
|
|
28
|
+
(information that arises only when considering multiple variables together),
|
|
29
|
+
and unique information.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
- p (np.ndarray): A multi-dimensional array of the histogram, where the first dimension
|
|
33
|
+
represents the target variable, and subsequent dimensions represent agent variables.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
- I_R (dict): Redundancies and unique information for each variable combination.
|
|
37
|
+
- I_S (dict): Synergies for each variable combination.
|
|
38
|
+
- MI (dict): Mutual information for each variable combination.
|
|
39
|
+
- info_leak (float): Estimation of the information leak
|
|
40
|
+
|
|
41
|
+
Example: To understand the mutual information between target variable T and
|
|
42
|
+
a combination of agent variables A1, A2, and A3, you can use:
|
|
43
|
+
I_R, I_S, MI, info_leak = surd(p)
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
# Ensure no zero values in the probability distribution to avoid NaNs during log computations
|
|
47
|
+
p += 1e-14
|
|
48
|
+
# Normalize the distribution
|
|
49
|
+
p /= p.sum()
|
|
50
|
+
|
|
51
|
+
# Total number of dimensions (target + agents)
|
|
52
|
+
Ntot = p.ndim
|
|
53
|
+
# Number of agent variables
|
|
54
|
+
Nvars = Ntot - 1
|
|
55
|
+
# Number of states for the target variable
|
|
56
|
+
Nt = p.shape[0]
|
|
57
|
+
inds = range(1, Ntot)
|
|
58
|
+
|
|
59
|
+
# Calculation of information leak
|
|
60
|
+
H = it.entropy_nvars(p, (0,) )
|
|
61
|
+
Hc = it.cond_entropy(p, (0,), range(1,Ntot) )
|
|
62
|
+
info_leak = Hc/H
|
|
63
|
+
|
|
64
|
+
# Compute the marginal distribution of the target variable
|
|
65
|
+
p_s = p.sum(axis=(*inds,), keepdims=True)
|
|
66
|
+
|
|
67
|
+
# Prepare for specific mutual information computation
|
|
68
|
+
combs, Is = [], {}
|
|
69
|
+
|
|
70
|
+
# Iterate over all combinations of agent variables
|
|
71
|
+
for i in inds:
|
|
72
|
+
for j in list(icmb(inds, i)):
|
|
73
|
+
combs.append(j)
|
|
74
|
+
noj = tuple(set(inds) - set(j))
|
|
75
|
+
|
|
76
|
+
# Compute joint and conditional distributions for current combinations
|
|
77
|
+
p_a = p.sum(axis=(0, *noj), keepdims=True)
|
|
78
|
+
p_as = p.sum(axis=noj, keepdims=True)
|
|
79
|
+
|
|
80
|
+
p_a_s = p_as / p_s
|
|
81
|
+
p_s_a = p_as / p_a
|
|
82
|
+
|
|
83
|
+
# Compute specific mutual information
|
|
84
|
+
Is[j] = (p_a_s * (it.mylog(p_s_a) - it.mylog(p_s))).sum(axis=j).ravel()
|
|
85
|
+
|
|
86
|
+
# Compute mutual information for each combination of agent variables
|
|
87
|
+
MI = {k: (Is[k] * p_s.squeeze()).sum() for k in Is.keys()}
|
|
88
|
+
|
|
89
|
+
# Initialize redundancy and synergy terms
|
|
90
|
+
I_R = {cc: 0 for cc in combs}
|
|
91
|
+
I_S = {cc: 0 for cc in combs[Nvars:]}
|
|
92
|
+
|
|
93
|
+
# Process each value of the target variable
|
|
94
|
+
for t in range(Nt):
|
|
95
|
+
# Extract specific mutual information for the current target value
|
|
96
|
+
I1 = np.array([ii[t] for ii in Is.values()])
|
|
97
|
+
|
|
98
|
+
# Sorting specific mutual information
|
|
99
|
+
i1 = np.argsort(I1)
|
|
100
|
+
lab = [combs[i_] for i_ in i1]
|
|
101
|
+
lens = np.array([len(l) for l in lab])
|
|
102
|
+
|
|
103
|
+
# Update specific mutual information based on existing maximum values
|
|
104
|
+
I1 = I1[i1]
|
|
105
|
+
for l in range(1, lens.max()):
|
|
106
|
+
inds_l2 = np.where(lens == l+1)[0]
|
|
107
|
+
Il1max = I1[lens==l].max()
|
|
108
|
+
inds_ = inds_l2[I1[inds_l2] < Il1max]
|
|
109
|
+
I1[inds_] = 0
|
|
110
|
+
|
|
111
|
+
# Recompute sorting of updated specific mutual information values
|
|
112
|
+
i1 = np.argsort(I1)
|
|
113
|
+
lab = [lab[i_] for i_ in i1]
|
|
114
|
+
|
|
115
|
+
# Compute differences in sorted specific mutual information values
|
|
116
|
+
Di = np.diff(I1[i1], prepend=0.)
|
|
117
|
+
red_vars = list(inds)
|
|
118
|
+
|
|
119
|
+
# Distribute mutual information to redundancy and synergy terms
|
|
120
|
+
for i_, ll in enumerate(lab):
|
|
121
|
+
info = Di[i_] * p_s.squeeze()[t]
|
|
122
|
+
if len(ll) == 1:
|
|
123
|
+
I_R[tuple(red_vars)] += info
|
|
124
|
+
red_vars.remove(ll[0])
|
|
125
|
+
else:
|
|
126
|
+
I_S[ll] += info
|
|
127
|
+
|
|
128
|
+
return I_R, I_S, MI, info_leak
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def surd_hd(Y: np.ndarray, nbins, max_combs) -> Tuple[Dict, Dict, Dict]:
|
|
132
|
+
'''
|
|
133
|
+
Extension of surd to high-dimensional systems. It computes the
|
|
134
|
+
the decomposition of information up to a given number of maximum combination
|
|
135
|
+
between variables.
|
|
136
|
+
|
|
137
|
+
Parameters:
|
|
138
|
+
- Y (np.ndarray): A multi-dimensional array with the temporal evolution of the variables.
|
|
139
|
+
The first dimension represents the target variable, and subsequent dimensions represent
|
|
140
|
+
agent variables.
|
|
141
|
+
- nbins: Number of bins to discretize the histogram.
|
|
142
|
+
- max_combs: maximum order of combitations for synergistic contributions
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
- I_R (dict): Redundancies and unique information for each variable combination.
|
|
146
|
+
- I_S (dict): Synergies for each variable combination.
|
|
147
|
+
- MI (dict): Mutual information for each variable combination.
|
|
148
|
+
|
|
149
|
+
Example: To understand the mutual information between target variable T and
|
|
150
|
+
a combination of agent variables A1, A2, and A3, you can use:
|
|
151
|
+
I_R, I_S, MI = surd(p)
|
|
152
|
+
'''
|
|
153
|
+
|
|
154
|
+
# Total number of dimensions (target + agents)
|
|
155
|
+
Ntot = Y.shape[0]
|
|
156
|
+
# Number of agent variables
|
|
157
|
+
Nvars = Ntot - 1
|
|
158
|
+
# Limit the maximum number of combinations to max_combs
|
|
159
|
+
max_inds = range(1, max_combs+1)
|
|
160
|
+
tot_inds = range(1, Ntot)
|
|
161
|
+
|
|
162
|
+
# Compute the marginal distribution of the target variable
|
|
163
|
+
p_target = it.myhistogram(Y[0,:].T, nbins)
|
|
164
|
+
p_target = p_target.reshape((nbins,) + (1,) * (Ntot - 1))
|
|
165
|
+
|
|
166
|
+
# Prepare for specific mutual information computation
|
|
167
|
+
combs, Is = [], {}
|
|
168
|
+
red_combs= []
|
|
169
|
+
|
|
170
|
+
# Iterate over all combinations of agent variables
|
|
171
|
+
for i in max_inds:
|
|
172
|
+
for j in list(icmb(tot_inds, i)):
|
|
173
|
+
combs.append(j)
|
|
174
|
+
# noj = tuple(set(inds) - set(j))
|
|
175
|
+
|
|
176
|
+
shape = np.ones(Ntot, dtype=int)
|
|
177
|
+
|
|
178
|
+
# Compute joint distributions for current combinations
|
|
179
|
+
p_a = it.myhistogram(Y[j,:].T, nbins)
|
|
180
|
+
for index in j:
|
|
181
|
+
shape[index] = nbins
|
|
182
|
+
p_a = p_a.reshape(tuple(shape))
|
|
183
|
+
p_as = it.myhistogram(Y[(0,) + j,:].T, nbins)
|
|
184
|
+
shape[0] = nbins
|
|
185
|
+
p_as = p_as.reshape(tuple(shape))
|
|
186
|
+
|
|
187
|
+
# Compute conditional distributions for current combinations
|
|
188
|
+
p_a_s = p_as / p_target
|
|
189
|
+
p_s_a = p_as / p_a
|
|
190
|
+
|
|
191
|
+
# Compute specific mutual information
|
|
192
|
+
Is[j] = (p_a_s * (it.mylog(p_s_a) - it.mylog(p_target))).sum(axis=j).ravel()
|
|
193
|
+
|
|
194
|
+
# Compute mutual information for each combination of agent variables
|
|
195
|
+
MI = {k: (Is[k] * p_target.squeeze()).sum() for k in Is.keys()}
|
|
196
|
+
|
|
197
|
+
# Initialize redundancy and synergy terms
|
|
198
|
+
for i in tot_inds:
|
|
199
|
+
for j in list(icmb(tot_inds, i)):
|
|
200
|
+
red_combs.append(j)
|
|
201
|
+
I_R = {cc: 0 for cc in red_combs}
|
|
202
|
+
I_S = {cc: 0 for cc in combs[Nvars:]}
|
|
203
|
+
|
|
204
|
+
# Process each value of the target variable
|
|
205
|
+
for t in range(nbins):
|
|
206
|
+
# Extract specific mutual information for the current target value
|
|
207
|
+
I1 = np.array([ii[t] for ii in Is.values()])
|
|
208
|
+
|
|
209
|
+
# Sorting specific mutual information
|
|
210
|
+
i1 = np.argsort(I1)
|
|
211
|
+
lab = [combs[i_] for i_ in i1]
|
|
212
|
+
lens = np.array([len(l) for l in lab])
|
|
213
|
+
|
|
214
|
+
# Update specific mutual information based on existing maximum values
|
|
215
|
+
I1 = I1[i1]
|
|
216
|
+
for l in range(1, lens.max()):
|
|
217
|
+
inds_l2 = np.where(lens == l+1)[0]
|
|
218
|
+
Il1max = I1[lens==l].max()
|
|
219
|
+
inds_ = inds_l2[I1[inds_l2] < Il1max]
|
|
220
|
+
I1[inds_] = 0
|
|
221
|
+
|
|
222
|
+
# Recompute sorting of updated specific mutual information values
|
|
223
|
+
i1 = np.argsort(I1)
|
|
224
|
+
lab = [lab[i_] for i_ in i1]
|
|
225
|
+
|
|
226
|
+
# Compute differences in sorted specific mutual information values
|
|
227
|
+
Di = np.diff(I1[i1], prepend=0.)
|
|
228
|
+
red_vars = list(tot_inds)
|
|
229
|
+
|
|
230
|
+
# Distribute mutual information to redundancy and synergy terms
|
|
231
|
+
for i_, ll in enumerate(lab):
|
|
232
|
+
info = Di[i_] * p_target.squeeze()[t]
|
|
233
|
+
if len(ll) == 1:
|
|
234
|
+
I_R[tuple(red_vars)] += info
|
|
235
|
+
red_vars.remove(ll[0])
|
|
236
|
+
else:
|
|
237
|
+
I_S[ll] += info
|
|
238
|
+
|
|
239
|
+
return I_R, I_S, MI
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def plot(I_R, I_S, info_leak, axs, nvars, threshold=0):
|
|
243
|
+
"""
|
|
244
|
+
This function computes and plots information flux for given data.
|
|
245
|
+
:param I_R: Data for redundant contribution
|
|
246
|
+
:param I_S: Data for synergistic contribution
|
|
247
|
+
:param axs: Axes for plotting
|
|
248
|
+
:param colors: Colors for redundant, unique and synergistic contributions
|
|
249
|
+
:param nvars: Number of variables
|
|
250
|
+
:param threshold: Threshold as a percentage of the maximum value to select contributions to plot
|
|
251
|
+
"""
|
|
252
|
+
colors = {}
|
|
253
|
+
colors['redundant'] = mcolors.to_rgb('#003049')
|
|
254
|
+
colors['unique'] = mcolors.to_rgb('#d62828')
|
|
255
|
+
colors['synergistic'] = mcolors.to_rgb('#f77f00')
|
|
256
|
+
|
|
257
|
+
for key, value in colors.items():
|
|
258
|
+
rgb = mcolors.to_rgb(value)
|
|
259
|
+
colors[key] = tuple([c + (1-c) * 0.4 for c in rgb])
|
|
260
|
+
|
|
261
|
+
# Generate keys and labels
|
|
262
|
+
# Redundant Contributions
|
|
263
|
+
I_R_keys = []
|
|
264
|
+
I_R_labels = []
|
|
265
|
+
for r in range(nvars, 0, -1):
|
|
266
|
+
for comb in icmb(range(1, nvars + 1), r):
|
|
267
|
+
prefix = 'U' if len(comb) == 1 else 'R'
|
|
268
|
+
I_R_keys.append(prefix + ''.join(map(str, comb)))
|
|
269
|
+
I_R_labels.append(f"$\\mathrm{{{prefix}}}{{{''.join(map(str, comb))}}}$")
|
|
270
|
+
|
|
271
|
+
# Synergestic Contributions
|
|
272
|
+
I_S_keys = ['S' + ''.join(map(str, comb)) for r in range(2, nvars+1) for comb in icmb(range(1, nvars + 1), r)]
|
|
273
|
+
I_S_labels = [f"$\\mathrm{{S}}{{{''.join(map(str, comb))}}}$" for r in range(2, nvars+1) for comb in icmb(range(1, nvars + 1), r)]
|
|
274
|
+
|
|
275
|
+
label_keys, labels = I_R_keys + I_S_keys, I_R_labels + I_S_labels
|
|
276
|
+
|
|
277
|
+
# Extracting and normalizing the values of information measures
|
|
278
|
+
values = [I_R.get(tuple(map(int, key[1:])), 0) if 'U' in key or 'R' in key
|
|
279
|
+
else I_S.get(tuple(map(int, key[1:])), 0)
|
|
280
|
+
for key in label_keys]
|
|
281
|
+
values /= sum(values)
|
|
282
|
+
max_value = max(values)
|
|
283
|
+
|
|
284
|
+
# Filtering based on threshold
|
|
285
|
+
labels = [label for value, label in zip(values, labels) if value >= threshold]
|
|
286
|
+
values = [value for value in values if value > threshold]
|
|
287
|
+
|
|
288
|
+
# Plotting the bar graph of information measures
|
|
289
|
+
for label, value in zip(labels, values):
|
|
290
|
+
if 'U' in label:
|
|
291
|
+
color = colors['unique']
|
|
292
|
+
elif 'S' in label:
|
|
293
|
+
color = colors['synergistic']
|
|
294
|
+
else:
|
|
295
|
+
color = colors['redundant']
|
|
296
|
+
axs[0].bar(label, value, color=color, edgecolor='black',linewidth=1.5)
|
|
297
|
+
|
|
298
|
+
if nvars == 2:
|
|
299
|
+
axs[0].set_box_aspect(1/2.5)
|
|
300
|
+
else:
|
|
301
|
+
axs[0].set_box_aspect(1/4)
|
|
302
|
+
|
|
303
|
+
# Plotting the information leak bar
|
|
304
|
+
axs[1].bar(' ', info_leak, color='gray', edgecolor='black')
|
|
305
|
+
axs[1].set_ylim([0, 1])
|
|
306
|
+
axs[0].set_yticks([0., 1.])
|
|
307
|
+
axs[0].set_ylim([0., 1.])
|
|
308
|
+
|
|
309
|
+
# change all spines
|
|
310
|
+
for axis in ['top','bottom','left','right']:
|
|
311
|
+
axs[0].spines[axis].set_linewidth(2)
|
|
312
|
+
axs[1].spines[axis].set_linewidth(2)
|
|
313
|
+
|
|
314
|
+
# increase tick width
|
|
315
|
+
axs[0].tick_params(width=3)
|
|
316
|
+
axs[1].tick_params(width=3)
|
|
317
|
+
|
|
318
|
+
return dict(zip(label_keys, values))
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def plot_nlabels(I_R, I_S, info_leak, axs, nvars, nlabels=-1):
|
|
322
|
+
"""
|
|
323
|
+
This function computes and plots information flux for given data.
|
|
324
|
+
:param I_R: Data for redundant contribution
|
|
325
|
+
:param I_S: Data for synergistic contribution
|
|
326
|
+
:param axs: Axes for plotting
|
|
327
|
+
:param colors: Colors for redundant, unique and synergistic contributions
|
|
328
|
+
:param nvars: Number of variables
|
|
329
|
+
:param threshold: Threshold as a percentage of the maximum value to select contributions to plot
|
|
330
|
+
"""
|
|
331
|
+
colors = {}
|
|
332
|
+
colors['redundant'] = mcolors.to_rgb('#003049')
|
|
333
|
+
colors['unique'] = mcolors.to_rgb('#d62828')
|
|
334
|
+
colors['synergistic'] = mcolors.to_rgb('#f77f00')
|
|
335
|
+
|
|
336
|
+
for key, value in colors.items():
|
|
337
|
+
rgb = mcolors.to_rgb(value)
|
|
338
|
+
colors[key] = tuple([c + (1-c) * 0.4 for c in rgb])
|
|
339
|
+
|
|
340
|
+
# Generate keys and labels
|
|
341
|
+
# Redundant Contributions
|
|
342
|
+
I_R_keys = []
|
|
343
|
+
I_R_labels = []
|
|
344
|
+
for r in range(nvars, 0, -1):
|
|
345
|
+
for comb in icmb(range(1, nvars + 1), r):
|
|
346
|
+
prefix = 'U' if len(comb) == 1 else 'R'
|
|
347
|
+
I_R_keys.append(prefix + ''.join(map(str, comb)))
|
|
348
|
+
I_R_labels.append(f"$\\mathrm{{{prefix}}}{{{''.join(map(str, comb))}}}$")
|
|
349
|
+
|
|
350
|
+
# Synergestic Contributions
|
|
351
|
+
I_S_keys = ['S' + ''.join(map(str, comb)) for r in range(2, nvars+1) for comb in icmb(range(1, nvars + 1), r)]
|
|
352
|
+
I_S_labels = [f"$\\mathrm{{S}}{{{''.join(map(str, comb))}}}$" for r in range(2, nvars+1) for comb in icmb(range(1, nvars + 1), r)]
|
|
353
|
+
|
|
354
|
+
label_keys, labels = I_R_keys + I_S_keys, I_R_labels + I_S_labels
|
|
355
|
+
|
|
356
|
+
# Extracting and normalizing the values of information measures
|
|
357
|
+
values = [I_R.get(tuple(map(int, key[1:])), 0) if 'U' in key or 'R' in key
|
|
358
|
+
else I_S.get(tuple(map(int, key[1:])), 0)
|
|
359
|
+
for key in label_keys]
|
|
360
|
+
values /= sum(values)
|
|
361
|
+
max_value = max(values)
|
|
362
|
+
|
|
363
|
+
# Filtering based on threshold
|
|
364
|
+
top_n_indices = np.argsort(values)[-nlabels:]
|
|
365
|
+
|
|
366
|
+
# Filter both the values and labels arrays
|
|
367
|
+
filtered_values = values[top_n_indices]
|
|
368
|
+
filtered_labels = np.array(labels)[top_n_indices]
|
|
369
|
+
original_order_indices = np.argsort(top_n_indices)
|
|
370
|
+
filtered_values_in_original_order = filtered_values[original_order_indices]
|
|
371
|
+
filtered_labels_in_original_order = filtered_labels[original_order_indices]
|
|
372
|
+
|
|
373
|
+
# Convert filtered arrays back to lists if necessary
|
|
374
|
+
values = filtered_values_in_original_order
|
|
375
|
+
labels = filtered_labels_in_original_order.tolist()
|
|
376
|
+
|
|
377
|
+
# Plotting the bar graph of information measures
|
|
378
|
+
for label, value in zip(labels, values):
|
|
379
|
+
if 'U' in label:
|
|
380
|
+
color = colors['unique']
|
|
381
|
+
elif 'S' in label:
|
|
382
|
+
color = colors['synergistic']
|
|
383
|
+
else:
|
|
384
|
+
color = colors['redundant']
|
|
385
|
+
axs[0].bar(label, value, color=color, edgecolor='black',linewidth=1.5)
|
|
386
|
+
|
|
387
|
+
axs[0].set_box_aspect(1/4)
|
|
388
|
+
|
|
389
|
+
# Plotting the information leak bar
|
|
390
|
+
axs[1].bar(' ', info_leak, color='gray', edgecolor='black')
|
|
391
|
+
axs[1].set_ylim([0, 1])
|
|
392
|
+
axs[0].set_yticks([0., 1.])
|
|
393
|
+
axs[0].set_ylim([0., 1.])
|
|
394
|
+
|
|
395
|
+
# change all spines
|
|
396
|
+
for axis in ['top','bottom','left','right']:
|
|
397
|
+
axs[0].spines[axis].set_linewidth(2)
|
|
398
|
+
axs[1].spines[axis].set_linewidth(2)
|
|
399
|
+
|
|
400
|
+
# increase tick width
|
|
401
|
+
axs[0].tick_params(width=3)
|
|
402
|
+
axs[1].tick_params(width=3)
|
|
403
|
+
|
|
404
|
+
return dict(zip(label_keys, values))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def nice_print( r_, s_, mi_, leak_ ):
|
|
408
|
+
'''Print the normalized redundancies, unique and synergy particles'''
|
|
409
|
+
|
|
410
|
+
r_ = {key: value / max(mi_.values()) for key, value in r_.items()}
|
|
411
|
+
s_ = {key: value / max(mi_.values()) for key, value in s_.items()}
|
|
412
|
+
|
|
413
|
+
print( ' Redundant (R):' )
|
|
414
|
+
for k_, v_ in r_.items():
|
|
415
|
+
if len(k_) > 1:
|
|
416
|
+
print( f' {str(k_):12s}: {v_:5.4f}' )
|
|
417
|
+
|
|
418
|
+
print( ' Unique (U):' )
|
|
419
|
+
for k_, v_ in r_.items():
|
|
420
|
+
if len(k_) == 1:
|
|
421
|
+
print( f' {str(k_):12s}: {v_:5.4f}' )
|
|
422
|
+
|
|
423
|
+
print( ' Synergystic (S):' )
|
|
424
|
+
for k_, v_ in s_.items():
|
|
425
|
+
print( f' {str(k_):12s}: {v_:5.4f}' )
|
|
426
|
+
|
|
427
|
+
print(f' Information Leak: {leak_ * 100:5.2f}%')
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def run(X, nvars, nlag, nbins, axs):
|
|
431
|
+
|
|
432
|
+
information_flux = {}
|
|
433
|
+
|
|
434
|
+
for i in range(nvars):
|
|
435
|
+
print(f'SURD CAUSALITY FOR SIGNAL {i+1}')
|
|
436
|
+
|
|
437
|
+
# Organize data (0 target variable, 1: agent variables)
|
|
438
|
+
Y = np.vstack([X[i, nlag:], X[:, :-nlag]])
|
|
439
|
+
|
|
440
|
+
# Run SURD
|
|
441
|
+
hist, _ = np.histogramdd(Y.T, nbins)
|
|
442
|
+
I_R, I_S, MI, info_leak = surd(hist)
|
|
443
|
+
|
|
444
|
+
# Print results
|
|
445
|
+
nice_print(I_R, I_S, MI, info_leak)
|
|
446
|
+
|
|
447
|
+
# Plot SURD
|
|
448
|
+
information_flux[i+1] = plot(I_R, I_S, info_leak, axs[i,:], nvars, threshold=-0.01)
|
|
449
|
+
|
|
450
|
+
# Plot formatting
|
|
451
|
+
axs[i,0].set_title(f'${{\\Delta I}}_{{(\\cdot) \\rightarrow {i+1}}} / I \\left(Q_{i+1}^+ ; \\mathrm{{\\mathbf{{Q}}}} \\right)$', pad=12)
|
|
452
|
+
axs[i,1].set_title(f'$\\frac{{{{\\Delta I}}_{{\\mathrm{{leak}} \\rightarrow {i+1}}}}}{{H \\left(Q_{i+1} \\right)}}$', pad=20)
|
|
453
|
+
axs[i,0].set_xticklabels(axs[i,0].get_xticklabels(), fontsize=20, rotation = 60, ha = 'right', rotation_mode='anchor')
|
|
454
|
+
print('\n')
|
|
455
|
+
|
|
456
|
+
# Show the results
|
|
457
|
+
for i in range(0,nvars-1):
|
|
458
|
+
axs[i,0].set_xticklabels('')
|
|
459
|
+
|
|
460
|
+
return I_R, I_S, MI, info_leak
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def run_parallel(X, nvars, nlag, nbins, axs):
|
|
464
|
+
|
|
465
|
+
information_flux = {}
|
|
466
|
+
|
|
467
|
+
def _one(i):
|
|
468
|
+
# Organize data (0 target variable, 1: agent variables)
|
|
469
|
+
Y = np.vstack([X[i, nlag:], X[:, :-nlag]])
|
|
470
|
+
|
|
471
|
+
# Run SURD
|
|
472
|
+
hist, _ = np.histogramdd(Y.T, nbins)
|
|
473
|
+
I_R, I_S, MI, info_leak = surd(hist)
|
|
474
|
+
|
|
475
|
+
# Print results
|
|
476
|
+
print(f'SURD CAUSALITY FOR SIGNAL {i+1}')
|
|
477
|
+
nice_print(I_R, I_S, MI, info_leak)
|
|
478
|
+
print('\n')
|
|
479
|
+
return I_R, I_S, MI, info_leak
|
|
480
|
+
|
|
481
|
+
if pymp is not None:
|
|
482
|
+
Rd_results = pymp.shared.dict({}) # Dictionary to store redundant contributions
|
|
483
|
+
Sy_results = pymp.shared.dict({}) # Dictionary to store synergistic contributions
|
|
484
|
+
MI_results = pymp.shared.dict({}) # Dictionary to store mutual information results
|
|
485
|
+
info_leak_results = pymp.shared.dict({}) # Dictionary to store information leak results
|
|
486
|
+
with pymp.Parallel(nvars) as par:
|
|
487
|
+
for i in par.range(nvars):
|
|
488
|
+
Rd_results[i+1], Sy_results[i+1], MI_results[i+1], info_leak_results[i+1] = _one(i)
|
|
489
|
+
else: # pymp not installed (e.g. Windows): same computation, one target at a time
|
|
490
|
+
Rd_results, Sy_results, MI_results, info_leak_results = {}, {}, {}, {}
|
|
491
|
+
for i in range(nvars):
|
|
492
|
+
Rd_results[i+1], Sy_results[i+1], MI_results[i+1], info_leak_results[i+1] = _one(i)
|
|
493
|
+
|
|
494
|
+
for i in range(nvars):
|
|
495
|
+
# Plot SURD
|
|
496
|
+
information_flux[i+1] = plot(Rd_results[i+1], Sy_results[i+1], info_leak_results[i+1], axs[i,:], nvars, threshold=-0.01)
|
|
497
|
+
|
|
498
|
+
# Plot formatting
|
|
499
|
+
axs[i,0].set_title(f'${{\\Delta I}}_{{(\\cdot) \\rightarrow {i+1}}} / I \\left(Q_{i+1}^+ ; \\mathrm{{\\mathbf{{Q}}}} \\right)$', pad=12)
|
|
500
|
+
axs[i,1].set_title(f'$\\frac{{{{\\Delta I}}_{{\\mathrm{{leak}} \\rightarrow {i+1}}}}}{{H \\left(Q_{i+1} \\right)}}$', pad=20)
|
|
501
|
+
axs[i,0].set_xticklabels(axs[i,0].get_xticklabels(), fontsize=20, rotation = 60, ha = 'right', rotation_mode='anchor')
|
|
502
|
+
|
|
503
|
+
# Show the results
|
|
504
|
+
for i in range(0,nvars-1):
|
|
505
|
+
axs[i,0].set_xticklabels('')
|
|
506
|
+
|
|
507
|
+
# Same return value as before: the decomposition for the first target
|
|
508
|
+
return Rd_results[1], Sy_results[1], MI_results[1], info_leak_results[1]
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def plot_multiple_lags(I_R, I_S, info_leak, axs, n_vars_lag, n_lag, threshold=0):
|
|
512
|
+
"""
|
|
513
|
+
This function computes and plots information flux for given data.
|
|
514
|
+
:param I_R: Data for redundant contribution
|
|
515
|
+
:param I_S: Data for synergistic contribution
|
|
516
|
+
:param axs: Axis for plotting
|
|
517
|
+
:param n_vars_lag: Number of variables including lags
|
|
518
|
+
:param n_lag: Number of lags
|
|
519
|
+
:param threshold: Threshold as a percentage of the maximum value to select contributions to plot
|
|
520
|
+
"""
|
|
521
|
+
colors = {}
|
|
522
|
+
colors['redundant'] = mcolors.to_rgb('#003049')
|
|
523
|
+
colors['unique'] = mcolors.to_rgb('#d62828')
|
|
524
|
+
colors['synergistic'] = mcolors.to_rgb('#f77f00')
|
|
525
|
+
|
|
526
|
+
for key, value in colors.items():
|
|
527
|
+
rgb = mcolors.to_rgb(value)
|
|
528
|
+
colors[key] = tuple([c + (1-c) * 0.4 for c in rgb])
|
|
529
|
+
|
|
530
|
+
# Generate keys and labels
|
|
531
|
+
n_vars = n_vars_lag // n_lag
|
|
532
|
+
|
|
533
|
+
# Redundant Contributions
|
|
534
|
+
I_R_keys = []
|
|
535
|
+
I_R_labels = []
|
|
536
|
+
# for r in range(1, n_vars_lag + 1):
|
|
537
|
+
for r in range(n_vars_lag, 0, -1):
|
|
538
|
+
for comb in itertools.combinations(range(1, n_vars_lag + 1), r):
|
|
539
|
+
prefix = 'U' if len(comb) == 1 else 'R'
|
|
540
|
+
I_R_keys.append(prefix + ''.join(map(str, comb)))
|
|
541
|
+
|
|
542
|
+
# New label generation with subscripts for lags
|
|
543
|
+
new_comb_labels = []
|
|
544
|
+
for c in comb:
|
|
545
|
+
lag_number = (c - 1) // n_vars
|
|
546
|
+
var_number = (c - 1) % n_vars + 1
|
|
547
|
+
new_label = f"{var_number}_{{{lag_number+1}}}"
|
|
548
|
+
new_comb_labels.append(new_label)
|
|
549
|
+
|
|
550
|
+
I_R_labels.append(f"$\\mathrm{{{prefix}}}{{{''.join(new_comb_labels)}}}$")
|
|
551
|
+
|
|
552
|
+
# Synergestic Contributions
|
|
553
|
+
I_S_keys = []
|
|
554
|
+
I_S_labels = []
|
|
555
|
+
for r in range(2, n_vars_lag + 1): # Starting from 2 because synergistic contributions require at least two variables
|
|
556
|
+
for comb in itertools.combinations(range(1, n_vars_lag + 1), r):
|
|
557
|
+
# Generating the key
|
|
558
|
+
I_S_keys.append('S' + ''.join(map(str, comb)))
|
|
559
|
+
|
|
560
|
+
# Generating the label with subscripts for lags
|
|
561
|
+
new_comb_labels = []
|
|
562
|
+
for c in comb:
|
|
563
|
+
lag_number = (c - 1) // n_vars
|
|
564
|
+
var_number = (c - 1) % n_vars + 1
|
|
565
|
+
new_label = f"{var_number}_{{{lag_number+1}}}"
|
|
566
|
+
new_comb_labels.append(new_label)
|
|
567
|
+
|
|
568
|
+
I_S_labels.append(f"$\\mathrm{{S}}{{{''.join(new_comb_labels)}}}$")
|
|
569
|
+
|
|
570
|
+
label_keys, labels = I_R_keys + I_S_keys, I_R_labels + I_S_labels
|
|
571
|
+
|
|
572
|
+
# Extracting and normalizing the values of information measures
|
|
573
|
+
values = [I_R.get(tuple(map(int, key[1:])), 0) if 'U' in key or 'R' in key
|
|
574
|
+
else I_S.get(tuple(map(int, key[1:])), 0)
|
|
575
|
+
for key in label_keys]
|
|
576
|
+
values /= sum(values)
|
|
577
|
+
max_value = max(values)
|
|
578
|
+
|
|
579
|
+
# Filtering based on threshold
|
|
580
|
+
labels = [label for value, label in zip(values, labels) if value >= threshold]
|
|
581
|
+
values = [value for value in values if value > threshold]
|
|
582
|
+
|
|
583
|
+
# Plotting the bar graph of information measures
|
|
584
|
+
for label, value in zip(labels, values):
|
|
585
|
+
if 'U' in label:
|
|
586
|
+
color = colors['unique']
|
|
587
|
+
elif 'S' in label:
|
|
588
|
+
color = colors['synergistic']
|
|
589
|
+
else:
|
|
590
|
+
color = colors['redundant']
|
|
591
|
+
axs[0].bar(label, value, color=color, edgecolor='black',linewidth=1.5)
|
|
592
|
+
|
|
593
|
+
# Plotting the bar graph of information measures
|
|
594
|
+
axs[0].set_xticks(range(len(values)))
|
|
595
|
+
shift_labels = axs[0].set_xticklabels(labels, fontsize=15, rotation = 60, ha = 'right', rotation_mode='anchor')
|
|
596
|
+
axs[0].set_box_aspect(1/5)
|
|
597
|
+
|
|
598
|
+
# Plotting the information leak bar
|
|
599
|
+
axs[1].bar(' ', info_leak, color='gray', edgecolor='black')
|
|
600
|
+
axs[1].set_ylim([0, 1])
|
|
601
|
+
|
|
602
|
+
# change all spines
|
|
603
|
+
for axis in ['top','bottom','left','right']:
|
|
604
|
+
axs[0].spines[axis].set_linewidth(1.5)
|
|
605
|
+
axs[1].spines[axis].set_linewidth(1.5)
|
|
606
|
+
|
|
607
|
+
# increase tick width
|
|
608
|
+
axs[0].tick_params(width=1.5)
|
|
609
|
+
axs[1].tick_params(width=1.5)
|
|
610
|
+
|
|
611
|
+
return dict(zip(label_keys, values))
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def run_multiple_lags(X, nvars, nlag, nbins, max_combs, axs):
|
|
615
|
+
"Run SURD causality for different lags (from lag 1 up to nlag)"
|
|
616
|
+
information_flux = {}
|
|
617
|
+
|
|
618
|
+
for i in range(nvars):
|
|
619
|
+
print(f'SURD CAUSALITY FOR SIGNAL {i+1}')
|
|
620
|
+
|
|
621
|
+
# Organize data (0 target variable, 1: agent variables)
|
|
622
|
+
Y = X[i, nlag+1:]
|
|
623
|
+
# Create the lagged versions of X and append to the list
|
|
624
|
+
for lag in range(nlag, 0, -1):
|
|
625
|
+
Y = np.vstack([Y, X[:, lag:-nlag + lag - 1]])
|
|
626
|
+
|
|
627
|
+
# Run SURD
|
|
628
|
+
I_R, I_S, MI = surd_hd(Y, nbins, max_combs)
|
|
629
|
+
|
|
630
|
+
# Calculate information leak
|
|
631
|
+
hist = it.myhistogram(Y[0,:].T, nbins)
|
|
632
|
+
H = it.entropy_nvars(hist, (0,) )
|
|
633
|
+
info_leak = 1 - (sum(I_R.values()) + sum(I_S.values())) / H
|
|
634
|
+
|
|
635
|
+
# Print results
|
|
636
|
+
nice_print(I_R, I_S, MI, info_leak)
|
|
637
|
+
print('\n')
|
|
638
|
+
|
|
639
|
+
# Plot SURD
|
|
640
|
+
information_flux[i+1] = plot_multiple_lags(I_R, I_S, info_leak, axs[i,:], nvars*nlag, nlag, threshold=-0.01)
|
|
641
|
+
|
|
642
|
+
# Plot formatting
|
|
643
|
+
axs[i,0].set_title(f'${{\\Delta I}}_{{(\\cdot) \\rightarrow {i+1}}} / I \\left(Q_{i+1}^+ ; \\mathrm{{\\mathbf{{Q}}}} \\right)$', pad=10)
|
|
644
|
+
axs[i,1].set_title(f'$\\frac{{{{\\Delta I}}_{{\\mathrm{{leak}} \\rightarrow {i+1}}}}}{{H \\left(Q_{i+1} \\right)}}$', pad=18)
|
|
645
|
+
axs[i,1].set_yticks([0,1])
|
|
646
|
+
axs[i,0].set_xticklabels(axs[i,0].get_xticklabels(), fontsize=14, rotation = 60, ha = 'right', rotation_mode='anchor')
|
|
647
|
+
|
|
648
|
+
# change all spines
|
|
649
|
+
for axis in ['top','bottom','left','right']:
|
|
650
|
+
axs[i,0].spines[axis].set_linewidth(2.5)
|
|
651
|
+
axs[i,1].spines[axis].set_linewidth(2.5)
|
|
652
|
+
axs[i,0].set_box_aspect(1/4.5)
|
|
653
|
+
|
|
654
|
+
# Show the results
|
|
655
|
+
for i in range(0,nvars-1):
|
|
656
|
+
axs[i,0].set_xticklabels('')
|
|
657
|
+
|
|
658
|
+
return I_R, I_S, MI, info_leak
|
surd/it_tools.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def myhistogram(x, nbins):
|
|
5
|
+
|
|
6
|
+
hist, _ = np.histogramdd(x, nbins)
|
|
7
|
+
|
|
8
|
+
hist += 1e-14
|
|
9
|
+
hist /= hist.sum()
|
|
10
|
+
|
|
11
|
+
return hist
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def mylog(x):
|
|
15
|
+
"""
|
|
16
|
+
Compute the logarithm in base 2 avoiding singularities.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
- x (np.array): Input data.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
- np.array: Logarithm in base 2 of the input.
|
|
23
|
+
"""
|
|
24
|
+
valid_indices = (x != 0) & (~np.isnan(x)) & (~np.isinf(x))
|
|
25
|
+
|
|
26
|
+
log_values = np.zeros_like(x)
|
|
27
|
+
log_values[valid_indices] = np.log2(x[valid_indices])
|
|
28
|
+
|
|
29
|
+
return log_values
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def entropy(p):
|
|
33
|
+
"""
|
|
34
|
+
Compute the entropy of a discrete probability distribution function.
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
- p (np.array): Probability distribution of the signal.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
- float: Entropy of the given distribution.
|
|
41
|
+
"""
|
|
42
|
+
return -np.sum(p * mylog(p))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def entropy_nvars(p, indices):
|
|
46
|
+
"""
|
|
47
|
+
Compute the joint entropy for specific dimensions of a probability distribution.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
- p (np.array): N-dimensional joint probability distribution.
|
|
51
|
+
- indices (tuple): Dimensions over which the entropy is to be computed.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
- float: Joint entropy for specified dimensions.
|
|
55
|
+
|
|
56
|
+
Example: compute the joint entropy H(X0,X3,X7)
|
|
57
|
+
>>> entropy_nvars(p, (0,3,7))
|
|
58
|
+
"""
|
|
59
|
+
excluded_indices = tuple(set(range(p.ndim)) - set(indices))
|
|
60
|
+
marginalized_distribution = p.sum(axis=excluded_indices)
|
|
61
|
+
|
|
62
|
+
return entropy(marginalized_distribution)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cond_entropy(p, target_indices, conditioning_indices):
|
|
66
|
+
"""
|
|
67
|
+
Compute the conditional entropy between two sets of variables.
|
|
68
|
+
|
|
69
|
+
Parameters:
|
|
70
|
+
- p (np.array): N-dimensional joint probability distribution.
|
|
71
|
+
- target_indices (tuple): Variables for which entropy is to be computed.
|
|
72
|
+
- conditioning_indices (tuple): Conditioning variables.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
- float: Conditional entropy.
|
|
76
|
+
|
|
77
|
+
Example: compute the conditional entropy H(X0,X2|X7)
|
|
78
|
+
>>> cond_entropy(p, (0, 2), (7,))
|
|
79
|
+
"""
|
|
80
|
+
joint_entropy = entropy_nvars(p, set(target_indices) | set(conditioning_indices))
|
|
81
|
+
conditioning_entropy = entropy_nvars(p, conditioning_indices)
|
|
82
|
+
|
|
83
|
+
return joint_entropy - conditioning_entropy
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def mutual_info(p, set1_indices, set2_indices):
|
|
87
|
+
"""
|
|
88
|
+
Compute the mutual information between two sets of variables.
|
|
89
|
+
|
|
90
|
+
Parameters:
|
|
91
|
+
- p (np.array): N-dimensional joint probability distribution.
|
|
92
|
+
- set1_indices (tuple): Indices of the first set of variables.
|
|
93
|
+
- set2_indices (tuple): Indices of the second set of variables.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
- float: Mutual information.
|
|
97
|
+
|
|
98
|
+
Example: compute the mutual information I(X0,X5;X4,X2)
|
|
99
|
+
>>> mutual_info(p, (0, 5), (4, 2))
|
|
100
|
+
"""
|
|
101
|
+
entropy_set1 = entropy_nvars(p, set1_indices)
|
|
102
|
+
conditional_entropy = cond_entropy(p, set1_indices, set2_indices)
|
|
103
|
+
|
|
104
|
+
return entropy_set1 - conditional_entropy
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cond_mutual_info(p, ind1, ind2, ind3):
|
|
108
|
+
"""
|
|
109
|
+
Compute the conditional mutual information between two sets of variables
|
|
110
|
+
conditioned to a third set.
|
|
111
|
+
|
|
112
|
+
Parameters:
|
|
113
|
+
- p (np.array): N-dimensional joint probability distribution.
|
|
114
|
+
- ind1 (tuple): Indices of the first set of variables.
|
|
115
|
+
- ind2 (tuple): Indices of the second set of variables.
|
|
116
|
+
- ind3 (tuple): Indices of the conditioning variables.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- float: Conditional mutual information.
|
|
120
|
+
|
|
121
|
+
Example: compute the conditional mutual information I(X0,X5;X4,X2|X1)
|
|
122
|
+
cond_mutual_info(p, (0, 5), (4, 2), (1,)))
|
|
123
|
+
"""
|
|
124
|
+
# Merge indices of ind2 and ind3
|
|
125
|
+
combined_indices = tuple(set(ind2) | set(ind3))
|
|
126
|
+
|
|
127
|
+
# Compute conditional mutual information
|
|
128
|
+
return cond_entropy(p, ind1, ind3) - cond_entropy(p, ind1, combined_indices)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def transfer_entropy(p, target_var):
|
|
132
|
+
"""
|
|
133
|
+
Calculate the transfer entropy from each input variable to the target variable.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
- p (np.array): Multi-dimensional array containing the pdfs of the variables.
|
|
137
|
+
The first dimension corresponds to the index of the variable:
|
|
138
|
+
p[0] -> target variable (in future)
|
|
139
|
+
p[1:] -> input variables (at present time)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
- np.array: Transfer entropy values for each input variable.
|
|
143
|
+
"""
|
|
144
|
+
num_vars = len(p.shape) - 1 # Excluding the future variable
|
|
145
|
+
TE = np.zeros(num_vars)
|
|
146
|
+
|
|
147
|
+
for i in range(1, num_vars + 1):
|
|
148
|
+
# The indices for the present variables
|
|
149
|
+
present_indices = tuple(range(1, num_vars + 1))
|
|
150
|
+
|
|
151
|
+
# The indices for the present variables excluding the i-th variable
|
|
152
|
+
# conditioning_indices = tuple([target_var] + [j for j in range(1, num_vars + 1) if j != i])
|
|
153
|
+
conditioning_indices = tuple([target_var] + [j for j in range(1, num_vars + 1) if j != i and j != target_var])
|
|
154
|
+
|
|
155
|
+
# Conditional entropy of the future state of the target variable given its own past
|
|
156
|
+
cond_ent_target_given_past = cond_entropy(p, (0,), conditioning_indices)
|
|
157
|
+
|
|
158
|
+
# Conditional entropy of the future state of the target variable given its own past and the ith input variable
|
|
159
|
+
cond_ent_target_given_past_and_input = cond_entropy(p, (0,), present_indices)
|
|
160
|
+
|
|
161
|
+
# Transfer entropy calculation
|
|
162
|
+
TE[i-1] = cond_ent_target_given_past - cond_ent_target_given_past_and_input
|
|
163
|
+
|
|
164
|
+
return TE
|
|
165
|
+
|
|
166
|
+
|
surd/transport_map.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import mpart as mt
|
|
3
|
+
from scipy.stats import multivariate_normal
|
|
4
|
+
from scipy.optimize import minimize
|
|
5
|
+
print('Kokkos is using', mt.Concurrency(), 'threads')
|
|
6
|
+
|
|
7
|
+
# Negative log likelihood objective
|
|
8
|
+
def obj(coeffs, tri_map,x):
|
|
9
|
+
""" Evaluates the log-likelihood of the samples using the map-induced density. """
|
|
10
|
+
num_points = x.shape[1]
|
|
11
|
+
tri_map.SetCoeffs(coeffs)
|
|
12
|
+
|
|
13
|
+
# Reference density
|
|
14
|
+
rho1 = multivariate_normal(np.zeros(1),np.eye(1))
|
|
15
|
+
|
|
16
|
+
# Compute the map-induced density at each point
|
|
17
|
+
map_of_x = tri_map.Evaluate(x)
|
|
18
|
+
rho_of_map_of_x = rho1.logpdf(map_of_x.T)
|
|
19
|
+
log_det = tri_map.LogDeterminant(x)
|
|
20
|
+
|
|
21
|
+
# Return the negative log-likelihood of the entire dataset
|
|
22
|
+
return -np.sum(rho_of_map_of_x + log_det)/num_points
|
|
23
|
+
|
|
24
|
+
def grad_obj(coeffs, tri_map, x):
|
|
25
|
+
""" Returns the gradient of the log-likelihood objective wrt the map parameters. """
|
|
26
|
+
num_points = x.shape[1]
|
|
27
|
+
tri_map.SetCoeffs(coeffs)
|
|
28
|
+
|
|
29
|
+
# Evaluate the map
|
|
30
|
+
map_of_x = tri_map.Evaluate(x)
|
|
31
|
+
|
|
32
|
+
# Now compute the inner product of the map jacobian (\nabla_w S) and the gradient (which is just -S(x) here)
|
|
33
|
+
grad_rho_of_map_of_x = -tri_map.CoeffGrad(x, map_of_x)
|
|
34
|
+
|
|
35
|
+
# Get the gradient of the log determinant with respect to the map coefficients
|
|
36
|
+
grad_log_det = tri_map.LogDeterminantCoeffGrad(x)
|
|
37
|
+
|
|
38
|
+
return -np.sum(grad_rho_of_map_of_x + grad_log_det, 1)/num_points
|
|
39
|
+
|
|
40
|
+
def pullback_pdf(tri_map,rho,x):
|
|
41
|
+
r = tri_map.Evaluate(x)
|
|
42
|
+
log_pdf = rho.logpdf(r.T)+tri_map.LogDeterminant(x)
|
|
43
|
+
return np.exp(log_pdf)
|
|
44
|
+
|
|
45
|
+
def transport_map(x, nbins):
|
|
46
|
+
nvars = x.shape[0] # Number of variables
|
|
47
|
+
total_order = 4 # Total order of the polynomial expansion
|
|
48
|
+
components = [] # To store each component of the map
|
|
49
|
+
|
|
50
|
+
for var in range(1, nvars + 1):
|
|
51
|
+
# Create a FixedMultiIndexSet for each variable up to the total order
|
|
52
|
+
fixed_mset = mt.FixedMultiIndexSet(var, total_order)
|
|
53
|
+
map_options = mt.MapOptions()
|
|
54
|
+
S = mt.CreateComponent(fixed_mset, map_options)
|
|
55
|
+
|
|
56
|
+
# Optimize the component
|
|
57
|
+
optimizer_options = {'gtol': 1e-4, 'disp': True}
|
|
58
|
+
res = minimize(obj, S.CoeffMap(), args=(S, x[:var, :]), jac=grad_obj, method='BFGS', options=optimizer_options)
|
|
59
|
+
|
|
60
|
+
# Store the optimized component
|
|
61
|
+
components.append(S)
|
|
62
|
+
|
|
63
|
+
# Assemble the TriangularMap from the optimized components
|
|
64
|
+
transport_map = mt.TriangularMap(components)
|
|
65
|
+
|
|
66
|
+
# Set coefficients for the entire map (assuming obj and grad_obj functions can handle this structure)
|
|
67
|
+
coeffs = np.concatenate([comp.CoeffMap() for comp in components])
|
|
68
|
+
transport_map.SetCoeffs(coeffs)
|
|
69
|
+
|
|
70
|
+
# Evaluation grid
|
|
71
|
+
grids = []
|
|
72
|
+
for i in range(x.shape[0]):
|
|
73
|
+
grid_i = np.linspace(x[i, :].min(), x[i, :].max(), nbins)
|
|
74
|
+
grids.append(grid_i)
|
|
75
|
+
|
|
76
|
+
meshgrids = np.meshgrid(*grids, indexing='ij')
|
|
77
|
+
grid_points = np.vstack([grid.flatten() for grid in meshgrids])
|
|
78
|
+
|
|
79
|
+
# Compute joint pdf
|
|
80
|
+
ref_distribution = multivariate_normal(np.zeros(nvars), np.eye(nvars))
|
|
81
|
+
map_induced_pdf = pullback_pdf(transport_map, ref_distribution, grid_points)
|
|
82
|
+
|
|
83
|
+
# Construct the shape tuple for reshaping
|
|
84
|
+
reshaped = (nbins,) * nvars
|
|
85
|
+
|
|
86
|
+
return map_induced_pdf.reshape(reshaped), meshgrids
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: surd
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Synergistic-Unique-Redundant Decomposition of causality for time series
|
|
5
|
+
Project-URL: Repository, https://github.com/ALD-Lab/SURD
|
|
6
|
+
Project-URL: Paper, https://doi.org/10.1038/s41467-024-53373-4
|
|
7
|
+
Project-URL: Changelog, https://github.com/ALD-Lab/SURD/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/ALD-Lab/SURD/issues
|
|
9
|
+
Author: Álvaro Martínez-Sánchez, Gonzalo Arranz, Adrián Lozano-Durán
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: causal inference,causality,information theory,mutual information,redundancy,synergy,time series,turbulence
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: matplotlib
|
|
29
|
+
Requires-Dist: numpy
|
|
30
|
+
Requires-Dist: scipy
|
|
31
|
+
Requires-Dist: tqdm
|
|
32
|
+
Provides-Extra: examples
|
|
33
|
+
Requires-Dist: jupyterlab; extra == 'examples'
|
|
34
|
+
Provides-Extra: parallel
|
|
35
|
+
Requires-Dist: pymp-pypi; (platform_system != 'Windows') and extra == 'parallel'
|
|
36
|
+
Provides-Extra: transport
|
|
37
|
+
Requires-Dist: mpart; extra == 'transport'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# SURD: Synergistic-Unique-Redundant Decomposition of causality
|
|
41
|
+
|
|
42
|
+
[](https://github.com/ALD-Lab/SURD/actions/workflows/tests.yml)
|
|
43
|
+
[](https://pypi.org/project/surd/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](https://doi.org/10.1038/s41467-024-53373-4)
|
|
46
|
+
|
|
47
|
+
SURD is a method for causal inference in time series. It measures the information that past observations of a set of variables carry about the future of a target variable and splits it into redundant, unique and synergistic contributions. The method also quantifies the causality leak, the part of the future of the target that the observed variables cannot explain. SURD needs only pairs of past and future events, so it applies to simulations and experiments alike. It handles several variables, dependencies at different time lags and instantaneous links. The method is described in Martínez-Sánchez, Arranz & Lozano-Durán, *Nature Communications* 15, 9296 (2024).
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
From PyPI:
|
|
54
|
+
```sh
|
|
55
|
+
pip install surd
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
From a clone of this repository:
|
|
59
|
+
```sh
|
|
60
|
+
git clone https://github.com/ALD-Lab/SURD.git
|
|
61
|
+
cd SURD
|
|
62
|
+
pip install .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
With conda, the environment file installs the dependencies and the package:
|
|
66
|
+
```sh
|
|
67
|
+
conda env create -f environment.yml
|
|
68
|
+
conda activate surd
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
SURD depends on NumPy, SciPy, Matplotlib and tqdm and runs on Python 3.9 or newer, on Linux, macOS and Windows. Three optional extras exist:
|
|
72
|
+
|
|
73
|
+
- `pip install "surd[parallel]"` adds `pymp-pypi`, which runs the loop over target variables of `surd.run_parallel` in parallel (Linux and macOS). Without it the targets run one after another with the same result.
|
|
74
|
+
- `pip install "surd[transport]"` adds `mpart` for density estimation with transport maps (example E07). On `osx-arm64` use `pip install mpart`, since the conda package does not exist for that platform. See the [MParT documentation](https://measuretransport.github.io/MParT/).
|
|
75
|
+
- `pip install "surd[examples]"` adds JupyterLab to run the notebooks.
|
|
76
|
+
|
|
77
|
+
The cost of SURD grows with the number of variables and bins, since it estimates joint probability distributions. The examples run on a laptop with 16 GB of RAM.
|
|
78
|
+
|
|
79
|
+
## Quick start
|
|
80
|
+
|
|
81
|
+
Two independent bits and a target equal to their XOR one step later. Each bit alone says nothing about the target, so the information is synergistic.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import numpy as np
|
|
85
|
+
import surd
|
|
86
|
+
|
|
87
|
+
rng = np.random.default_rng(0)
|
|
88
|
+
q1, q2 = rng.integers(0, 2, size=(2, 200_000)) # two independent bits in time
|
|
89
|
+
target = np.roll(q1 ^ q2, 1) # their XOR, one step later
|
|
90
|
+
Y = np.vstack([target[1:], q1[:-1], q2[:-1]]) # future of the target first, sources after
|
|
91
|
+
hist, _ = np.histogramdd(Y.T, bins=2)
|
|
92
|
+
I_R, I_S, MI, info_leak = surd.surd(hist)
|
|
93
|
+
surd.nice_print(I_R, I_S, MI, info_leak)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Redundant (R):
|
|
98
|
+
(1, 2) : 0.0000
|
|
99
|
+
Unique (U):
|
|
100
|
+
(1,) : 0.0000
|
|
101
|
+
(2,) : 0.0000
|
|
102
|
+
Synergystic (S):
|
|
103
|
+
(1, 2) : 1.0000
|
|
104
|
+
Information Leak: 0.00%
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## How to use the method
|
|
108
|
+
|
|
109
|
+
The input is a histogram of the joint distribution of the future of the target and the present of the sources. Axis 0 is the target and axes 1 to N are the sources. The outputs are dictionaries keyed by tuples of source indices, counted from 1.
|
|
110
|
+
|
|
111
|
+
| Function | What it does |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `surd.surd(p)` | Decomposition of the histogram `p`. Returns `I_R` (redundant terms, with the unique terms under the one-element keys), `I_S` (synergistic terms), `MI` (mutual information of each subset of sources with the target) and `info_leak`, the causality leak in [0, 1]. |
|
|
114
|
+
| `surd.surd_hd(Y, nbins, max_combs)` | Same decomposition from the signals `Y` (target first) for systems with many variables. Synergies are computed up to `max_combs` sources at a time. Returns `I_R`, `I_S` and `MI`. |
|
|
115
|
+
| `surd.run(X, nvars, nlag, nbins, axs)` | Runs `surd.surd` with each of the `nvars` signals of `X` as target at lag `nlag`, prints and plots the result. |
|
|
116
|
+
| `surd.run_parallel(...)` | Same as `surd.run`, with the targets in parallel when `pymp` is available. |
|
|
117
|
+
| `surd.run_multiple_lags(X, nvars, nlag, nbins, max_combs, axs)` | Same as `surd.run` with all lags from 1 to `nlag` as sources. |
|
|
118
|
+
| `surd.nice_print(I_R, I_S, MI, info_leak)` | Prints the terms normalized by the mutual information of the full set of sources and the leak in percent. |
|
|
119
|
+
| `surd.plot(I_R, I_S, info_leak, axs, nvars, threshold)` | Bar chart of the normalized terms (blue redundant, red unique, orange synergistic) and a separate bar for the leak. |
|
|
120
|
+
|
|
121
|
+
`surd.it_tools` has the entropies, conditional entropies, mutual information and transfer entropy used by the method. `surd.analytic_eqs` has the benchmark systems of the paper. `surd.transport_map` estimates densities with transport maps and needs `mpart`.
|
|
122
|
+
|
|
123
|
+
Scripts written for the previous layout, which added `utils/` to `sys.path` and imported `surd`, `it_tools`, `analytic_eqs` and `transport_map` as top-level modules, keep working. The files in `utils/` load the package.
|
|
124
|
+
|
|
125
|
+
## Examples
|
|
126
|
+
|
|
127
|
+
The notebooks in [`examples/`](examples) reproduce the cases of the paper. Run them from the `examples` folder. They import the installed package and fall back to `../src` from a plain clone.
|
|
128
|
+
|
|
129
|
+
| Notebook | What it shows |
|
|
130
|
+
|---|---|
|
|
131
|
+
| [E01](examples/E01_logic_gates.ipynb) | Logic gates. A duplicated source is redundant, a copied source is unique, an XOR is synergistic. |
|
|
132
|
+
| [E02](examples/E02_building_blocks.ipynb) | Building blocks of causal interactions: mediator, confounder, synergistic collider and redundant collider. |
|
|
133
|
+
| [E03](examples/E03_energy_cascade.ipynb) | Energy cascade in isotropic turbulence. Causality between the energy transfer at four scales from a direct numerical simulation. |
|
|
134
|
+
| [E04](examples/E04_lagged_dependencies.ipynb) | Stochastic systems with dependencies at several time lags, linear and nonlinear. |
|
|
135
|
+
| [E05](examples/E05_Moran_effect.ipynb) | Moran effect. Two populations driven by a common environmental forcing that is not observed. |
|
|
136
|
+
| [E06](examples/E06_logistic_system.ipynb) | Coupled logistic maps with one-way and two-way coupling. |
|
|
137
|
+
| [E07](examples/E07_transport_map.ipynb) | Density estimation with transport maps instead of histograms (needs `mpart`). |
|
|
138
|
+
| [E08](examples/E08_eight_species.ipynb) | System with eight interacting species. Decomposition with synergies truncated at a given order. |
|
|
139
|
+
| [E09](examples/E09_contemporaneous.ipynb) | Systems with contemporaneous links on top of mediator, confounder and synergistic interactions. |
|
|
140
|
+
| [E10](examples/E10_inner_outer.ipynb) | Experimental data from a turbulent boundary layer. Causality between inner-layer and outer-layer velocity motions as a function of the time lag. |
|
|
141
|
+
|
|
142
|
+
The data for E03 and E10 are in [`data/`](data). E02 writes the signals it generates to `data/` on the first run.
|
|
143
|
+
|
|
144
|
+
## Citation
|
|
145
|
+
|
|
146
|
+
If you use SURD in your research or software, please cite the paper:
|
|
147
|
+
|
|
148
|
+
```bibtex
|
|
149
|
+
@article{surd,
|
|
150
|
+
author={Mart{\'\i}nez-S{\'a}nchez, {\'A}lvaro and Arranz, Gonzalo and Lozano-Dur{\'a}n, Adri{\'a}n},
|
|
151
|
+
title={Decomposing causality into its synergistic, unique, and redundant components},
|
|
152
|
+
journal={Nature Communications},
|
|
153
|
+
year={2024},
|
|
154
|
+
month={Nov},
|
|
155
|
+
day={01},
|
|
156
|
+
volume={15},
|
|
157
|
+
number={1},
|
|
158
|
+
pages={9296},
|
|
159
|
+
issn={2041-1723},
|
|
160
|
+
doi={10.1038/s41467-024-53373-4}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The file [`CITATION.cff`](CITATION.cff) carries the same reference in machine-readable form.
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
surd/__init__.py,sha256=OrqXAu2IN48Uqg5jJE_IYOxMUiuJneV4P_KA4oC7oW8,2176
|
|
2
|
+
surd/analytic_eqs.py,sha256=L0pzMtZrD7Z-5Av68oMVH-p3qhyrxxwm-FObB3ZJdpY,1542
|
|
3
|
+
surd/core.py,sha256=5Ikb2ACxgtZBdUidDXj0rAZXfV80S1-xeBggaoCyqqA,25132
|
|
4
|
+
surd/it_tools.py,sha256=Omx1LjeWF3TMlR26-F0V4vw5SGw9Qu-o4H0O3pBVThg,5223
|
|
5
|
+
surd/transport_map.py,sha256=6LaVYyVxju8FJLuQVGHO75x1kHneFzNlaieaMWdWxSg,3228
|
|
6
|
+
surd-1.1.0.dist-info/METADATA,sha256=EFZpjq6JYGp_k1d5C1TpSRaC17u1RNpsR5QL9z_jZBk,9513
|
|
7
|
+
surd-1.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
surd-1.1.0.dist-info/licenses/LICENSE,sha256=v2fBj03zZIq4jRRC6nk1irOrFj141DdJKUtG_DT-dco,1091
|
|
9
|
+
surd-1.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 MIT Computational Turbulence Group
|
|
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.
|