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.
Files changed (121) hide show
  1. vbi/__init__.py +37 -0
  2. vbi/_version.py +17 -0
  3. vbi/dataset/__init__.py +0 -0
  4. vbi/dataset/connectivity_84/centers.txt +84 -0
  5. vbi/dataset/connectivity_84/centres.txt +84 -0
  6. vbi/dataset/connectivity_84/cortical.txt +84 -0
  7. vbi/dataset/connectivity_84/tract_lengths.txt +84 -0
  8. vbi/dataset/connectivity_84/weights.txt +84 -0
  9. vbi/dataset/connectivity_88/Aud_88.txt +88 -0
  10. vbi/dataset/connectivity_88/Bold.npz +0 -0
  11. vbi/dataset/connectivity_88/Labels.txt +17 -0
  12. vbi/dataset/connectivity_88/Region_labels.txt +88 -0
  13. vbi/dataset/connectivity_88/tract_lengths.txt +88 -0
  14. vbi/dataset/connectivity_88/weights.txt +88 -0
  15. vbi/feature_extraction/__init__.py +1 -0
  16. vbi/feature_extraction/calc_features.py +293 -0
  17. vbi/feature_extraction/features.json +535 -0
  18. vbi/feature_extraction/features.py +2124 -0
  19. vbi/feature_extraction/features_settings.py +374 -0
  20. vbi/feature_extraction/features_utils.py +1357 -0
  21. vbi/feature_extraction/infodynamics.jar +0 -0
  22. vbi/feature_extraction/utility.py +507 -0
  23. vbi/inference.py +98 -0
  24. vbi/models/__init__.py +0 -0
  25. vbi/models/cpp/__init__.py +0 -0
  26. vbi/models/cpp/_src/__init__.py +0 -0
  27. vbi/models/cpp/_src/__pycache__/mpr_sde.cpython-310.pyc +0 -0
  28. vbi/models/cpp/_src/_do.cpython-310-x86_64-linux-gnu.so +0 -0
  29. vbi/models/cpp/_src/_jr_sdde.cpython-310-x86_64-linux-gnu.so +0 -0
  30. vbi/models/cpp/_src/_jr_sde.cpython-310-x86_64-linux-gnu.so +0 -0
  31. vbi/models/cpp/_src/_km_sde.cpython-310-x86_64-linux-gnu.so +0 -0
  32. vbi/models/cpp/_src/_mpr_sde.cpython-310-x86_64-linux-gnu.so +0 -0
  33. vbi/models/cpp/_src/_vep.cpython-310-x86_64-linux-gnu.so +0 -0
  34. vbi/models/cpp/_src/_wc_ode.cpython-310-x86_64-linux-gnu.so +0 -0
  35. vbi/models/cpp/_src/bold.hpp +303 -0
  36. vbi/models/cpp/_src/do.hpp +167 -0
  37. vbi/models/cpp/_src/do.i +17 -0
  38. vbi/models/cpp/_src/do.py +467 -0
  39. vbi/models/cpp/_src/do_wrap.cxx +12811 -0
  40. vbi/models/cpp/_src/jr_sdde.hpp +352 -0
  41. vbi/models/cpp/_src/jr_sdde.i +19 -0
  42. vbi/models/cpp/_src/jr_sdde.py +688 -0
  43. vbi/models/cpp/_src/jr_sdde_wrap.cxx +18718 -0
  44. vbi/models/cpp/_src/jr_sde.hpp +264 -0
  45. vbi/models/cpp/_src/jr_sde.i +17 -0
  46. vbi/models/cpp/_src/jr_sde.py +470 -0
  47. vbi/models/cpp/_src/jr_sde_wrap.cxx +13406 -0
  48. vbi/models/cpp/_src/km_sde.hpp +158 -0
  49. vbi/models/cpp/_src/km_sde.i +19 -0
  50. vbi/models/cpp/_src/km_sde.py +671 -0
  51. vbi/models/cpp/_src/km_sde_wrap.cxx +17367 -0
  52. vbi/models/cpp/_src/makefile +52 -0
  53. vbi/models/cpp/_src/mpr_sde.hpp +327 -0
  54. vbi/models/cpp/_src/mpr_sde.i +19 -0
  55. vbi/models/cpp/_src/mpr_sde.py +711 -0
  56. vbi/models/cpp/_src/mpr_sde_wrap.cxx +18618 -0
  57. vbi/models/cpp/_src/utility.hpp +307 -0
  58. vbi/models/cpp/_src/vep.hpp +171 -0
  59. vbi/models/cpp/_src/vep.i +16 -0
  60. vbi/models/cpp/_src/vep.py +464 -0
  61. vbi/models/cpp/_src/vep_wrap.cxx +12968 -0
  62. vbi/models/cpp/_src/wc_ode.hpp +294 -0
  63. vbi/models/cpp/_src/wc_ode.i +19 -0
  64. vbi/models/cpp/_src/wc_ode.py +686 -0
  65. vbi/models/cpp/_src/wc_ode_wrap.cxx +24263 -0
  66. vbi/models/cpp/damp_oscillator.py +143 -0
  67. vbi/models/cpp/jansen_rit.py +543 -0
  68. vbi/models/cpp/km.py +187 -0
  69. vbi/models/cpp/mpr.py +289 -0
  70. vbi/models/cpp/vep.py +150 -0
  71. vbi/models/cpp/wc.py +216 -0
  72. vbi/models/cupy/__init__.py +0 -0
  73. vbi/models/cupy/bold.py +111 -0
  74. vbi/models/cupy/ghb.py +284 -0
  75. vbi/models/cupy/jansen_rit.py +473 -0
  76. vbi/models/cupy/km.py +224 -0
  77. vbi/models/cupy/mpr.py +475 -0
  78. vbi/models/cupy/mpr_modified_bold.py +12 -0
  79. vbi/models/cupy/utils.py +184 -0
  80. vbi/models/numba/__init__.py +0 -0
  81. vbi/models/numba/_ww_EI.py +444 -0
  82. vbi/models/numba/damp_oscillator.py +162 -0
  83. vbi/models/numba/ghb.py +208 -0
  84. vbi/models/numba/mpr.py +383 -0
  85. vbi/models/pytorch/__init__.py +0 -0
  86. vbi/models/pytorch/data/default_parameters.npz +0 -0
  87. vbi/models/pytorch/data/input/ROI_sim.mat +0 -0
  88. vbi/models/pytorch/data/input/fc_test.csv +68 -0
  89. vbi/models/pytorch/data/input/fc_train.csv +68 -0
  90. vbi/models/pytorch/data/input/fc_vali.csv +68 -0
  91. vbi/models/pytorch/data/input/fcd_test.mat +0 -0
  92. vbi/models/pytorch/data/input/fcd_test_high_window.mat +0 -0
  93. vbi/models/pytorch/data/input/fcd_test_low_window.mat +0 -0
  94. vbi/models/pytorch/data/input/fcd_train.mat +0 -0
  95. vbi/models/pytorch/data/input/fcd_vali.mat +0 -0
  96. vbi/models/pytorch/data/input/myelin.csv +68 -0
  97. vbi/models/pytorch/data/input/rsfc_gradient.csv +68 -0
  98. vbi/models/pytorch/data/input/run_label_testset.mat +0 -0
  99. vbi/models/pytorch/data/input/sc_test.csv +68 -0
  100. vbi/models/pytorch/data/input/sc_train.csv +68 -0
  101. vbi/models/pytorch/data/input/sc_vali.csv +68 -0
  102. vbi/models/pytorch/data/obs_kong0.npz +0 -0
  103. vbi/models/pytorch/ww_sde_kong.py +570 -0
  104. vbi/models/tvbk/__init__.py +9 -0
  105. vbi/models/tvbk/tvbk_wrapper.py +166 -0
  106. vbi/models/tvbk/utils.py +72 -0
  107. vbi/papers/__init__.py +0 -0
  108. vbi/papers/pavlides_pcb_2015/pavlides.py +211 -0
  109. vbi/tests/__init__.py +0 -0
  110. vbi/tests/_test_mpr_nb.py +36 -0
  111. vbi/tests/test_features.py +355 -0
  112. vbi/tests/test_ghb_cupy.py +90 -0
  113. vbi/tests/test_mpr_cupy.py +49 -0
  114. vbi/tests/test_mpr_numba.py +84 -0
  115. vbi/tests/test_suite.py +19 -0
  116. vbi/utils.py +402 -0
  117. vbi-0.1.3.dist-info/METADATA +166 -0
  118. vbi-0.1.3.dist-info/RECORD +121 -0
  119. vbi-0.1.3.dist-info/WHEEL +5 -0
  120. vbi-0.1.3.dist-info/licenses/LICENSE +201 -0
  121. 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