vbi 0.1.3__cp310-cp310-manylinux2014_x86_64.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.
- vbi/__init__.py +37 -0
- vbi/_version.py +17 -0
- vbi/dataset/__init__.py +0 -0
- vbi/dataset/connectivity_84/centers.txt +84 -0
- vbi/dataset/connectivity_84/centres.txt +84 -0
- vbi/dataset/connectivity_84/cortical.txt +84 -0
- vbi/dataset/connectivity_84/tract_lengths.txt +84 -0
- vbi/dataset/connectivity_84/weights.txt +84 -0
- vbi/dataset/connectivity_88/Aud_88.txt +88 -0
- vbi/dataset/connectivity_88/Bold.npz +0 -0
- vbi/dataset/connectivity_88/Labels.txt +17 -0
- vbi/dataset/connectivity_88/Region_labels.txt +88 -0
- vbi/dataset/connectivity_88/tract_lengths.txt +88 -0
- vbi/dataset/connectivity_88/weights.txt +88 -0
- vbi/feature_extraction/__init__.py +1 -0
- vbi/feature_extraction/calc_features.py +293 -0
- vbi/feature_extraction/features.json +535 -0
- vbi/feature_extraction/features.py +2124 -0
- vbi/feature_extraction/features_settings.py +374 -0
- vbi/feature_extraction/features_utils.py +1357 -0
- vbi/feature_extraction/infodynamics.jar +0 -0
- vbi/feature_extraction/utility.py +507 -0
- vbi/inference.py +98 -0
- vbi/models/__init__.py +0 -0
- vbi/models/cpp/__init__.py +0 -0
- vbi/models/cpp/_src/__init__.py +0 -0
- vbi/models/cpp/_src/__pycache__/mpr_sde.cpython-310.pyc +0 -0
- vbi/models/cpp/_src/_do.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_jr_sdde.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_jr_sde.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_km_sde.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_mpr_sde.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_vep.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/_wc_ode.cpython-310-x86_64-linux-gnu.so +0 -0
- vbi/models/cpp/_src/bold.hpp +303 -0
- vbi/models/cpp/_src/do.hpp +167 -0
- vbi/models/cpp/_src/do.i +17 -0
- vbi/models/cpp/_src/do.py +467 -0
- vbi/models/cpp/_src/do_wrap.cxx +12811 -0
- vbi/models/cpp/_src/jr_sdde.hpp +352 -0
- vbi/models/cpp/_src/jr_sdde.i +19 -0
- vbi/models/cpp/_src/jr_sdde.py +688 -0
- vbi/models/cpp/_src/jr_sdde_wrap.cxx +18718 -0
- vbi/models/cpp/_src/jr_sde.hpp +264 -0
- vbi/models/cpp/_src/jr_sde.i +17 -0
- vbi/models/cpp/_src/jr_sde.py +470 -0
- vbi/models/cpp/_src/jr_sde_wrap.cxx +13406 -0
- vbi/models/cpp/_src/km_sde.hpp +158 -0
- vbi/models/cpp/_src/km_sde.i +19 -0
- vbi/models/cpp/_src/km_sde.py +671 -0
- vbi/models/cpp/_src/km_sde_wrap.cxx +17367 -0
- vbi/models/cpp/_src/makefile +52 -0
- vbi/models/cpp/_src/mpr_sde.hpp +327 -0
- vbi/models/cpp/_src/mpr_sde.i +19 -0
- vbi/models/cpp/_src/mpr_sde.py +711 -0
- vbi/models/cpp/_src/mpr_sde_wrap.cxx +18618 -0
- vbi/models/cpp/_src/utility.hpp +307 -0
- vbi/models/cpp/_src/vep.hpp +171 -0
- vbi/models/cpp/_src/vep.i +16 -0
- vbi/models/cpp/_src/vep.py +464 -0
- vbi/models/cpp/_src/vep_wrap.cxx +12968 -0
- vbi/models/cpp/_src/wc_ode.hpp +294 -0
- vbi/models/cpp/_src/wc_ode.i +19 -0
- vbi/models/cpp/_src/wc_ode.py +686 -0
- vbi/models/cpp/_src/wc_ode_wrap.cxx +24263 -0
- vbi/models/cpp/damp_oscillator.py +143 -0
- vbi/models/cpp/jansen_rit.py +543 -0
- vbi/models/cpp/km.py +187 -0
- vbi/models/cpp/mpr.py +289 -0
- vbi/models/cpp/vep.py +150 -0
- vbi/models/cpp/wc.py +216 -0
- vbi/models/cupy/__init__.py +0 -0
- vbi/models/cupy/bold.py +111 -0
- vbi/models/cupy/ghb.py +284 -0
- vbi/models/cupy/jansen_rit.py +473 -0
- vbi/models/cupy/km.py +224 -0
- vbi/models/cupy/mpr.py +475 -0
- vbi/models/cupy/mpr_modified_bold.py +12 -0
- vbi/models/cupy/utils.py +184 -0
- vbi/models/numba/__init__.py +0 -0
- vbi/models/numba/_ww_EI.py +444 -0
- vbi/models/numba/damp_oscillator.py +162 -0
- vbi/models/numba/ghb.py +208 -0
- vbi/models/numba/mpr.py +383 -0
- vbi/models/pytorch/__init__.py +0 -0
- vbi/models/pytorch/data/default_parameters.npz +0 -0
- vbi/models/pytorch/data/input/ROI_sim.mat +0 -0
- vbi/models/pytorch/data/input/fc_test.csv +68 -0
- vbi/models/pytorch/data/input/fc_train.csv +68 -0
- vbi/models/pytorch/data/input/fc_vali.csv +68 -0
- vbi/models/pytorch/data/input/fcd_test.mat +0 -0
- vbi/models/pytorch/data/input/fcd_test_high_window.mat +0 -0
- vbi/models/pytorch/data/input/fcd_test_low_window.mat +0 -0
- vbi/models/pytorch/data/input/fcd_train.mat +0 -0
- vbi/models/pytorch/data/input/fcd_vali.mat +0 -0
- vbi/models/pytorch/data/input/myelin.csv +68 -0
- vbi/models/pytorch/data/input/rsfc_gradient.csv +68 -0
- vbi/models/pytorch/data/input/run_label_testset.mat +0 -0
- vbi/models/pytorch/data/input/sc_test.csv +68 -0
- vbi/models/pytorch/data/input/sc_train.csv +68 -0
- vbi/models/pytorch/data/input/sc_vali.csv +68 -0
- vbi/models/pytorch/data/obs_kong0.npz +0 -0
- vbi/models/pytorch/ww_sde_kong.py +570 -0
- vbi/models/tvbk/__init__.py +9 -0
- vbi/models/tvbk/tvbk_wrapper.py +166 -0
- vbi/models/tvbk/utils.py +72 -0
- vbi/papers/__init__.py +0 -0
- vbi/papers/pavlides_pcb_2015/pavlides.py +211 -0
- vbi/tests/__init__.py +0 -0
- vbi/tests/_test_mpr_nb.py +36 -0
- vbi/tests/test_features.py +355 -0
- vbi/tests/test_ghb_cupy.py +90 -0
- vbi/tests/test_mpr_cupy.py +49 -0
- vbi/tests/test_mpr_numba.py +84 -0
- vbi/tests/test_suite.py +19 -0
- vbi/utils.py +402 -0
- vbi-0.1.3.dist-info/METADATA +166 -0
- vbi-0.1.3.dist-info/RECORD +121 -0
- vbi-0.1.3.dist-info/WHEEL +5 -0
- vbi-0.1.3.dist-info/licenses/LICENSE +201 -0
- vbi-0.1.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,2124 @@
|
|
1
|
+
import vbi
|
2
|
+
import numpy as np
|
3
|
+
import scipy.signal
|
4
|
+
from scipy.signal import hilbert
|
5
|
+
from scipy.stats import moment, skew, kurtosis
|
6
|
+
import scipy.stats as stats
|
7
|
+
from vbi.feature_extraction.utility import prepare_input_ts
|
8
|
+
from vbi.feature_extraction.features_utils import (
|
9
|
+
km_order,
|
10
|
+
get_fc,
|
11
|
+
get_fcd,
|
12
|
+
matrix_stat,
|
13
|
+
compute_time,
|
14
|
+
init_jvm,
|
15
|
+
nat2bit,
|
16
|
+
kde,
|
17
|
+
gaussian,
|
18
|
+
calc_fft,
|
19
|
+
wavelet,
|
20
|
+
state_duration,
|
21
|
+
seizure_onset_indicator,
|
22
|
+
max_frequency,
|
23
|
+
max_psd,
|
24
|
+
spectral_distance,
|
25
|
+
fundamental_frequency,
|
26
|
+
spectral_centroid,
|
27
|
+
spectral_variation,
|
28
|
+
spectral_kurtosis,
|
29
|
+
median_frequency
|
30
|
+
)
|
31
|
+
|
32
|
+
from typing import List, Tuple, Dict
|
33
|
+
|
34
|
+
try:
|
35
|
+
import ssm
|
36
|
+
except:
|
37
|
+
pass
|
38
|
+
|
39
|
+
try:
|
40
|
+
import jpype as jp
|
41
|
+
except:
|
42
|
+
pass
|
43
|
+
|
44
|
+
|
45
|
+
def abs_energy(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
46
|
+
"""Computes the absolute energy of the time serie.
|
47
|
+
|
48
|
+
>>> abs_energy([1, 2, 3, 4, 5])
|
49
|
+
(array([55]), ['abs_energy_0'])
|
50
|
+
|
51
|
+
Parameters
|
52
|
+
----------
|
53
|
+
ts : nd-arrays [n_regions x n_samples]
|
54
|
+
Input from which the area under the curve is computed
|
55
|
+
indices: list of int
|
56
|
+
Indices of the time series to compute the feature
|
57
|
+
|
58
|
+
Returns
|
59
|
+
-------
|
60
|
+
values: list of float
|
61
|
+
Absolute energy
|
62
|
+
labels: list of str
|
63
|
+
Labels of the features
|
64
|
+
|
65
|
+
"""
|
66
|
+
|
67
|
+
info, ts = prepare_input_ts(ts, indices)
|
68
|
+
if not info:
|
69
|
+
if verbose:
|
70
|
+
print("Error in abs_energy")
|
71
|
+
return [np.nan], [f"abs_energy_{0}"]
|
72
|
+
else:
|
73
|
+
values = np.sum(np.abs(ts) ** 2, axis=1)
|
74
|
+
labels = [f"abs_energy_{i}" for i in range(len(values))]
|
75
|
+
|
76
|
+
return values, labels
|
77
|
+
|
78
|
+
|
79
|
+
def average_power(ts: np.ndarray, fs: float = 1.0, indices: List[int] = None, verbose=False):
|
80
|
+
"""Computes the average power of the time serie.
|
81
|
+
|
82
|
+
>>> average_power([1, 2, 3, 4, 5], 1)
|
83
|
+
(array([13.75]), ['average_power_0'])
|
84
|
+
|
85
|
+
Parameters
|
86
|
+
----------
|
87
|
+
ts : nd-arrays [n_regions x n_samples]
|
88
|
+
Input from which the area under the curve is computed
|
89
|
+
fs : float
|
90
|
+
Sampling frequency
|
91
|
+
indices: list of int
|
92
|
+
Indices of the time series to compute the feature
|
93
|
+
|
94
|
+
Returns
|
95
|
+
-------
|
96
|
+
values: list of float
|
97
|
+
Average power
|
98
|
+
labels: list of str
|
99
|
+
Labels of the features
|
100
|
+
|
101
|
+
"""
|
102
|
+
|
103
|
+
info, ts = prepare_input_ts(ts, indices)
|
104
|
+
if not info:
|
105
|
+
if verbose:
|
106
|
+
print("Error in average_power")
|
107
|
+
return [np.nan], [f"average_power_{0}"]
|
108
|
+
else:
|
109
|
+
|
110
|
+
times = compute_time(ts[0], fs)
|
111
|
+
values = np.sum(ts**2, axis=1) / (times[-1] - times[0])
|
112
|
+
labels = [f"average_power_{i}" for i in range(len(values))]
|
113
|
+
return values, labels
|
114
|
+
|
115
|
+
|
116
|
+
def auc(
|
117
|
+
ts: np.ndarray, dx: float = None, x: np.ndarray = None, indices: List[int] = None, verbose=False
|
118
|
+
):
|
119
|
+
"""Computes the area under the curve of the signal computed with trapezoid rule.
|
120
|
+
|
121
|
+
>>> auc(np.array([[1, 2, 3], [4, 5, 6]]), None, np.array([0, 1, 2]))
|
122
|
+
(array([ 4., 10.]), ['auc_0', 'auc_1'])
|
123
|
+
|
124
|
+
Parameters
|
125
|
+
----------
|
126
|
+
ts : nd-arrays [n_regions x n_samples]
|
127
|
+
Input from which the area under the curve is computed
|
128
|
+
dx: float
|
129
|
+
Spacing between values
|
130
|
+
x: array_like, optional
|
131
|
+
x values of the time series
|
132
|
+
indices: list of int
|
133
|
+
Indices of the time series to compute the feature
|
134
|
+
|
135
|
+
Returns
|
136
|
+
-------
|
137
|
+
list of float
|
138
|
+
The area under the curve value
|
139
|
+
labels: list of str
|
140
|
+
Labels of the features
|
141
|
+
|
142
|
+
"""
|
143
|
+
|
144
|
+
info, ts = prepare_input_ts(ts, indices)
|
145
|
+
if not info:
|
146
|
+
if verbose:
|
147
|
+
print("Error in auc")
|
148
|
+
return [np.nan], ["auc_0"]
|
149
|
+
|
150
|
+
if dx is None:
|
151
|
+
dx = 1
|
152
|
+
values = np.trapz(ts, x=x, dx=dx, axis=1)
|
153
|
+
labels = [f"auc_{i}" for i in range(len(values))]
|
154
|
+
|
155
|
+
return values, labels
|
156
|
+
|
157
|
+
|
158
|
+
def auc_lim(
|
159
|
+
ts: np.ndarray,
|
160
|
+
dx: float = None,
|
161
|
+
x: np.ndarray = None,
|
162
|
+
xlim: List[Tuple[float, float]] = None,
|
163
|
+
indices: List[int] = None,
|
164
|
+
verbose=False
|
165
|
+
):
|
166
|
+
"""
|
167
|
+
Compute the area under the curve for a given time series within a given limit
|
168
|
+
|
169
|
+
>>> auc_lim(np.array([[1, 2, 3], [4, 5, 6]]), None, None, [(0, 1), (1, 2)])
|
170
|
+
([1.5, 4.5, 2.5, 5.5], ['auc_lim_0', 'auc_lim_1', 'auc_lim_2', 'auc_lim_3'])
|
171
|
+
|
172
|
+
Parameters
|
173
|
+
----------
|
174
|
+
ts : nd-arrays [n_regions x n_samples]
|
175
|
+
Input from which the area under the curve is computed
|
176
|
+
dx: float
|
177
|
+
Spacing between values
|
178
|
+
x: array_like
|
179
|
+
x values of the time series
|
180
|
+
xlim: list of tuples
|
181
|
+
The limits of the time series
|
182
|
+
indices: list of int
|
183
|
+
Indices of the time series to compute the feature
|
184
|
+
|
185
|
+
Returns
|
186
|
+
-------
|
187
|
+
list of float
|
188
|
+
The area under the curve value
|
189
|
+
labels: list of str
|
190
|
+
Labels of the features
|
191
|
+
|
192
|
+
"""
|
193
|
+
|
194
|
+
info, ts = prepare_input_ts(ts, indices)
|
195
|
+
if not info:
|
196
|
+
if verbose:
|
197
|
+
print("Error in auc_lim")
|
198
|
+
return [np.nan], ["auc_lim_0"]
|
199
|
+
|
200
|
+
if x is None:
|
201
|
+
x = np.arange(0, ts.shape[1])
|
202
|
+
else:
|
203
|
+
x = np.array(x)
|
204
|
+
assert x.shape[0] == ts.shape[1], "x and ts must have the same length"
|
205
|
+
|
206
|
+
if xlim is None:
|
207
|
+
xlim = [(x[0], x[-1])]
|
208
|
+
|
209
|
+
if not isinstance(xlim[0], (list, tuple)):
|
210
|
+
xlim = [xlim]
|
211
|
+
|
212
|
+
values = []
|
213
|
+
for i, (xmin, xmax) in enumerate(xlim):
|
214
|
+
idx = np.where((x >= xmin) & (x <= xmax))[0]
|
215
|
+
if len(idx) == 0:
|
216
|
+
continue
|
217
|
+
values.extend(np.trapz(ts[:, idx], x=x[idx], dx=dx, axis=1))
|
218
|
+
labels = [f"auc_lim_{i}" for i in range(len(values))]
|
219
|
+
|
220
|
+
return values, labels
|
221
|
+
|
222
|
+
|
223
|
+
def calc_var(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
224
|
+
"""Computes variance of the time series.
|
225
|
+
|
226
|
+
>>> calc_var(np.array([[1, 2, 3], [4, 5, 6]]))
|
227
|
+
(array([0.66666667, 0.66666667]), ['var_0', 'var_1'])
|
228
|
+
|
229
|
+
Parameters
|
230
|
+
----------
|
231
|
+
ts : nd-array [n_regions x n_samples]
|
232
|
+
Input from which var is computed
|
233
|
+
indices: list of int
|
234
|
+
Indices of the time series to compute the feature
|
235
|
+
|
236
|
+
Returns
|
237
|
+
-------
|
238
|
+
values: array-like
|
239
|
+
variance of the time series
|
240
|
+
labels: array-like
|
241
|
+
labels of the features
|
242
|
+
|
243
|
+
"""
|
244
|
+
info, ts = prepare_input_ts(ts, indices)
|
245
|
+
if not info:
|
246
|
+
if verbose:
|
247
|
+
print("Error in calc_var")
|
248
|
+
return [np.nan], ["var_0"]
|
249
|
+
|
250
|
+
values = np.var(ts, axis=1)
|
251
|
+
labels = [f"var_{i}" for i in range(len(values))]
|
252
|
+
|
253
|
+
return values, labels
|
254
|
+
|
255
|
+
|
256
|
+
def calc_std(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
257
|
+
"""Computes standard deviation of the time serie.
|
258
|
+
|
259
|
+
>>> calc_std(np.array([[1, 2, 3], [4, 5, 6]]))
|
260
|
+
(array([0.81649658, 0.81649658]), ['std_0', 'std_1'])
|
261
|
+
|
262
|
+
Parameters
|
263
|
+
----------
|
264
|
+
ts : nd-array [n_regions x n_samples]
|
265
|
+
Input from which std is computed
|
266
|
+
indices: list of int
|
267
|
+
Indices of the time series to compute the feature
|
268
|
+
|
269
|
+
Returns
|
270
|
+
-------
|
271
|
+
values: array-like
|
272
|
+
std of the time series
|
273
|
+
labels: array-like
|
274
|
+
labels of the features
|
275
|
+
"""
|
276
|
+
|
277
|
+
info, ts = prepare_input_ts(ts, indices)
|
278
|
+
if not info:
|
279
|
+
if verbose:
|
280
|
+
print("Error in calc_std")
|
281
|
+
return [np.nan], [f"std_{0}"]
|
282
|
+
else:
|
283
|
+
values = np.std(ts, axis=1)
|
284
|
+
labels = [f"std_{i}" for i in range(len(values))]
|
285
|
+
return values, labels
|
286
|
+
|
287
|
+
|
288
|
+
def calc_mean(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
289
|
+
"""Computes median of the time serie.
|
290
|
+
|
291
|
+
>>> calc_mean(np.array([[1, 2, 3], [4, 5, 6]]))
|
292
|
+
(array([2., 5.]), ['mean_0', 'mean_1'])
|
293
|
+
|
294
|
+
Parameters
|
295
|
+
----------
|
296
|
+
ts : nd-array [n_regions x n_samples]
|
297
|
+
Input from which median is computed
|
298
|
+
indices: list of int
|
299
|
+
Indices of the time series to compute the feature
|
300
|
+
|
301
|
+
Returns
|
302
|
+
-------
|
303
|
+
values: array-like
|
304
|
+
mean of the time series
|
305
|
+
labels: array-like
|
306
|
+
labels of the features
|
307
|
+
|
308
|
+
"""
|
309
|
+
|
310
|
+
info, ts = prepare_input_ts(ts, indices)
|
311
|
+
if not info:
|
312
|
+
if verbose:
|
313
|
+
print("Error in calc_mean")
|
314
|
+
return [np.nan], [f"mean_{0}"]
|
315
|
+
else:
|
316
|
+
values = np.mean(ts, axis=1)
|
317
|
+
labels = [f"mean_{i}" for i in range(len(values))]
|
318
|
+
return values, labels
|
319
|
+
|
320
|
+
|
321
|
+
def calc_centroid(ts: np.ndarray, fs: float, indices: List[int] = None, verbose=False):
|
322
|
+
"""Computes the centroid along the time axis.
|
323
|
+
|
324
|
+
Parameters
|
325
|
+
----------
|
326
|
+
signal : nd-array
|
327
|
+
Input from which centroid is computed
|
328
|
+
fs: int
|
329
|
+
Signal sampling frequency
|
330
|
+
indices: list of int
|
331
|
+
Indices of the time series to compute the feature
|
332
|
+
|
333
|
+
Returns
|
334
|
+
-------
|
335
|
+
float
|
336
|
+
Temporal centroid
|
337
|
+
|
338
|
+
"""
|
339
|
+
info, ts = prepare_input_ts(ts, indices)
|
340
|
+
if not info:
|
341
|
+
if verbose:
|
342
|
+
print("Error in calc_centroid")
|
343
|
+
return [np.nan], [f"centroid_{0}"]
|
344
|
+
else:
|
345
|
+
tol = 1e-10
|
346
|
+
r, c = ts.shape
|
347
|
+
centroid = np.zeros(r)
|
348
|
+
time = compute_time(ts[0], fs)
|
349
|
+
energy = ts**2
|
350
|
+
t_energy = np.dot(time, energy.T)
|
351
|
+
energy_sum = np.sum(energy, axis=1)
|
352
|
+
ind_nonzero = (np.abs(energy_sum) > tol) | (np.abs(t_energy) > tol)
|
353
|
+
centroid[ind_nonzero] = t_energy[ind_nonzero] / energy_sum[ind_nonzero]
|
354
|
+
labels = [f"centroid_{i}" for i in range(len(centroid))]
|
355
|
+
|
356
|
+
return centroid, labels
|
357
|
+
|
358
|
+
|
359
|
+
def calc_kurtosis(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
360
|
+
"""
|
361
|
+
Computes the kurtosis of the time series.
|
362
|
+
|
363
|
+
Parameters
|
364
|
+
----------
|
365
|
+
ts : nd-array [n_regions x n_samples]
|
366
|
+
Input from which kurtosis is computed
|
367
|
+
indices: list of int
|
368
|
+
Indices of the time series to compute the feature
|
369
|
+
|
370
|
+
Returns
|
371
|
+
-------
|
372
|
+
values: array-like
|
373
|
+
kurtosis of the time series
|
374
|
+
labels: array-like
|
375
|
+
labels of the features
|
376
|
+
|
377
|
+
"""
|
378
|
+
|
379
|
+
info, ts = prepare_input_ts(ts, indices)
|
380
|
+
if not info:
|
381
|
+
if verbose:
|
382
|
+
print("Error in calc_kurtosis")
|
383
|
+
return [np.nan], [f"kurtosis_{0}"]
|
384
|
+
else:
|
385
|
+
values = kurtosis(ts, axis=1)
|
386
|
+
labels = [f"kurtosis_{i}" for i in range(len(values))]
|
387
|
+
return values, labels
|
388
|
+
|
389
|
+
|
390
|
+
def calc_skewness(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
391
|
+
"""
|
392
|
+
Computes the skewness of the time series.
|
393
|
+
|
394
|
+
Parameters
|
395
|
+
----------
|
396
|
+
ts : nd-array [n_regions x n_samples]
|
397
|
+
Input from which skewness is computed
|
398
|
+
|
399
|
+
Returns
|
400
|
+
-------
|
401
|
+
values: array-like
|
402
|
+
skewness of the time series
|
403
|
+
labels: array-like
|
404
|
+
labels of the features
|
405
|
+
|
406
|
+
"""
|
407
|
+
|
408
|
+
info, n = prepare_input_ts(ts, indices)
|
409
|
+
if not info:
|
410
|
+
if verbose:
|
411
|
+
print("Error in calc_skewness")
|
412
|
+
return [np.nan], [f"skewness_{0}"]
|
413
|
+
else:
|
414
|
+
values = skew(ts, axis=1)
|
415
|
+
labels = [f"skewness_{i}" for i in range(len(values))]
|
416
|
+
return values, labels
|
417
|
+
|
418
|
+
|
419
|
+
def calc_max(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
420
|
+
"""
|
421
|
+
Computes the maximum of the time series.
|
422
|
+
|
423
|
+
Parameters
|
424
|
+
----------
|
425
|
+
ts : nd-array [n_regions x n_samples]
|
426
|
+
Input from which maximum is computed
|
427
|
+
|
428
|
+
Returns
|
429
|
+
-------
|
430
|
+
values: array-like
|
431
|
+
maximum of the time series
|
432
|
+
labels: array-like
|
433
|
+
labels of the features
|
434
|
+
|
435
|
+
"""
|
436
|
+
|
437
|
+
info, ts = prepare_input_ts(ts, indices)
|
438
|
+
if not info:
|
439
|
+
if verbose:
|
440
|
+
print("Error in calc_max")
|
441
|
+
return [np.nan], [f"max_{0}"]
|
442
|
+
else:
|
443
|
+
values = np.max(ts, axis=1)
|
444
|
+
labels = [f"max_{i}" for i in range(len(values))]
|
445
|
+
return values, labels
|
446
|
+
|
447
|
+
|
448
|
+
def calc_min(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
449
|
+
"""
|
450
|
+
Computes the minimum of the time series.
|
451
|
+
|
452
|
+
Parameters
|
453
|
+
----------
|
454
|
+
ts : nd-array [n_regions x n_samples]
|
455
|
+
Input from which minimum is computed
|
456
|
+
indices: list of int
|
457
|
+
Indices of the time series to compute the feature
|
458
|
+
|
459
|
+
Returns
|
460
|
+
-------
|
461
|
+
values: array-like
|
462
|
+
minimum of the time series
|
463
|
+
labels: array-like
|
464
|
+
labels of the features
|
465
|
+
|
466
|
+
"""
|
467
|
+
|
468
|
+
info, ts = prepare_input_ts(ts, indices)
|
469
|
+
if not info:
|
470
|
+
if verbose:
|
471
|
+
print("Error in calc_min")
|
472
|
+
return [np.nan], [f"min_{0}"]
|
473
|
+
else:
|
474
|
+
values = np.min(ts, axis=1)
|
475
|
+
labels = [f"min_{i}" for i in range(len(values))]
|
476
|
+
return values, labels
|
477
|
+
|
478
|
+
|
479
|
+
def calc_median(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
480
|
+
"""
|
481
|
+
Computes the median of the time series.
|
482
|
+
|
483
|
+
Parameters
|
484
|
+
----------
|
485
|
+
ts : nd-array [n_regions x n_samples]
|
486
|
+
Input from which median is computed
|
487
|
+
indices: list of int
|
488
|
+
Indices of the time series to compute the feature
|
489
|
+
|
490
|
+
Returns
|
491
|
+
-------
|
492
|
+
values: array-like
|
493
|
+
median of the time series
|
494
|
+
labels: array-like
|
495
|
+
labels of the features
|
496
|
+
"""
|
497
|
+
|
498
|
+
info, ts = prepare_input_ts(ts, indices)
|
499
|
+
if not info:
|
500
|
+
if verbose:
|
501
|
+
print("Error in calc_median")
|
502
|
+
return [np.nan], [f"median_{0}"]
|
503
|
+
else:
|
504
|
+
values = np.median(ts, axis=1)
|
505
|
+
labels = [f"median_{i}" for i in range(len(values))]
|
506
|
+
return values, labels
|
507
|
+
|
508
|
+
|
509
|
+
def mean_abs_dev(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
510
|
+
"""
|
511
|
+
Computes the mean absolute deviation of the time series.
|
512
|
+
|
513
|
+
Parameters
|
514
|
+
----------
|
515
|
+
ts : nd-array [n_regions x n_samples]
|
516
|
+
Input from which mean absolute deviation is computed
|
517
|
+
|
518
|
+
Returns
|
519
|
+
-------
|
520
|
+
values: array-like
|
521
|
+
mean absolute deviation of the time series
|
522
|
+
labels: array-like
|
523
|
+
labels of the features
|
524
|
+
|
525
|
+
"""
|
526
|
+
|
527
|
+
info, ts = prepare_input_ts(ts, indices)
|
528
|
+
if not info:
|
529
|
+
if verbose:
|
530
|
+
print("Error in mean_abs_dev")
|
531
|
+
return [np.nan], [f"mean_abs_dev_{0}"]
|
532
|
+
else:
|
533
|
+
values = np.mean(np.abs(ts - np.mean(ts, axis=1, keepdims=True)), axis=1)
|
534
|
+
labels = [f"mean_abs_dev_{i}" for i in range(len(values))]
|
535
|
+
return values, labels
|
536
|
+
|
537
|
+
|
538
|
+
def median_abs_dev(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
539
|
+
"""
|
540
|
+
Computes the median absolute deviation of the time series.
|
541
|
+
|
542
|
+
Parameters
|
543
|
+
----------
|
544
|
+
ts : nd-array [n_regions x n_samples]
|
545
|
+
Input from which median absolute deviation is computed
|
546
|
+
indices: list of int
|
547
|
+
Indices of the time series to compute the feature
|
548
|
+
|
549
|
+
Returns
|
550
|
+
-------
|
551
|
+
values: array-like
|
552
|
+
median absolute deviation of the time series
|
553
|
+
labels: array-like
|
554
|
+
labels of the features
|
555
|
+
|
556
|
+
"""
|
557
|
+
|
558
|
+
info, ts = prepare_input_ts(ts, indices)
|
559
|
+
if not info:
|
560
|
+
if verbose:
|
561
|
+
print("Error in median_abs_dev")
|
562
|
+
return [np.nan], [f"median_abs_dev_{0}"]
|
563
|
+
else:
|
564
|
+
values = np.median(np.abs(ts - np.median(ts, axis=1, keepdims=True)), axis=1)
|
565
|
+
labels = [f"median_abs_dev_{i}" for i in range(len(values))]
|
566
|
+
return values, labels
|
567
|
+
|
568
|
+
|
569
|
+
def rms(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
570
|
+
"""
|
571
|
+
Computes the root mean square of the time series.
|
572
|
+
|
573
|
+
Parameters
|
574
|
+
----------
|
575
|
+
ts : nd-array [n_regions x n_samples]
|
576
|
+
Input from which root mean square is computed
|
577
|
+
indices: list of int
|
578
|
+
Indices of the time series to compute the feature
|
579
|
+
|
580
|
+
Returns
|
581
|
+
-------
|
582
|
+
values: array-like
|
583
|
+
root mean square of the time series
|
584
|
+
labels: array-like
|
585
|
+
labels of the features
|
586
|
+
|
587
|
+
"""
|
588
|
+
|
589
|
+
info, ts = prepare_input_ts(ts, indices)
|
590
|
+
if not info:
|
591
|
+
if verbose:
|
592
|
+
print("Error in rms")
|
593
|
+
return [np.nan], [f"rms_{0}"]
|
594
|
+
else:
|
595
|
+
values = np.sqrt(np.mean(ts**2, axis=1))
|
596
|
+
labels = [f"rms_{i}" for i in range(len(values))]
|
597
|
+
return values, labels
|
598
|
+
|
599
|
+
|
600
|
+
def interq_range(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
601
|
+
"""
|
602
|
+
Computes the interquartile range of the time series.
|
603
|
+
|
604
|
+
Parameters
|
605
|
+
----------
|
606
|
+
ts : nd-array [n_regions x n_samples]
|
607
|
+
Input from which interquartile range is computed
|
608
|
+
indices: list of int
|
609
|
+
Indices of the time series to compute the feature
|
610
|
+
|
611
|
+
Returns
|
612
|
+
-------
|
613
|
+
values: array-like
|
614
|
+
interquartile range of the time series
|
615
|
+
labels: array-like
|
616
|
+
labels of the features
|
617
|
+
|
618
|
+
"""
|
619
|
+
|
620
|
+
info, ts = prepare_input_ts(ts, indices)
|
621
|
+
if not info:
|
622
|
+
if verbose:
|
623
|
+
print("Error in interq_range")
|
624
|
+
return [np.nan], [f"interq_range_{0}"]
|
625
|
+
else:
|
626
|
+
values = np.subtract(*np.percentile(ts, [75, 25], axis=1))
|
627
|
+
labels = [f"interq_range_{i}" for i in range(len(values))]
|
628
|
+
return values, labels
|
629
|
+
|
630
|
+
|
631
|
+
def zero_crossing(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
632
|
+
"""
|
633
|
+
Computes the number of zero crossings of the time series.
|
634
|
+
|
635
|
+
Parameters
|
636
|
+
----------
|
637
|
+
ts : nd-array [n_regions x n_samples]
|
638
|
+
Input from which number of zero crossings is computed
|
639
|
+
indices: list of int
|
640
|
+
Indices of the time series to compute the feature
|
641
|
+
|
642
|
+
Returns
|
643
|
+
-------
|
644
|
+
values: array-like
|
645
|
+
number of zero crossings of the time series
|
646
|
+
labels: array-like
|
647
|
+
labels of the features
|
648
|
+
|
649
|
+
"""
|
650
|
+
info, ts = prepare_input_ts(ts, indices)
|
651
|
+
if not info:
|
652
|
+
if verbose:
|
653
|
+
print("Error in zero_crossing")
|
654
|
+
return [np.nan], [f"zero_crossing_{0}"]
|
655
|
+
else:
|
656
|
+
values = np.array([np.sum(np.diff(np.sign(y_i)) != 0) for y_i in ts], dtype=int)
|
657
|
+
labels = [f"zero_crossing_{i}" for i in range(len(values))]
|
658
|
+
return values, labels
|
659
|
+
|
660
|
+
|
661
|
+
def seizure_onset(ts: np.ndarray,
|
662
|
+
threshold: float = 0.02,
|
663
|
+
indices: List[int] = None, verbose=False):
|
664
|
+
'''
|
665
|
+
Computes the seizure onset of the time series.
|
666
|
+
|
667
|
+
Parameters
|
668
|
+
----------
|
669
|
+
ts : nd-array [n_regions x n_samples]
|
670
|
+
Input from which number of zero crossings is computed
|
671
|
+
indices: list of int
|
672
|
+
Indices of the time series to compute the feature
|
673
|
+
|
674
|
+
Returns
|
675
|
+
-------
|
676
|
+
values: array-like
|
677
|
+
index of the onset of the seizures in the time series, zero if no onset in each region
|
678
|
+
labels: array-like
|
679
|
+
labels of the features
|
680
|
+
'''
|
681
|
+
|
682
|
+
info, ts = prepare_input_ts(ts, indices)
|
683
|
+
if not info:
|
684
|
+
if verbose:
|
685
|
+
print("Error in zero_crossing")
|
686
|
+
return [np.nan], [f"seizure_onset_{0}"]
|
687
|
+
else:
|
688
|
+
values = seizure_onset_indicator(ts, threshold)
|
689
|
+
labels = [f"seizure_onset_{i}" for i in range(len(values))]
|
690
|
+
return values, labels
|
691
|
+
|
692
|
+
|
693
|
+
|
694
|
+
|
695
|
+
# def calc_ress(
|
696
|
+
# ts: np.ndarray, percentile: Union[int, float] = 95, indices: List[int] = None
|
697
|
+
# ):
|
698
|
+
# """
|
699
|
+
# Calculates Residual Sum of Squares (RSS) with given percentile
|
700
|
+
|
701
|
+
# Parameters
|
702
|
+
# ----------
|
703
|
+
# ts : nd-array [n_regions x n_samples]
|
704
|
+
# Input time seris
|
705
|
+
# percentile : float
|
706
|
+
# Percentile of RSS
|
707
|
+
# indices: list of int
|
708
|
+
# Indices of the time series to compute the feature
|
709
|
+
|
710
|
+
# Returns
|
711
|
+
# -------
|
712
|
+
# values: array-like
|
713
|
+
# RSS of the time series
|
714
|
+
# labels: array-like
|
715
|
+
# labels of the features
|
716
|
+
# """
|
717
|
+
|
718
|
+
# info, ts = prepare_input_ts(ts, indices)
|
719
|
+
# if not info:
|
720
|
+
# return [np.nan], [f"ress_{0}"]
|
721
|
+
# else:
|
722
|
+
# nn, nt = ts.shape
|
723
|
+
# rss = np.zeros(nt)
|
724
|
+
# for t in range(nt):
|
725
|
+
# z = np.power(np.outer(ts[:, t], ts[:, t]), 2)
|
726
|
+
# rss[t] = np.sqrt(np.einsum("ij->", z))
|
727
|
+
# return np.percentile(rss, percentile), ["ress"]
|
728
|
+
|
729
|
+
|
730
|
+
def kop(ts: np.ndarray, indices: List[int] = None, verbose=False, extract_phase=False):
|
731
|
+
"""
|
732
|
+
Calculate the Kuramoto order parameter (KOP)
|
733
|
+
|
734
|
+
"""
|
735
|
+
|
736
|
+
info, ts = prepare_input_ts(ts, indices)
|
737
|
+
if not info:
|
738
|
+
if verbose:
|
739
|
+
print("Error in kop")
|
740
|
+
return [np.nan], ["kop"]
|
741
|
+
else:
|
742
|
+
if extract_phase:
|
743
|
+
analytic_signal = hilbert(ts, axis=1)
|
744
|
+
amplitude_envelope = np.abs(analytic_signal)
|
745
|
+
instantaneous_phase = np.unwrap(np.angle(analytic_signal))
|
746
|
+
R = km_order(instantaneous_phase, indices=indices, avg=True)
|
747
|
+
else:
|
748
|
+
R = km_order(ts, indices=indices, avg=True)
|
749
|
+
return R, ["kop"]
|
750
|
+
|
751
|
+
|
752
|
+
def calc_moments(
|
753
|
+
ts: np.ndarray, indices: List[int] = None, orders: List[int] = [2, 3, 4, 5, 6], verbose=False
|
754
|
+
):
|
755
|
+
"""
|
756
|
+
Computes the moments of the time series.
|
757
|
+
|
758
|
+
Parameters
|
759
|
+
----------
|
760
|
+
ts : nd-array [n_regions x n_samples]
|
761
|
+
Input from which moments are computed
|
762
|
+
orders: list
|
763
|
+
List of orders of the moments
|
764
|
+
|
765
|
+
Returns
|
766
|
+
-------
|
767
|
+
values: array-like
|
768
|
+
moments of the time series
|
769
|
+
labels: array-like
|
770
|
+
labels of the features
|
771
|
+
|
772
|
+
"""
|
773
|
+
|
774
|
+
info, ts = prepare_input_ts(ts, indices)
|
775
|
+
if not info:
|
776
|
+
if verbose:
|
777
|
+
print("Error in calc_moments")
|
778
|
+
return [np.nan], ["moments"]
|
779
|
+
else:
|
780
|
+
labels = []
|
781
|
+
values = np.array([])
|
782
|
+
for i in orders:
|
783
|
+
v = moment(ts, moment=i, axis=1)
|
784
|
+
values = np.append(values, v)
|
785
|
+
labels.extend([f"moments_{i}_{j}" for j in range(len(v))])
|
786
|
+
|
787
|
+
return values, labels
|
788
|
+
|
789
|
+
|
790
|
+
def calc_envelope(
|
791
|
+
ts: np.ndarray,
|
792
|
+
indices: List[int] = None,
|
793
|
+
features: List[str] = ["mean", "std", "median", "max", "min"],
|
794
|
+
verbose=False,
|
795
|
+
):
|
796
|
+
"""
|
797
|
+
calculate some statistics on envelope of the time series using hilbert transform
|
798
|
+
"""
|
799
|
+
|
800
|
+
from numpy import mean, std, median, max, min
|
801
|
+
|
802
|
+
info, ts = prepare_input_ts(ts, indices)
|
803
|
+
if not info:
|
804
|
+
if verbose:
|
805
|
+
print("Error in calc_envelope")
|
806
|
+
return [np.nan], ["envelope"]
|
807
|
+
else:
|
808
|
+
analytic_signal = hilbert(ts, axis=1)
|
809
|
+
amplitude_envelope = np.abs(analytic_signal)
|
810
|
+
instantaneous_phase = np.unwrap(np.angle(analytic_signal))
|
811
|
+
|
812
|
+
labels = []
|
813
|
+
values = np.array([])
|
814
|
+
|
815
|
+
for f in features:
|
816
|
+
v = np.append(values, eval(f"{f}(amplitude_envelope, axis=1)"))
|
817
|
+
l = [f"env_amp_{f}_{j}" for j in range(len(v))]
|
818
|
+
values = np.append(values, v)
|
819
|
+
labels.extend(l)
|
820
|
+
|
821
|
+
for f in features:
|
822
|
+
v = eval(f"{f}(instantaneous_phase, axis=1)")
|
823
|
+
l = [f"env_ph_{f}_{j}" for j in range(len(v))]
|
824
|
+
values = np.append(values, v)
|
825
|
+
labels.extend(l)
|
826
|
+
|
827
|
+
return values, labels
|
828
|
+
|
829
|
+
|
830
|
+
def fc_sum(x: np.ndarray, positive=False, masks: Dict[str, np.ndarray] = None, verbose=False):
|
831
|
+
"""
|
832
|
+
Calculate the sum of functional connectivity (FC)
|
833
|
+
|
834
|
+
Parameters
|
835
|
+
----------
|
836
|
+
ts : nd-array [n_regions x n_samples]
|
837
|
+
Input from which var is computed
|
838
|
+
|
839
|
+
Returns
|
840
|
+
-------
|
841
|
+
result: float
|
842
|
+
sum of functional connectivity
|
843
|
+
"""
|
844
|
+
|
845
|
+
label = "fc_sum"
|
846
|
+
|
847
|
+
info, ts = prepare_input_ts(x)
|
848
|
+
if not info:
|
849
|
+
if verbose:
|
850
|
+
print("Error in fc_sum")
|
851
|
+
return [np.nan], [label]
|
852
|
+
if ts.shape[0] < 2:
|
853
|
+
return [np.nan], [label]
|
854
|
+
nn = ts.shape[0]
|
855
|
+
|
856
|
+
if masks is None:
|
857
|
+
masks = {"full": np.ones((nn, nn))}
|
858
|
+
|
859
|
+
for key in masks.keys():
|
860
|
+
assert (
|
861
|
+
masks[key].shape[0] == nn
|
862
|
+
), "mask size must be equal to the number of regions"
|
863
|
+
|
864
|
+
fc = np.corrcoef(x)
|
865
|
+
if positive:
|
866
|
+
fc = fc * (fc > 0)
|
867
|
+
|
868
|
+
values = np.array([])
|
869
|
+
for key in masks.keys():
|
870
|
+
mask = masks[key]
|
871
|
+
fc = fc * mask
|
872
|
+
v = np.sum(np.abs(fc)) - np.trace(np.abs(fc))
|
873
|
+
values = np.append(values, v)
|
874
|
+
labels = [f"{label}_{key}" for key in masks.keys()]
|
875
|
+
|
876
|
+
return values, labels
|
877
|
+
|
878
|
+
|
879
|
+
def fc_stat(
|
880
|
+
ts: np.ndarray,
|
881
|
+
k: int = 0,
|
882
|
+
positive: bool = False,
|
883
|
+
eigenvalues: bool = True,
|
884
|
+
pca_num_components: int = 3,
|
885
|
+
fc_function: str = "corrcoef",
|
886
|
+
masks: Dict[str, np.ndarray] = None,
|
887
|
+
quantiles: List[float] = [0.05, 0.25, 0.5, 0.75, 0.95],
|
888
|
+
features: List[str] = ["sum", "max", "min", "mean", "std", "skew", "kurtosis"],
|
889
|
+
verbose=False,
|
890
|
+
):
|
891
|
+
"""
|
892
|
+
extract features from functional connectivity (FC)
|
893
|
+
|
894
|
+
Parameters
|
895
|
+
----------
|
896
|
+
|
897
|
+
ts: np.ndarray [n_regions, n_samples]
|
898
|
+
input array
|
899
|
+
k: int
|
900
|
+
to remove up to kth diagonal of FC matrix
|
901
|
+
pca_num_components: int
|
902
|
+
number of components for PCA
|
903
|
+
positive: bool
|
904
|
+
if True, ignore negative values of fc elements
|
905
|
+
masks: dict
|
906
|
+
dictionary of masks
|
907
|
+
features: list of str
|
908
|
+
list of features to be extracted
|
909
|
+
quantiles: list of float
|
910
|
+
list of quantiles, set 0 to ignore
|
911
|
+
eigenvalues: bool
|
912
|
+
if True, extract features from eigenvalues
|
913
|
+
fc_function: str
|
914
|
+
functional connectivity function: 'corrcoef' or 'cov'
|
915
|
+
|
916
|
+
Returns
|
917
|
+
-------
|
918
|
+
stats: np.ndarray (1d)
|
919
|
+
feature values
|
920
|
+
labels: list of str
|
921
|
+
feature labels
|
922
|
+
"""
|
923
|
+
|
924
|
+
info, ts = prepare_input_ts(ts)
|
925
|
+
if not info:
|
926
|
+
if verbose:
|
927
|
+
print("Error in fc_")
|
928
|
+
return [np.nan], ["fc_0"]
|
929
|
+
|
930
|
+
nn = ts.shape[0]
|
931
|
+
if nn < 2:
|
932
|
+
return [np.nan], ["fc_0"]
|
933
|
+
|
934
|
+
if masks is None:
|
935
|
+
masks = {"full": np.ones((nn, nn))}
|
936
|
+
|
937
|
+
for key in masks.keys():
|
938
|
+
assert (
|
939
|
+
masks[key].shape[0] == nn
|
940
|
+
), "mask size must be equal to the number of regions"
|
941
|
+
|
942
|
+
Values = []
|
943
|
+
Labels = []
|
944
|
+
|
945
|
+
fc = get_fc(ts, masks=masks, fc_fucntion=fc_function, positive=positive)
|
946
|
+
|
947
|
+
for key in fc.keys():
|
948
|
+
values, labels = matrix_stat(
|
949
|
+
fc[key],
|
950
|
+
k=k,
|
951
|
+
features=features,
|
952
|
+
quantiles=quantiles,
|
953
|
+
eigenvalues=eigenvalues,
|
954
|
+
pca_num_components=pca_num_components,
|
955
|
+
)
|
956
|
+
labels = [f"fc_{key}_{label}" for label in labels]
|
957
|
+
Values.extend(values)
|
958
|
+
Labels.extend(labels)
|
959
|
+
|
960
|
+
return Values, Labels
|
961
|
+
|
962
|
+
|
963
|
+
def fc_homotopic(
|
964
|
+
ts: np.ndarray, average: bool = False, positive: bool = True, fc_function="corrcoef", verbose=False
|
965
|
+
):
|
966
|
+
"""
|
967
|
+
Calculate the homotopic connectivity vector of a given brain activity
|
968
|
+
|
969
|
+
Parameters
|
970
|
+
----------
|
971
|
+
bold: array_like [nn, nt]
|
972
|
+
The brain activity to be analyzed.
|
973
|
+
averag: bool
|
974
|
+
If True, the average homotopic connectivity is returned.
|
975
|
+
Otherwise, the homotopic connectivity vector is returned.
|
976
|
+
positive: bool
|
977
|
+
If True, only positive correlations are considered.
|
978
|
+
|
979
|
+
Returns
|
980
|
+
-------
|
981
|
+
values : array_like [n_nodes]
|
982
|
+
The homotopic correlation vector.
|
983
|
+
labels : list of str
|
984
|
+
The labels of the homotopic correlation vector.
|
985
|
+
|
986
|
+
Negative correlations may be artificially induced when using global signal regression
|
987
|
+
in functional imaging pre-processing (Fox et al., 2009; Murphy et al., 2009; Murphy and Fox, 2017).
|
988
|
+
Therefore, results on negative weights should be interpreted with caution and should be understood
|
989
|
+
as complementary information underpinning the findings based on positive connections
|
990
|
+
"""
|
991
|
+
|
992
|
+
from numpy import corrcoef, cov
|
993
|
+
|
994
|
+
info, ts = prepare_input_ts(ts)
|
995
|
+
if not info:
|
996
|
+
if verbose:
|
997
|
+
print("Error in fc_homotopic")
|
998
|
+
return [np.nan], ["fc_homotopic"]
|
999
|
+
|
1000
|
+
nn, nt = ts.shape
|
1001
|
+
if nn < 2:
|
1002
|
+
return [np.nan], ["fc_homotopic"]
|
1003
|
+
|
1004
|
+
NHALF = int(nn // 2)
|
1005
|
+
fc = eval(fc_function)(ts)
|
1006
|
+
|
1007
|
+
if positive:
|
1008
|
+
fc = fc * (fc > 0)
|
1009
|
+
fc = fc - np.diag(np.diag(fc)) # not necessary for hfc
|
1010
|
+
hfc = np.diag(fc, k=NHALF)
|
1011
|
+
if average:
|
1012
|
+
return [np.mean(hfc)], ["fc_homotopic_avg"]
|
1013
|
+
else:
|
1014
|
+
values = hfc.squeeze()
|
1015
|
+
labels = [f"fc_homotopic_{i}" for i in range(len(values))]
|
1016
|
+
return values, labels
|
1017
|
+
|
1018
|
+
|
1019
|
+
def coactivation_degree(ts: np.ndarray, modality="noncor"):
|
1020
|
+
"""
|
1021
|
+
calculate coactivation degree (CAD) #!TODO not tested
|
1022
|
+
|
1023
|
+
Parameters
|
1024
|
+
----------
|
1025
|
+
ts: np.ndarray [n_regions, n_samples]
|
1026
|
+
input array
|
1027
|
+
modality: str
|
1028
|
+
|
1029
|
+
Returns
|
1030
|
+
-------
|
1031
|
+
values: array-like
|
1032
|
+
coactivation degree
|
1033
|
+
labels: array-like
|
1034
|
+
labels of the features
|
1035
|
+
|
1036
|
+
|
1037
|
+
"""
|
1038
|
+
nn, nt = ts.shape
|
1039
|
+
ts = stats.zscore(ts, axis=1)
|
1040
|
+
if modality == "cor":
|
1041
|
+
global_signal = stats.zscore(np.mean(ts, axis=1))
|
1042
|
+
|
1043
|
+
M = np.zeros((nn, nt))
|
1044
|
+
for i in range(nn):
|
1045
|
+
if modality != "cor":
|
1046
|
+
global_signal = np.mean(np.delete(ts, i, axis=0), axis=0)
|
1047
|
+
M[i] = ts[i, :] * global_signal
|
1048
|
+
return M.tolist()
|
1049
|
+
|
1050
|
+
|
1051
|
+
def coactivation_phase(ts):
|
1052
|
+
"""
|
1053
|
+
calculate the coactivation phase (CAP) #!TODO not tested
|
1054
|
+
|
1055
|
+
Parameters
|
1056
|
+
----------
|
1057
|
+
ts: np.ndarray [n_regions, n_samples]
|
1058
|
+
input array
|
1059
|
+
|
1060
|
+
Returns
|
1061
|
+
-------
|
1062
|
+
CAP: list
|
1063
|
+
"""
|
1064
|
+
|
1065
|
+
if isinstance(ts, (list, tuple)):
|
1066
|
+
ts = np.array(ts)
|
1067
|
+
if ts.ndim == 1:
|
1068
|
+
ts = ts.reshape(-1, 1)
|
1069
|
+
|
1070
|
+
ts = stats.zscore(ts, axis=1)
|
1071
|
+
|
1072
|
+
# phase global
|
1073
|
+
GS = np.mean(ts, axis=0)
|
1074
|
+
Phase = np.unwrap(np.angle(hilbert(GS)))
|
1075
|
+
Phase = (Phase + np.pi) % (2 * np.pi) - np.pi
|
1076
|
+
|
1077
|
+
# phase regional
|
1078
|
+
phase_i = np.unwrap(np.angle(hilbert(ts, axis=1)), axis=1)
|
1079
|
+
phase_i = (phase_i + np.pi) % (2 * np.pi) - np.pi
|
1080
|
+
MSphase = np.mean(Phase - phase_i, axis=1)
|
1081
|
+
|
1082
|
+
return MSphase.tolist()
|
1083
|
+
|
1084
|
+
|
1085
|
+
def burstiness(ts: np.ndarray, indices: List[int] = None, verbose=False):
|
1086
|
+
"""
|
1087
|
+
calculate the burstiness statistic
|
1088
|
+
- Goh and Barabasi, 'Burstiness and memory in complex systems' Europhys. Lett.
|
1089
|
+
81, 48002 (2008).
|
1090
|
+
[from hctsa-py]
|
1091
|
+
|
1092
|
+
Parameters
|
1093
|
+
----------
|
1094
|
+
x: np.ndarray [n_regions, n_samples]
|
1095
|
+
input array
|
1096
|
+
indices: list of int
|
1097
|
+
Indices of the time series to compute the feature
|
1098
|
+
|
1099
|
+
Returns
|
1100
|
+
-------
|
1101
|
+
B: list of floats
|
1102
|
+
burstiness statistic
|
1103
|
+
"""
|
1104
|
+
|
1105
|
+
info, ts = prepare_input_ts(ts, indices)
|
1106
|
+
if not info:
|
1107
|
+
if verbose:
|
1108
|
+
print("Error in burstiness")
|
1109
|
+
return [np.nan], ["burstiness"]
|
1110
|
+
|
1111
|
+
if ts.mean() == 0:
|
1112
|
+
return [0], ["burstiness"]
|
1113
|
+
|
1114
|
+
r = np.std(ts, axis=1) / np.mean(ts, axis=1)
|
1115
|
+
B = (r - 1) / (r + 1)
|
1116
|
+
labels = [f"burstiness_{i}" for i in range(len(B))]
|
1117
|
+
|
1118
|
+
return B, labels
|
1119
|
+
|
1120
|
+
|
1121
|
+
def fcd_stat(
|
1122
|
+
ts,
|
1123
|
+
TR=1,
|
1124
|
+
win_len=30,
|
1125
|
+
masks=None,
|
1126
|
+
positive=False,
|
1127
|
+
eigenvalues=True,
|
1128
|
+
pca_num_components=3,
|
1129
|
+
quantiles=[0.05, 0.25, 0.5, 0.75, 0.95],
|
1130
|
+
features=["sum", "max", "min", "mean", "std", "skew", "kurtosis"],
|
1131
|
+
verbose=False,
|
1132
|
+
):
|
1133
|
+
|
1134
|
+
from numpy import sum, max, min, mean, std
|
1135
|
+
from scipy.stats import skew, kurtosis
|
1136
|
+
|
1137
|
+
info, ts = prepare_input_ts(ts)
|
1138
|
+
if not info:
|
1139
|
+
if verbose:
|
1140
|
+
print("Error in fcd_stat")
|
1141
|
+
return [np.nan], ["fcd_stat_0"]
|
1142
|
+
|
1143
|
+
Values = []
|
1144
|
+
Labels = []
|
1145
|
+
|
1146
|
+
k = int(win_len / TR)
|
1147
|
+
fcd = get_fcd(ts=ts, TR=TR, win_len=win_len, positive=positive, masks=masks)
|
1148
|
+
for key in fcd.keys():
|
1149
|
+
values, labels = matrix_stat(
|
1150
|
+
fcd[key],
|
1151
|
+
k=k,
|
1152
|
+
features=features,
|
1153
|
+
quantiles=quantiles,
|
1154
|
+
eigenvalues=eigenvalues,
|
1155
|
+
pca_num_components=pca_num_components,
|
1156
|
+
)
|
1157
|
+
labels = [f"fcd_{key}_{label}" for label in labels]
|
1158
|
+
Values.extend(values)
|
1159
|
+
Labels.extend(labels)
|
1160
|
+
|
1161
|
+
return Values, Labels
|
1162
|
+
|
1163
|
+
|
1164
|
+
def calc_mi(
|
1165
|
+
ts: np.ndarray,
|
1166
|
+
k: int = 4,
|
1167
|
+
time_diff: int = 1,
|
1168
|
+
num_threads: int = 1,
|
1169
|
+
source_indices: List[int] = None,
|
1170
|
+
target_indices: List[int] = None,
|
1171
|
+
mode: str = "pairwise",
|
1172
|
+
verbose=False,
|
1173
|
+
**kwargs,
|
1174
|
+
):
|
1175
|
+
"""
|
1176
|
+
calculate the mutual information between time series
|
1177
|
+
based on the Kraskov method #!TODO bug in multiprocessing
|
1178
|
+
|
1179
|
+
Parameters
|
1180
|
+
----------
|
1181
|
+
ts: np.ndarray [n_regions, n_samples]
|
1182
|
+
input array
|
1183
|
+
k: int
|
1184
|
+
kth nearest neighbor
|
1185
|
+
time_diff: int
|
1186
|
+
time difference between time series
|
1187
|
+
num_threads: int
|
1188
|
+
number of threads
|
1189
|
+
source_indices: list or np.ndarray
|
1190
|
+
indices of source time series, if None, all time series are used
|
1191
|
+
target_indices: list or np.ndarray
|
1192
|
+
indices of target time series, if None, all time series are used
|
1193
|
+
mode: str
|
1194
|
+
"pairwise" or "all", if "pairwise", source_indices and target_indices must have the same length
|
1195
|
+
|
1196
|
+
Returns
|
1197
|
+
-------
|
1198
|
+
MI: list of floats
|
1199
|
+
mutual information
|
1200
|
+
labels: list of str
|
1201
|
+
labels of the features
|
1202
|
+
"""
|
1203
|
+
|
1204
|
+
num_surrogates = kwargs.get("num_surrogates", 0)
|
1205
|
+
|
1206
|
+
if not isinstance(ts, np.ndarray):
|
1207
|
+
ts = np.array(ts)
|
1208
|
+
if ts.ndim == 1:
|
1209
|
+
assert False, "ts must be a 2d array"
|
1210
|
+
|
1211
|
+
init_jvm()
|
1212
|
+
calcClass = jp.JPackage(
|
1213
|
+
"infodynamics.measures.continuous.kraskov"
|
1214
|
+
).MutualInfoCalculatorMultiVariateKraskov2
|
1215
|
+
calc = calcClass()
|
1216
|
+
calc.setProperty("k", str(int(k)))
|
1217
|
+
calc.setProperty("NUM_THREADS", str(int(num_threads)))
|
1218
|
+
calc.setProperty("TIME_DIFF", str(int(time_diff)))
|
1219
|
+
calc.initialise()
|
1220
|
+
calc.startAddObservations()
|
1221
|
+
|
1222
|
+
if source_indices is None:
|
1223
|
+
source_indices = np.arange(ts.shape[0])
|
1224
|
+
if target_indices is None:
|
1225
|
+
target_indices = np.arange(ts.shape[0])
|
1226
|
+
|
1227
|
+
ts = ts.tolist()
|
1228
|
+
if mode == "all":
|
1229
|
+
for i in source_indices:
|
1230
|
+
for j in target_indices:
|
1231
|
+
calc.addObservations(ts[i], ts[j])
|
1232
|
+
|
1233
|
+
elif mode == "pairwise":
|
1234
|
+
assert len(source_indices) == len(target_indices)
|
1235
|
+
for i, j in zip(source_indices, target_indices):
|
1236
|
+
calc.addObservations(ts[i], ts[j])
|
1237
|
+
calc.finaliseAddObservations()
|
1238
|
+
MI = calc.computeAverageLocalOfObservations()
|
1239
|
+
|
1240
|
+
if num_surrogates > 0:
|
1241
|
+
NullDist = calc.computeSignificance(num_surrogates)
|
1242
|
+
NullMean = NullDist.getMeanOfDistribution()
|
1243
|
+
MI = MI - NullMean if (MI >= NullMean) else 0.0
|
1244
|
+
|
1245
|
+
MI = nat2bit(MI)
|
1246
|
+
MI = MI if MI >= 0 else 0.0
|
1247
|
+
label = "mi"
|
1248
|
+
|
1249
|
+
return [MI], [label]
|
1250
|
+
|
1251
|
+
|
1252
|
+
def calc_te(
|
1253
|
+
ts: np.ndarray,
|
1254
|
+
k: int = 4,
|
1255
|
+
delay: int = 1,
|
1256
|
+
num_threads: int = 1,
|
1257
|
+
source_indices: List[int] = None,
|
1258
|
+
target_indices: List[int] = None,
|
1259
|
+
mode: str = "pairwise",
|
1260
|
+
verbose=False,
|
1261
|
+
**kwargs,
|
1262
|
+
):
|
1263
|
+
"""
|
1264
|
+
calculate the transfer entropy between time series based on the Kraskov method.
|
1265
|
+
|
1266
|
+
Parameters
|
1267
|
+
----------
|
1268
|
+
ts: np.ndarray [n_regions, n_samples]
|
1269
|
+
input array
|
1270
|
+
num_threads: int
|
1271
|
+
number of threads
|
1272
|
+
source_indices: list or np.ndarray
|
1273
|
+
indices of source time series, if None, all time series are used
|
1274
|
+
target_indices: list or np.ndarray
|
1275
|
+
indices of target time series, if None, all time series are used
|
1276
|
+
mode: str
|
1277
|
+
"pairwise" or "all", if "pairwise", source_indices and target_indices must have the same length
|
1278
|
+
|
1279
|
+
|
1280
|
+
Returns
|
1281
|
+
-------
|
1282
|
+
TE: list of floats
|
1283
|
+
transfer entropy
|
1284
|
+
"""
|
1285
|
+
|
1286
|
+
num_surrogates = kwargs.get("num_surrogates", 0)
|
1287
|
+
|
1288
|
+
info, ts = prepare_input_ts(ts)
|
1289
|
+
if not info:
|
1290
|
+
return [np.nan], ["te"]
|
1291
|
+
|
1292
|
+
if ts.shape[0] == 1:
|
1293
|
+
assert False, "ts must have more than one time series"
|
1294
|
+
|
1295
|
+
init_jvm()
|
1296
|
+
calcClass = jp.JPackage(
|
1297
|
+
"infodynamics.measures.continuous.kraskov"
|
1298
|
+
).TransferEntropyCalculatorKraskov
|
1299
|
+
calc = calcClass()
|
1300
|
+
calc.setProperty("NUM_THREADS", str(int(num_threads)))
|
1301
|
+
calc.setProperty("DELAY", str(int(delay)))
|
1302
|
+
calc.setProperty("AUTO_EMBED_RAGWITZ_NUM_NNS", "4")
|
1303
|
+
calc.setProperty("k", str(int(k)))
|
1304
|
+
calc.initialise()
|
1305
|
+
calc.startAddObservations()
|
1306
|
+
|
1307
|
+
if source_indices is None:
|
1308
|
+
source_indices = np.arange(ts.shape[0])
|
1309
|
+
if target_indices is None:
|
1310
|
+
target_indices = np.arange(ts.shape[0])
|
1311
|
+
|
1312
|
+
ts = ts.tolist()
|
1313
|
+
if mode == "all":
|
1314
|
+
for i in source_indices:
|
1315
|
+
for j in target_indices:
|
1316
|
+
calc.addObservations(ts[i], ts[j])
|
1317
|
+
|
1318
|
+
elif mode == "pairwise":
|
1319
|
+
assert len(source_indices) == len(target_indices)
|
1320
|
+
for i, j in zip(source_indices, target_indices):
|
1321
|
+
calc.addObservations(ts[i], ts[j])
|
1322
|
+
calc.finaliseAddObservations()
|
1323
|
+
te = calc.computeAverageLocalOfObservations()
|
1324
|
+
|
1325
|
+
if num_surrogates > 0:
|
1326
|
+
NullDist = calc.computeSignificance(num_surrogates)
|
1327
|
+
NullMean = NullDist.getMeanOfDistribution()
|
1328
|
+
# NullStd = NullDist.getStdOfDistribution()
|
1329
|
+
te = te - NullMean if (te >= NullMean) else 0.0
|
1330
|
+
te = te if te >= 0 else 0.0
|
1331
|
+
label = "te"
|
1332
|
+
|
1333
|
+
return [te], [label]
|
1334
|
+
|
1335
|
+
|
1336
|
+
def calc_entropy(ts: np.ndarray, average: bool = False, verbose=False):
|
1337
|
+
"""
|
1338
|
+
calculate entropy of time series
|
1339
|
+
"""
|
1340
|
+
|
1341
|
+
if not isinstance(ts, np.ndarray):
|
1342
|
+
ts = np.array(ts)
|
1343
|
+
if ts.ndim == 1:
|
1344
|
+
ts = ts.reshape(1, -1)
|
1345
|
+
n = ts.shape[0]
|
1346
|
+
labels = [f"entropy_{i}" for i in range(n)]
|
1347
|
+
|
1348
|
+
if ts.size == 0:
|
1349
|
+
return np.nan, labels
|
1350
|
+
if np.isnan(ts).any() or np.isinf(ts).any():
|
1351
|
+
n = ts.shape[0]
|
1352
|
+
return [np.nan] * n, labels
|
1353
|
+
|
1354
|
+
init_jvm()
|
1355
|
+
|
1356
|
+
calcClass = jp.JPackage(
|
1357
|
+
"infodynamics.measures.continuous.kozachenko"
|
1358
|
+
).EntropyCalculatorMultiVariateKozachenko
|
1359
|
+
calc = calcClass()
|
1360
|
+
|
1361
|
+
values = []
|
1362
|
+
if not average:
|
1363
|
+
for i in range(n):
|
1364
|
+
calc.initialise()
|
1365
|
+
calc.setObservations(ts[i, :])
|
1366
|
+
value = nat2bit(calc.computeAverageLocalOfObservations())
|
1367
|
+
values.append(value)
|
1368
|
+
else:
|
1369
|
+
calc.initialise()
|
1370
|
+
ts = ts.squeeze().flatten().tolist()
|
1371
|
+
calc.setObservations(ts)
|
1372
|
+
values = nat2bit(calc.computeAverageLocalOfObservations())
|
1373
|
+
labels = "entropy"
|
1374
|
+
|
1375
|
+
return values, labels
|
1376
|
+
|
1377
|
+
|
1378
|
+
def calc_entropy_bin(ts: np.ndarray, prob: str = "standard", average: bool = False, verbose=False):
|
1379
|
+
"""Computes the entropy of the signal using the Shannon Entropy.
|
1380
|
+
|
1381
|
+
Description in Article:
|
1382
|
+
Regularities Unseen, Randomness Observed: Levels of Entropy Convergence
|
1383
|
+
Authors: Crutchfield J. Feldman David
|
1384
|
+
|
1385
|
+
Parameters
|
1386
|
+
----------
|
1387
|
+
signal : nd-array
|
1388
|
+
Input from which entropy is computed
|
1389
|
+
prob : string
|
1390
|
+
Probability function (kde or gaussian functions are available)
|
1391
|
+
|
1392
|
+
Returns
|
1393
|
+
-------
|
1394
|
+
values: float or array-like
|
1395
|
+
The normalized entropy value
|
1396
|
+
labels: string or array-like
|
1397
|
+
The label of the feature
|
1398
|
+
|
1399
|
+
"""
|
1400
|
+
|
1401
|
+
def one_dim(x):
|
1402
|
+
if prob == "standard":
|
1403
|
+
value, counts = np.unique(ts, return_counts=True)
|
1404
|
+
p = counts / counts.sum()
|
1405
|
+
elif prob == "kde":
|
1406
|
+
p = kde(ts)
|
1407
|
+
elif prob == "gauss":
|
1408
|
+
p = gaussian(ts)
|
1409
|
+
|
1410
|
+
if np.sum(p) == 0:
|
1411
|
+
return 0.0
|
1412
|
+
|
1413
|
+
# Handling zero probability values
|
1414
|
+
p = p[np.where(p != 0)]
|
1415
|
+
|
1416
|
+
# If probability all in one value, there is no entropy
|
1417
|
+
if np.log2(len(ts)) == 1:
|
1418
|
+
return 0.0
|
1419
|
+
elif np.sum(p * np.log2(p)) / np.log2(len(ts)) == 0:
|
1420
|
+
return 0.0
|
1421
|
+
else:
|
1422
|
+
return -np.sum(p * np.log2(p)) / np.log2(len(ts))
|
1423
|
+
|
1424
|
+
info, ts = prepare_input_ts(ts)
|
1425
|
+
if not info:
|
1426
|
+
return [np.nan], [f"entropy_bin_{0}"]
|
1427
|
+
else:
|
1428
|
+
r, c = ts.shape
|
1429
|
+
values = np.zeros(r)
|
1430
|
+
for i in range(r):
|
1431
|
+
values[i] = one_dim(ts[i])
|
1432
|
+
if average:
|
1433
|
+
values = np.mean(values)
|
1434
|
+
labels = "entropy_bin"
|
1435
|
+
else:
|
1436
|
+
labels = [f"entropy_bin_{i}" for i in range(len(values))]
|
1437
|
+
return values, labels
|
1438
|
+
|
1439
|
+
|
1440
|
+
def spectrum_stats(
|
1441
|
+
ts: np.ndarray,
|
1442
|
+
fs: float,
|
1443
|
+
method: str = "fft",
|
1444
|
+
nperseg: int = None,
|
1445
|
+
verbose=False,
|
1446
|
+
indices: List[int] = None,
|
1447
|
+
average=False,
|
1448
|
+
features: List[str] = [
|
1449
|
+
"spectral_distance",
|
1450
|
+
"fundamental_frequency",
|
1451
|
+
"max_frequency",
|
1452
|
+
"max_psd"
|
1453
|
+
"median_frequency",
|
1454
|
+
"spectral_centroid",
|
1455
|
+
"spectral_kurtosis",
|
1456
|
+
"spectral_variation",
|
1457
|
+
],
|
1458
|
+
):
|
1459
|
+
"""
|
1460
|
+
compute some statistics of the power spectrum of the time series.
|
1461
|
+
|
1462
|
+
Parameters
|
1463
|
+
----------
|
1464
|
+
ts : nd-array [n_regions x n_samples]
|
1465
|
+
Input from which power spectrum statistics are computed
|
1466
|
+
fs : float
|
1467
|
+
Sampling frequency
|
1468
|
+
method : str
|
1469
|
+
Method to compute the power spectrum. Can be 'welch' or 'fft'
|
1470
|
+
indices: list of int
|
1471
|
+
indices of the regions to be used
|
1472
|
+
|
1473
|
+
Returns
|
1474
|
+
-------
|
1475
|
+
values: array-like
|
1476
|
+
power spectrum statistics of the time series
|
1477
|
+
labels: array-like
|
1478
|
+
labels of the features
|
1479
|
+
"""
|
1480
|
+
|
1481
|
+
info, ts = prepare_input_ts(ts, indices)
|
1482
|
+
if not info:
|
1483
|
+
return [np.nan], [f"spectrum_stats_{0}"]
|
1484
|
+
else:
|
1485
|
+
ts = ts - ts.mean(axis=1, keepdims=True)
|
1486
|
+
|
1487
|
+
if method == "welch":
|
1488
|
+
if nperseg is None:
|
1489
|
+
nperseg = ts.shape[1] // 2
|
1490
|
+
freq, psd = scipy.signal.welch(ts, fs=fs, axis=1, nperseg=nperseg)
|
1491
|
+
elif method == "fft":
|
1492
|
+
freq, psd = calc_fft(ts, fs)
|
1493
|
+
else:
|
1494
|
+
raise ValueError("method must be one of 'welch', 'fft'")
|
1495
|
+
|
1496
|
+
if average:
|
1497
|
+
psd = np.mean(psd, axis=0).reshape(1, -1)
|
1498
|
+
|
1499
|
+
values = np.array([])
|
1500
|
+
labels = []
|
1501
|
+
|
1502
|
+
for f in features:
|
1503
|
+
|
1504
|
+
|
1505
|
+
v, l = eval(f)(freq, psd)
|
1506
|
+
values = np.append(values, v)
|
1507
|
+
labels = labels + l
|
1508
|
+
|
1509
|
+
return values, labels
|
1510
|
+
|
1511
|
+
|
1512
|
+
def spectrum_auc(
|
1513
|
+
ts, fs, method="fft", bands=None, nperseg=None, average=False, indices=None, verbose=False
|
1514
|
+
):
|
1515
|
+
"""
|
1516
|
+
calculate the area under the curve of the power spectrum of the time series over given frequency bands.
|
1517
|
+
|
1518
|
+
Parameters
|
1519
|
+
----------
|
1520
|
+
ts : nd-array [n_regions x n_samples]
|
1521
|
+
Input time series
|
1522
|
+
fs : float
|
1523
|
+
Sampling frequency
|
1524
|
+
method : str
|
1525
|
+
Method to compute the power spectrum. Can be 'welch' or 'fft'
|
1526
|
+
bands : list of tuples
|
1527
|
+
Frequency bands
|
1528
|
+
nperseg: int
|
1529
|
+
Length of each segment. default is half of the time series
|
1530
|
+
avg: bool
|
1531
|
+
averaging psd over all regions
|
1532
|
+
indices: list of int
|
1533
|
+
indices of the regions to be used
|
1534
|
+
|
1535
|
+
Returns
|
1536
|
+
-------
|
1537
|
+
values: array-like
|
1538
|
+
area under the curve of the power spectrum of the time series
|
1539
|
+
labels: array-like
|
1540
|
+
labels of the features
|
1541
|
+
|
1542
|
+
"""
|
1543
|
+
|
1544
|
+
info, ts = prepare_input_ts(ts)
|
1545
|
+
if not info:
|
1546
|
+
return [np.nan], [f"spectrum_auc_{0}"]
|
1547
|
+
else:
|
1548
|
+
ts = ts - ts.mean(axis=1, keepdims=True)
|
1549
|
+
# r, c = ts.shape
|
1550
|
+
|
1551
|
+
if indices is None:
|
1552
|
+
indices = np.arange(ts.shape[0])
|
1553
|
+
else:
|
1554
|
+
indices = np.array(indices, dtype=int)
|
1555
|
+
ts = ts[indices, :]
|
1556
|
+
if len(indices) == 1:
|
1557
|
+
ts = ts.reshape(1, -1)
|
1558
|
+
|
1559
|
+
if method == "welch":
|
1560
|
+
if nperseg is None:
|
1561
|
+
nperseg = ts.shape[1] // 2
|
1562
|
+
freq, psd = scipy.signal.welch(ts, fs=fs, axis=1, nperseg=nperseg)
|
1563
|
+
elif method == "fft":
|
1564
|
+
freq, psd = calc_fft(ts, fs)
|
1565
|
+
else:
|
1566
|
+
raise ValueError("method must be one of 'welch', 'fft'")
|
1567
|
+
|
1568
|
+
if bands is None:
|
1569
|
+
bands = [(0, 4), (4, 8), (8, 12), (12, 30), (30, 70)]
|
1570
|
+
|
1571
|
+
if average:
|
1572
|
+
psd = np.mean(psd, axis=0).reshape(1, -1)
|
1573
|
+
|
1574
|
+
values = []
|
1575
|
+
for i, band in enumerate(bands):
|
1576
|
+
idx = (freq >= band[0]) & (freq < band[1])
|
1577
|
+
if np.sum(idx) == 0:
|
1578
|
+
continue
|
1579
|
+
psd_band = psd[:, idx]
|
1580
|
+
values.append(np.trapz(psd_band, axis=1))
|
1581
|
+
|
1582
|
+
if len(values) > 0:
|
1583
|
+
values = np.concatenate(values)
|
1584
|
+
labels = [f"spectrum_auc_{i}" for i in range(len(values))]
|
1585
|
+
if len(values) == 0:
|
1586
|
+
values = [np.nan]
|
1587
|
+
labels = ["spectrum_auc"]
|
1588
|
+
|
1589
|
+
return values, labels
|
1590
|
+
|
1591
|
+
|
1592
|
+
def spectrum_moments(
|
1593
|
+
ts,
|
1594
|
+
fs,
|
1595
|
+
method="fft",
|
1596
|
+
nperseg=None,
|
1597
|
+
avg=False,
|
1598
|
+
moments=[2, 3, 4, 5, 6],
|
1599
|
+
normalize=False,
|
1600
|
+
indices=None,
|
1601
|
+
verbose=False,
|
1602
|
+
):
|
1603
|
+
"""
|
1604
|
+
Computes the moments of power spectrum
|
1605
|
+
|
1606
|
+
Parameters
|
1607
|
+
----------
|
1608
|
+
ts : nd-array [n_regions x n_samples]
|
1609
|
+
Input from which power spectrum statistics are computed
|
1610
|
+
fs : float
|
1611
|
+
Sampling frequency
|
1612
|
+
method : str
|
1613
|
+
Method to compute the power spectrum. Can be 'welch' or 'fft'
|
1614
|
+
nperseg: int
|
1615
|
+
...
|
1616
|
+
avg: bool
|
1617
|
+
averaging over all regions
|
1618
|
+
nm: list of int
|
1619
|
+
moments orders
|
1620
|
+
|
1621
|
+
Returns
|
1622
|
+
-------
|
1623
|
+
values: array-like
|
1624
|
+
power spectrum statistics of the time series
|
1625
|
+
labels: array-like
|
1626
|
+
labels of the features
|
1627
|
+
"""
|
1628
|
+
|
1629
|
+
info, n = prepare_input_ts(ts)
|
1630
|
+
if not info:
|
1631
|
+
return [np.nan] * n, [f"spectrum_moment_{i}" for i in range(n)]
|
1632
|
+
else:
|
1633
|
+
ts = n
|
1634
|
+
ts = ts - ts.mean(axis=1, keepdims=True)
|
1635
|
+
# r, c = ts.shape
|
1636
|
+
if indices is None:
|
1637
|
+
indices = np.arange(ts.shape[0])
|
1638
|
+
else:
|
1639
|
+
indices = np.array(indices, dtype=int)
|
1640
|
+
ts = ts[indices, :]
|
1641
|
+
if len(indices) == 1:
|
1642
|
+
ts = ts.reshape(1, -1)
|
1643
|
+
|
1644
|
+
if method == "welch":
|
1645
|
+
if nperseg is None:
|
1646
|
+
nperseg = ts.shape[1] // 2
|
1647
|
+
freq, psd = scipy.signal.welch(ts, fs=fs, axis=1, nperseg=nperseg)
|
1648
|
+
elif method == "fft":
|
1649
|
+
freq, psd = calc_fft(ts, fs)
|
1650
|
+
else:
|
1651
|
+
raise ValueError("method must be one of 'welch', 'fft'")
|
1652
|
+
|
1653
|
+
Values = np.array([])
|
1654
|
+
Labels = []
|
1655
|
+
if normalize:
|
1656
|
+
psd = psd / np.max(psd, axis=1, keepdims=True)
|
1657
|
+
|
1658
|
+
if avg:
|
1659
|
+
psd = np.mean(psd, axis=0)
|
1660
|
+
|
1661
|
+
for i in moments:
|
1662
|
+
_m = moment(psd, i, axis=1)
|
1663
|
+
Values = np.append(Values, _m)
|
1664
|
+
Labels = Labels + [f"spectrum_moment_{i}_{j}" for j in range(len(_m))]
|
1665
|
+
return Values, Labels
|
1666
|
+
|
1667
|
+
|
1668
|
+
def psd_raw(
|
1669
|
+
ts,
|
1670
|
+
fs,
|
1671
|
+
bands=[(0, 4), (4, 8), (8, 12), (12, 30), (30, 70)],
|
1672
|
+
df=None,
|
1673
|
+
method="fft",
|
1674
|
+
nperseg=None,
|
1675
|
+
average=False,
|
1676
|
+
normalize=False,
|
1677
|
+
normalize_to: float = None, # normalize to given value in Hz
|
1678
|
+
indices=None,
|
1679
|
+
verbose=False,
|
1680
|
+
):
|
1681
|
+
"""
|
1682
|
+
Calculate frequency spectrum and return with specified frequency resolution.
|
1683
|
+
|
1684
|
+
Parameters
|
1685
|
+
----------
|
1686
|
+
|
1687
|
+
ts : nd-array [n_regions x n_samples]
|
1688
|
+
Input time series
|
1689
|
+
fs : float
|
1690
|
+
Sampling frequency
|
1691
|
+
bands : list of tuples
|
1692
|
+
Frequency bands
|
1693
|
+
df : float
|
1694
|
+
Frequency resolution, default is fs / n_samples
|
1695
|
+
method : str
|
1696
|
+
Method to compute the power spectrum. Can be 'welch' or 'fft'
|
1697
|
+
nperseg: int
|
1698
|
+
Length of each segment. default is half of the time series
|
1699
|
+
avg: bool
|
1700
|
+
averaging psd over all regions
|
1701
|
+
normalize: bool
|
1702
|
+
normalize the psd by the maximum value
|
1703
|
+
normalize_to: float
|
1704
|
+
normalize the psd to the given frequency value
|
1705
|
+
indices: list of int
|
1706
|
+
indices of the regions to be used
|
1707
|
+
|
1708
|
+
Returns
|
1709
|
+
-------
|
1710
|
+
psd: array-like
|
1711
|
+
power spectrum density
|
1712
|
+
|
1713
|
+
"""
|
1714
|
+
|
1715
|
+
info, ts = prepare_input_ts(ts)
|
1716
|
+
if not info:
|
1717
|
+
return [np.nan], [f"spectrum_moment_{0}"]
|
1718
|
+
else:
|
1719
|
+
ts = ts - ts.mean(axis=1, keepdims=True)
|
1720
|
+
# r, c = ts.shape
|
1721
|
+
if indices is None:
|
1722
|
+
indices = np.arange(ts.shape[0])
|
1723
|
+
else:
|
1724
|
+
indices = np.array(indices, dtype=int)
|
1725
|
+
ts = ts[indices, :]
|
1726
|
+
if len(indices) == 1:
|
1727
|
+
ts = ts.reshape(1, -1)
|
1728
|
+
|
1729
|
+
if method == "welch":
|
1730
|
+
if nperseg is None:
|
1731
|
+
nperseg = ts.shape[1] // 2
|
1732
|
+
freq, psd = scipy.signal.welch(ts, fs=fs, axis=1, nperseg=nperseg)
|
1733
|
+
elif method == "fft":
|
1734
|
+
freq, psd = calc_fft(ts, fs)
|
1735
|
+
else:
|
1736
|
+
raise ValueError("method must be one of 'welch', 'fft'")
|
1737
|
+
|
1738
|
+
if average:
|
1739
|
+
psd = np.mean(psd, axis=0).reshape(1, -1)
|
1740
|
+
|
1741
|
+
if normalize and (normalize_to is not None):
|
1742
|
+
raise ValueError("normalize and normalize_to cannot be used together")
|
1743
|
+
|
1744
|
+
if normalize_to is not None:
|
1745
|
+
# check if the value is in the frequency range
|
1746
|
+
if normalize_to < 0 or normalize_to > fs / 2:
|
1747
|
+
raise ValueError("normalize_to must be in the range of 0 to fs/2")
|
1748
|
+
|
1749
|
+
# find index of the frequency closest to the given value
|
1750
|
+
idx = np.argmin(np.abs(freq - normalize_to))
|
1751
|
+
psd = psd / psd[:, idx].reshape(-1, 1)
|
1752
|
+
elif normalize:
|
1753
|
+
psd = psd / np.max(psd, axis=1, keepdims=True)
|
1754
|
+
|
1755
|
+
if df is None:
|
1756
|
+
df = fs / ts.shape[1]
|
1757
|
+
fr_intp = np.arange(0, fs / 2, df)
|
1758
|
+
psd_intp = np.apply_along_axis(
|
1759
|
+
lambda row: np.interp(fr_intp, freq, row), axis=1, arr=psd
|
1760
|
+
)
|
1761
|
+
|
1762
|
+
psd_bands = np.array([])
|
1763
|
+
for i in range(len(bands)):
|
1764
|
+
idx = (fr_intp >= bands[i][0]) & (fr_intp < bands[i][1])
|
1765
|
+
if np.sum(idx) == 0:
|
1766
|
+
continue
|
1767
|
+
psd_bands = np.append(psd_bands, psd_intp[:, idx].flatten())
|
1768
|
+
|
1769
|
+
psd_bands = psd_bands.astype(float)
|
1770
|
+
labels = [f"psd_{i}" for i in range(len(psd_bands))]
|
1771
|
+
|
1772
|
+
return psd_bands, labels
|
1773
|
+
|
1774
|
+
|
1775
|
+
def wavelet_abs_mean_1d(ts, function=None, widths=np.arange(1, 10), verbose=False):
|
1776
|
+
"""Computes CWT absolute mean value of each wavelet scale.
|
1777
|
+
|
1778
|
+
Parameters
|
1779
|
+
----------
|
1780
|
+
ts : nd-array
|
1781
|
+
Input from which CWT is computed
|
1782
|
+
function : wavelet function
|
1783
|
+
Default: scipy.signal.ricker
|
1784
|
+
widths : nd-array
|
1785
|
+
Widths to use for transformation
|
1786
|
+
Default: np.arange(1,10)
|
1787
|
+
|
1788
|
+
Returns
|
1789
|
+
-------
|
1790
|
+
tuple
|
1791
|
+
CWT absolute mean value
|
1792
|
+
|
1793
|
+
"""
|
1794
|
+
if function is None:
|
1795
|
+
function = scipy.signal.ricker
|
1796
|
+
|
1797
|
+
return tuple(np.abs(np.mean(wavelet(ts, function, widths), axis=1)))
|
1798
|
+
|
1799
|
+
|
1800
|
+
def wavelet_abs_mean(ts, function=None, widths=np.arange(1, 10), verbose=False):
|
1801
|
+
'''
|
1802
|
+
"""Computes CWT absolute mean value of each wavelet scale.
|
1803
|
+
|
1804
|
+
Parameters
|
1805
|
+
----------
|
1806
|
+
ts : nd-array [n_regions x n_samples]
|
1807
|
+
Input from which CWT is computed
|
1808
|
+
function : wavelet function
|
1809
|
+
Default: scipy.signal.ricker
|
1810
|
+
widths : nd-array
|
1811
|
+
Widths to use for transformation
|
1812
|
+
Default: np.arange(1,10)
|
1813
|
+
|
1814
|
+
Returns
|
1815
|
+
-------
|
1816
|
+
values: array-like
|
1817
|
+
CWT absolute mean value of the time series
|
1818
|
+
labels: array-like
|
1819
|
+
labels of the features
|
1820
|
+
'''
|
1821
|
+
|
1822
|
+
if function is None:
|
1823
|
+
function = scipy.signal.ricker
|
1824
|
+
|
1825
|
+
info, n = prepare_input_ts(ts)
|
1826
|
+
if not info:
|
1827
|
+
return [np.nan] * n, [f"wavelet_abs_mean_{i}" for i in range(n)]
|
1828
|
+
else:
|
1829
|
+
ts = n
|
1830
|
+
r, _ = ts.shape
|
1831
|
+
values = np.zeros((r, len(widths)))
|
1832
|
+
for i in range(r):
|
1833
|
+
values[i] = wavelet_abs_mean_1d(ts[i], function, widths)
|
1834
|
+
|
1835
|
+
values = values.flatten()
|
1836
|
+
labels = [
|
1837
|
+
f"wavelet_abs_mean_n{i}_w{j}"
|
1838
|
+
for i in range(len(values))
|
1839
|
+
for j in range(len(widths))
|
1840
|
+
]
|
1841
|
+
return values, labels
|
1842
|
+
|
1843
|
+
|
1844
|
+
def wavelet_std(ts, function=None, widths=np.arange(1, 10), verbose=False):
|
1845
|
+
"""
|
1846
|
+
Computes CWT std value of each wavelet scale.
|
1847
|
+
|
1848
|
+
Parameters
|
1849
|
+
----------
|
1850
|
+
ts : nd-array [n_regions x n_samples]
|
1851
|
+
Input from which CWT is computed
|
1852
|
+
function : wavelet function
|
1853
|
+
Default: scipy.signal.ricker
|
1854
|
+
widths : nd-array
|
1855
|
+
Widths to use for transformation
|
1856
|
+
Default: np.arange(1,10)
|
1857
|
+
|
1858
|
+
Returns
|
1859
|
+
-------
|
1860
|
+
values: array-like
|
1861
|
+
CWT std value of the time series
|
1862
|
+
labels: array-like
|
1863
|
+
labels of the features
|
1864
|
+
|
1865
|
+
"""
|
1866
|
+
|
1867
|
+
if function is None:
|
1868
|
+
function = scipy.signal.ricker
|
1869
|
+
|
1870
|
+
info, n = prepare_input_ts(ts)
|
1871
|
+
if not info:
|
1872
|
+
return [np.nan] * n, [f"wavelet_std_{i}" for i in range(n)]
|
1873
|
+
else:
|
1874
|
+
ts = n
|
1875
|
+
r, _ = ts.shape
|
1876
|
+
values = np.zeros((r, len(widths)))
|
1877
|
+
for i in range(r):
|
1878
|
+
values[i] = np.std(wavelet(ts[i], function, widths), axis=1)
|
1879
|
+
|
1880
|
+
values = values.flatten()
|
1881
|
+
labels = [
|
1882
|
+
f"wavelet_std_n{i}_w{j}"
|
1883
|
+
for i in range(len(values))
|
1884
|
+
for j in range(len(widths))
|
1885
|
+
]
|
1886
|
+
return values, labels
|
1887
|
+
|
1888
|
+
|
1889
|
+
def wavelet_energy_1d(ts, function=None, widths=np.arange(1, 10), verbose=False):
|
1890
|
+
"""Computes CWT energy of each wavelet scale.
|
1891
|
+
|
1892
|
+
Implementation details:
|
1893
|
+
https://stackoverflow.com/questions/37659422/energy-for-1-d-wavelet-in-python
|
1894
|
+
|
1895
|
+
Feature computational cost: 2
|
1896
|
+
|
1897
|
+
Parameters
|
1898
|
+
----------
|
1899
|
+
signal : nd-array
|
1900
|
+
Input from which CWT is computed
|
1901
|
+
function : wavelet function
|
1902
|
+
Default: scipy.signal.ricker
|
1903
|
+
widths : nd-array
|
1904
|
+
Widths to use for transformation
|
1905
|
+
Default: np.arange(1,10)
|
1906
|
+
|
1907
|
+
Returns
|
1908
|
+
-------
|
1909
|
+
tuple
|
1910
|
+
CWT energy
|
1911
|
+
|
1912
|
+
"""
|
1913
|
+
if function is None:
|
1914
|
+
function = scipy.signal.ricker
|
1915
|
+
cwt = wavelet(ts, function, widths)
|
1916
|
+
energy = np.sqrt(np.sum(cwt**2, axis=1) / np.shape(cwt)[1])
|
1917
|
+
|
1918
|
+
return tuple(energy)
|
1919
|
+
|
1920
|
+
|
1921
|
+
def wavelet_energy(ts, function=None, widths=np.arange(1, 10), verbose=False):
|
1922
|
+
"""
|
1923
|
+
Computes CWT energy of each wavelet scale.
|
1924
|
+
|
1925
|
+
Parameters
|
1926
|
+
----------
|
1927
|
+
ts : nd-array [n_regions x n_samples]
|
1928
|
+
Input from which CWT is computed
|
1929
|
+
function : wavelet function
|
1930
|
+
Default: scipy.signal.ricker
|
1931
|
+
widths : nd-array
|
1932
|
+
Widths to use for transformation
|
1933
|
+
Default: np.arange(1,10)
|
1934
|
+
|
1935
|
+
Returns
|
1936
|
+
-------
|
1937
|
+
values: array-like
|
1938
|
+
CWT energy of the time series
|
1939
|
+
labels: array-like
|
1940
|
+
labels of the features
|
1941
|
+
|
1942
|
+
"""
|
1943
|
+
if function is None:
|
1944
|
+
function = scipy.signal.ricker
|
1945
|
+
|
1946
|
+
info, n = prepare_input_ts(ts)
|
1947
|
+
if not info:
|
1948
|
+
return [np.nan] * n, [f"wavelet_energy_{i}" for i in range(n)]
|
1949
|
+
else:
|
1950
|
+
ts = n
|
1951
|
+
r, _ = ts.shape
|
1952
|
+
values = np.zeros((r, len(widths)))
|
1953
|
+
for i in range(r):
|
1954
|
+
values[i] = wavelet_energy_1d(ts[i], function, widths)
|
1955
|
+
|
1956
|
+
values = values.flatten()
|
1957
|
+
labels = [
|
1958
|
+
f"wavelet_energy_n{i}_w{j}"
|
1959
|
+
for i in range(len(values))
|
1960
|
+
for j in range(len(widths))
|
1961
|
+
]
|
1962
|
+
return values, labels
|
1963
|
+
|
1964
|
+
|
1965
|
+
# -----------------------------------------------------------------------------
|
1966
|
+
|
1967
|
+
|
1968
|
+
def hmm_stat(
|
1969
|
+
ts,
|
1970
|
+
node_indices=None,
|
1971
|
+
n_states=4,
|
1972
|
+
subname="",
|
1973
|
+
n_iter=100,
|
1974
|
+
seed=None,
|
1975
|
+
observations="gaussian",
|
1976
|
+
method="em",
|
1977
|
+
tcut=5,
|
1978
|
+
bins=10,
|
1979
|
+
verbose=False,
|
1980
|
+
):
|
1981
|
+
"""
|
1982
|
+
Calculate the state duration of the HMM.
|
1983
|
+
|
1984
|
+
Parameters
|
1985
|
+
----------
|
1986
|
+
ts : nd-array [n_regions x n_samples]
|
1987
|
+
Input from which HMM is computed
|
1988
|
+
node_indices : list
|
1989
|
+
List of node indices to be used for HMM
|
1990
|
+
n_states : int
|
1991
|
+
Number of states
|
1992
|
+
subname : str
|
1993
|
+
subname for the labels
|
1994
|
+
n_iter : int
|
1995
|
+
Number of iterations
|
1996
|
+
seed : int
|
1997
|
+
Random seed
|
1998
|
+
observations : str
|
1999
|
+
Observation distribution
|
2000
|
+
method : str
|
2001
|
+
Method to fit the HMM
|
2002
|
+
t_cut : int
|
2003
|
+
maximum duration of a state, default is 5
|
2004
|
+
|
2005
|
+
Returns
|
2006
|
+
-------
|
2007
|
+
stat_vec : array-like
|
2008
|
+
HMM features
|
2009
|
+
labels : array-like
|
2010
|
+
labels of the features
|
2011
|
+
|
2012
|
+
"""
|
2013
|
+
|
2014
|
+
if seed is not None:
|
2015
|
+
np.random.seed(seed)
|
2016
|
+
|
2017
|
+
info, ts = prepare_input_ts(ts, indices)
|
2018
|
+
if not info:
|
2019
|
+
return [np.nan], [f"hmm_dur"]
|
2020
|
+
else:
|
2021
|
+
|
2022
|
+
obs = ts[node_indices, :].T
|
2023
|
+
nt, obs_dim = obs.shape
|
2024
|
+
model = ssm.HMM(n_states, obs_dim, observations=observations)
|
2025
|
+
model_lls = model.fit(obs, method=method, num_iters=n_iter, verbose=0)
|
2026
|
+
hmm_z = model.most_likely_states(obs)
|
2027
|
+
# emmision_hmm_z, emmision_hmm_y = model.sample(nt) #!TODO: check if need to be used
|
2028
|
+
# hmm_x = model.smooth(obs)
|
2029
|
+
# upper = np.triu_indices(n_states, 0)
|
2030
|
+
trans_mat = (model.transitions.transition_matrix).flatten() # [upper]
|
2031
|
+
|
2032
|
+
stat_duration = state_duration(hmm_z, n_states, avg=True, tcut=tcut, bins=bins)
|
2033
|
+
labels = [f"hmm{subname}_dur_{i}" for i in range(len(stat_duration))]
|
2034
|
+
labels += [f"hmm{subname}_trans_{i}" for i in range(len(trans_mat))]
|
2035
|
+
stat_vec = np.concatenate([stat_duration, trans_mat])
|
2036
|
+
|
2037
|
+
return stat_vec, labels
|
2038
|
+
|
2039
|
+
|
2040
|
+
def catch22(
|
2041
|
+
ts,
|
2042
|
+
indices: List[int] = None,
|
2043
|
+
catch24=False,
|
2044
|
+
verbose=False,
|
2045
|
+
features=[
|
2046
|
+
"DN_HistogramMode_5",
|
2047
|
+
"DN_HistogramMode_10",
|
2048
|
+
"CO_f1ecac",
|
2049
|
+
"CO_FirstMin_ac",
|
2050
|
+
"CO_HistogramAMI_even_2_5",
|
2051
|
+
"CO_trev_1_num",
|
2052
|
+
"MD_hrv_classic_pnn40",
|
2053
|
+
"SB_BinaryStats_mean_longstretch1",
|
2054
|
+
"SB_TransitionMatrix_3ac_sumdiagcov",
|
2055
|
+
"PD_PeriodicityWang_th0_01",
|
2056
|
+
"CO_Embed2_Dist_tau_d_expfit_meandiff",
|
2057
|
+
"IN_AutoMutualInfoStats_40_gaussian_fmmi",
|
2058
|
+
"FC_LocalSimple_mean1_tauresrat",
|
2059
|
+
"DN_OutlierInclude_p_001_mdrmd",
|
2060
|
+
"DN_OutlierInclude_n_001_mdrmd",
|
2061
|
+
"SP_Summaries_welch_rect_area_5_1",
|
2062
|
+
"SB_BinaryStats_diff_longstretch0",
|
2063
|
+
"SB_MotifThree_quantile_hh",
|
2064
|
+
"SC_FluctAnal_2_rsrangefit_50_1_logi_prop_r1",
|
2065
|
+
"SC_FluctAnal_2_dfa_50_1_2_logi_prop_r1",
|
2066
|
+
"SP_Summaries_welch_rect_centroid",
|
2067
|
+
"FC_LocalSimple_mean3_stderr",
|
2068
|
+
],
|
2069
|
+
):
|
2070
|
+
"""
|
2071
|
+
Calculate the Catch22 features.
|
2072
|
+
|
2073
|
+
Parameters
|
2074
|
+
----------
|
2075
|
+
ts : nd-array [n_regions x n_samples]
|
2076
|
+
Input from which Catch22 features are computed
|
2077
|
+
node_indices : list
|
2078
|
+
List of node indices to be used for Catch22
|
2079
|
+
catch24 : bool
|
2080
|
+
If True, calculate mean and std of the features
|
2081
|
+
|
2082
|
+
Returns
|
2083
|
+
-------
|
2084
|
+
values : array-like
|
2085
|
+
feature values
|
2086
|
+
labels : array-like
|
2087
|
+
labels of the features
|
2088
|
+
|
2089
|
+
"""
|
2090
|
+
try:
|
2091
|
+
import catch22_C
|
2092
|
+
except ImportError:
|
2093
|
+
raise ImportError(
|
2094
|
+
"pycatch22 is not installed. Please install it using `pip install pycatch22`"
|
2095
|
+
)
|
2096
|
+
|
2097
|
+
if catch24:
|
2098
|
+
features = features.copy()
|
2099
|
+
features.append('DN_Mean')
|
2100
|
+
features.append('DN_Spread_Std')
|
2101
|
+
|
2102
|
+
def get_features(x, features):
|
2103
|
+
out = []
|
2104
|
+
for f in features:
|
2105
|
+
f_fun = getattr(catch22_C, f)
|
2106
|
+
out.append(f_fun(list(x)))
|
2107
|
+
return out
|
2108
|
+
|
2109
|
+
info, ts = prepare_input_ts(ts, indices)
|
2110
|
+
if not info:
|
2111
|
+
return [np.nan], [f"catch22"]
|
2112
|
+
|
2113
|
+
else:
|
2114
|
+
nn = ts.shape[0]
|
2115
|
+
nf = 22 if not catch24 else 24
|
2116
|
+
values = np.zeros((nn, nf))
|
2117
|
+
for i in range(nn):
|
2118
|
+
v = get_features(ts[i], features)
|
2119
|
+
values[i] = v
|
2120
|
+
|
2121
|
+
values = values.flatten()
|
2122
|
+
labels = features * nn
|
2123
|
+
|
2124
|
+
return values, labels
|