py-neuromodulation 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii +0 -0
- py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -0
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +106 -0
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +119 -0
- py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat +0 -0
- py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat +0 -0
- py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii +0 -0
- py_neuromodulation/ConnectivityDecoding/rmap_struc.nii +0 -0
- py_neuromodulation/{helper.py → _write_example_dataset_helper.py} +1 -1
- py_neuromodulation/nm_EpochStream.py +2 -3
- py_neuromodulation/nm_IO.py +43 -70
- py_neuromodulation/nm_RMAP.py +308 -11
- py_neuromodulation/nm_analysis.py +1 -1
- py_neuromodulation/nm_artifacts.py +25 -0
- py_neuromodulation/nm_bispectra.py +64 -29
- py_neuromodulation/nm_bursts.py +44 -30
- py_neuromodulation/nm_coherence.py +2 -1
- py_neuromodulation/nm_features.py +4 -2
- py_neuromodulation/nm_filter.py +63 -32
- py_neuromodulation/nm_filter_preprocessing.py +91 -0
- py_neuromodulation/nm_fooof.py +47 -29
- py_neuromodulation/nm_mne_connectivity.py +1 -1
- py_neuromodulation/nm_normalization.py +50 -74
- py_neuromodulation/nm_oscillatory.py +151 -31
- py_neuromodulation/nm_plots.py +13 -10
- py_neuromodulation/nm_rereference.py +10 -8
- py_neuromodulation/nm_run_analysis.py +28 -13
- py_neuromodulation/nm_settings.json +51 -3
- py_neuromodulation/nm_sharpwaves.py +103 -136
- py_neuromodulation/nm_stats.py +44 -30
- py_neuromodulation/nm_stream_abc.py +18 -10
- py_neuromodulation/nm_stream_offline.py +188 -46
- py_neuromodulation/utils/_logging.py +24 -0
- {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info}/METADATA +72 -32
- py_neuromodulation-0.0.4.dist-info/RECORD +72 -0
- {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info}/WHEEL +1 -1
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/MOV_aligned_features_ch_ECOG_RIGHT_0_all.png +0 -0
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/all_feature_plt.pdf +0 -0
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_FEATURES.csv +0 -182
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_LM_ML_RES.p +0 -0
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_SETTINGS.json +0 -273
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_SIDECAR.json +0 -6
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_decoding_performance.png +0 -0
- py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_nm_channels.csv +0 -11
- py_neuromodulation/py_neuromodulation.egg-info/PKG-INFO +0 -104
- py_neuromodulation/py_neuromodulation.egg-info/dependency_links.txt +0 -1
- py_neuromodulation/py_neuromodulation.egg-info/requires.txt +0 -26
- py_neuromodulation/py_neuromodulation.egg-info/top_level.txt +0 -1
- py_neuromodulation-0.0.2.dist-info/RECORD +0 -73
- /py_neuromodulation/{py_neuromodulation.egg-info/SOURCES.txt → utils/__init__.py} +0 -0
- {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info/licenses}/LICENSE +0 -0
py_neuromodulation/nm_bursts.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import numpy as np
|
|
3
3
|
from typing import Iterable
|
|
4
|
+
from scipy import signal
|
|
4
5
|
|
|
5
6
|
from py_neuromodulation import nm_features_abc, nm_filter
|
|
6
7
|
|
|
@@ -9,12 +10,16 @@ class Burst(nm_features_abc.Feature):
|
|
|
9
10
|
def __init__(
|
|
10
11
|
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
11
12
|
) -> None:
|
|
12
|
-
|
|
13
13
|
self.s = settings
|
|
14
14
|
self.sfreq = sfreq
|
|
15
15
|
self.ch_names = ch_names
|
|
16
16
|
self.threshold = self.s["burst_settings"]["threshold"]
|
|
17
17
|
self.time_duration_s = self.s["burst_settings"]["time_duration_s"]
|
|
18
|
+
self.samples_overlap = int(
|
|
19
|
+
self.sfreq
|
|
20
|
+
* (self.s["segment_length_features_ms"] / 1000)
|
|
21
|
+
/ self.s["sampling_rate_features_hz"]
|
|
22
|
+
)
|
|
18
23
|
|
|
19
24
|
self.fband_names = self.s["burst_settings"]["frequency_bands"]
|
|
20
25
|
self.f_ranges = [
|
|
@@ -34,9 +39,11 @@ class Burst(nm_features_abc.Feature):
|
|
|
34
39
|
)
|
|
35
40
|
).astype(int)
|
|
36
41
|
|
|
37
|
-
self.num_max_samples_ring_buffer = int(
|
|
42
|
+
self.num_max_samples_ring_buffer = int(
|
|
43
|
+
self.sfreq * self.time_duration_s
|
|
44
|
+
)
|
|
38
45
|
|
|
39
|
-
self.bandpass_filter = nm_filter.
|
|
46
|
+
self.bandpass_filter = nm_filter.MNEFilter(
|
|
40
47
|
f_ranges=self.f_ranges,
|
|
41
48
|
sfreq=self.sfreq,
|
|
42
49
|
filter_length=self.sfreq - 1,
|
|
@@ -62,35 +69,40 @@ class Burst(nm_features_abc.Feature):
|
|
|
62
69
|
ch_names: Iterable[str],
|
|
63
70
|
sfreq: int | float,
|
|
64
71
|
):
|
|
65
|
-
assert (
|
|
66
|
-
|
|
72
|
+
assert isinstance(
|
|
73
|
+
settings["burst_settings"]["threshold"], (float, int)
|
|
67
74
|
), f"burst settings threshold needs to be type int or float, got: {settings['burst_settings']['threshold']}"
|
|
68
75
|
assert (
|
|
69
76
|
0 < settings["burst_settings"]["threshold"] < 100
|
|
70
77
|
), f"burst setting threshold needs to be between 0 and 100, got: {settings['burst_settings']['threshold']}"
|
|
71
|
-
assert (
|
|
72
|
-
|
|
78
|
+
assert isinstance(
|
|
79
|
+
settings["burst_settings"]["time_duration_s"], (float, int)
|
|
73
80
|
), f"burst settings time_duration_s needs to be type int or float, got: {settings['burst_settings']['time_duration_s']}"
|
|
74
81
|
assert (
|
|
75
82
|
settings["burst_settings"]["time_duration_s"] > 0
|
|
76
83
|
), f"burst setting time_duration_s needs to be greater than 0, got: {settings['burst_settings']['time_duration_s']}"
|
|
77
84
|
|
|
78
85
|
for fband_burst in settings["burst_settings"]["frequency_bands"]:
|
|
79
|
-
assert (
|
|
80
|
-
|
|
86
|
+
assert fband_burst in list(
|
|
87
|
+
settings["frequency_ranges_hz"].keys()
|
|
81
88
|
), f"bursting {fband_burst} needs to be defined in settings['frequency_ranges_hz']"
|
|
82
89
|
|
|
83
|
-
for burst_feature in settings["burst_settings"][
|
|
90
|
+
for burst_feature in settings["burst_settings"][
|
|
91
|
+
"burst_features"
|
|
92
|
+
].keys():
|
|
84
93
|
assert isinstance(
|
|
85
|
-
settings["burst_settings"]["burst_features"][burst_feature],
|
|
94
|
+
settings["burst_settings"]["burst_features"][burst_feature],
|
|
95
|
+
bool,
|
|
86
96
|
), (
|
|
87
97
|
f"bursting feature {burst_feature} needs to be type bool, "
|
|
88
98
|
f"got: {settings['burst_settings']['burst_features'][burst_feature]}"
|
|
89
99
|
)
|
|
90
|
-
def calc_feature(self, data: np.array, features_compute: dict) -> dict:
|
|
91
100
|
|
|
101
|
+
def calc_feature(self, data: np.array, features_compute: dict) -> dict:
|
|
92
102
|
# filter_data returns (n_channels, n_fbands, n_samples)
|
|
93
|
-
filtered_data =
|
|
103
|
+
filtered_data = np.abs(
|
|
104
|
+
signal.hilbert(self.bandpass_filter.filter_data(data), axis=2)
|
|
105
|
+
)
|
|
94
106
|
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
95
107
|
for fband_idx, fband_name in enumerate(self.fband_names):
|
|
96
108
|
new_dat = filtered_data[ch_idx, fband_idx, :]
|
|
@@ -98,8 +110,12 @@ class Burst(nm_features_abc.Feature):
|
|
|
98
110
|
self.data_buffer[ch_name][fband_name] = new_dat
|
|
99
111
|
else:
|
|
100
112
|
self.data_buffer[ch_name][fband_name] = np.concatenate(
|
|
101
|
-
(
|
|
102
|
-
|
|
113
|
+
(
|
|
114
|
+
self.data_buffer[ch_name][fband_name],
|
|
115
|
+
new_dat[-self.samples_overlap :],
|
|
116
|
+
),
|
|
117
|
+
axis=0,
|
|
118
|
+
)[-self.num_max_samples_ring_buffer :]
|
|
103
119
|
|
|
104
120
|
# calc features
|
|
105
121
|
burst_thr = np.percentile(
|
|
@@ -145,9 +161,9 @@ class Burst(nm_features_abc.Feature):
|
|
|
145
161
|
if self.data_buffer[ch_name][fband_name][-1] > burst_thr:
|
|
146
162
|
in_burst = True
|
|
147
163
|
|
|
148
|
-
features_compute[
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
features_compute[f"{ch_name}_bursts_{fband_name}_in_burst"] = (
|
|
165
|
+
in_burst
|
|
166
|
+
)
|
|
151
167
|
return features_compute
|
|
152
168
|
|
|
153
169
|
@staticmethod
|
|
@@ -160,25 +176,23 @@ class Burst(nm_features_abc.Feature):
|
|
|
160
176
|
bursts = np.zeros((beta_averp_norm.shape[0] + 1), dtype=bool)
|
|
161
177
|
bursts[1:] = beta_averp_norm >= burst_thr
|
|
162
178
|
deriv = np.diff(bursts)
|
|
163
|
-
isburst = False
|
|
164
179
|
burst_length = []
|
|
165
180
|
burst_amplitude = []
|
|
166
|
-
burst_start = 0
|
|
167
181
|
|
|
168
|
-
|
|
169
|
-
if burst_state == True:
|
|
170
|
-
if isburst == True:
|
|
171
|
-
burst_length.append(index - burst_start)
|
|
172
|
-
burst_amplitude.append(beta_averp_norm[burst_start:index])
|
|
182
|
+
burst_time_points = np.where(deriv == True)[0]
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
184
|
+
for i in range(burst_time_points.size // 2):
|
|
185
|
+
burst_length.append(
|
|
186
|
+
burst_time_points[2 * i + 1] - burst_time_points[2 * i]
|
|
187
|
+
)
|
|
188
|
+
burst_amplitude.append(
|
|
189
|
+
beta_averp_norm[
|
|
190
|
+
burst_time_points[2 * i] : burst_time_points[2 * i + 1]
|
|
191
|
+
]
|
|
192
|
+
)
|
|
178
193
|
|
|
179
194
|
# the last burst length (in case isburst == True) is omitted,
|
|
180
195
|
# since the true burst length cannot be estimated
|
|
181
|
-
|
|
182
196
|
burst_length = np.array(burst_length) / sfreq
|
|
183
197
|
|
|
184
198
|
return burst_amplitude, burst_length
|
|
@@ -111,7 +111,7 @@ class CoherenceObject:
|
|
|
111
111
|
|
|
112
112
|
class NM_Coherence(nm_features_abc.Feature):
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
|
|
116
116
|
def __init__(
|
|
117
117
|
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
@@ -119,6 +119,7 @@ class NM_Coherence(nm_features_abc.Feature):
|
|
|
119
119
|
self.s = settings
|
|
120
120
|
self.sfreq = sfreq
|
|
121
121
|
self.ch_names = ch_names
|
|
122
|
+
self.coherence_objects: Iterable[CoherenceObject] = []
|
|
122
123
|
|
|
123
124
|
for idx_coh in range(len(self.s["coherence"]["channels"])):
|
|
124
125
|
fband_names = self.s["coherence"]["frequency_bands"]
|
|
@@ -11,7 +11,7 @@ from py_neuromodulation import (
|
|
|
11
11
|
nm_oscillatory,
|
|
12
12
|
nm_bursts,
|
|
13
13
|
nm_linelength,
|
|
14
|
-
nm_bispectra
|
|
14
|
+
nm_bispectra,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
@@ -56,6 +56,8 @@ class Features:
|
|
|
56
56
|
FeatureClass = nm_oscillatory.STFT
|
|
57
57
|
case "fft":
|
|
58
58
|
FeatureClass = nm_oscillatory.FFT
|
|
59
|
+
case "welch":
|
|
60
|
+
FeatureClass = nm_oscillatory.Welch
|
|
59
61
|
case "sharpwave_analysis":
|
|
60
62
|
FeatureClass = nm_sharpwaves.SharpwaveAnalyzer
|
|
61
63
|
case "fooof":
|
|
@@ -111,4 +113,4 @@ class Features:
|
|
|
111
113
|
features_compute,
|
|
112
114
|
)
|
|
113
115
|
|
|
114
|
-
return features_compute
|
|
116
|
+
return features_compute
|
py_neuromodulation/nm_filter.py
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"""Module for filter functionality."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger("PynmLogger")
|
|
6
|
+
|
|
2
7
|
import mne
|
|
3
8
|
from mne.filter import _overlap_add_filter
|
|
4
9
|
import numpy as np
|
|
5
10
|
|
|
6
11
|
|
|
7
|
-
class
|
|
8
|
-
"""
|
|
12
|
+
class MNEFilter:
|
|
13
|
+
"""mne.filter wrapper
|
|
9
14
|
|
|
10
15
|
This class stores for given frequency band ranges the filter
|
|
11
16
|
coefficients with length "filter_len".
|
|
12
17
|
The filters can then be used sequentially for band power estimation with
|
|
13
18
|
apply_filter().
|
|
19
|
+
Note that this filter can be a bandpass, bandstop, lowpass, or highpass filter
|
|
20
|
+
depending on the frequency ranges given (see further details in mne.filter.create_filter).
|
|
14
21
|
|
|
15
22
|
Parameters
|
|
16
23
|
----------
|
|
@@ -35,7 +42,7 @@ class BandPassFilter:
|
|
|
35
42
|
|
|
36
43
|
def __init__(
|
|
37
44
|
self,
|
|
38
|
-
f_ranges: list[list[int | float | None]],
|
|
45
|
+
f_ranges: list[list[int | float | None]] | list[int | float | None],
|
|
39
46
|
sfreq: int | float,
|
|
40
47
|
filter_length: str | float = "999ms",
|
|
41
48
|
l_trans_bandwidth: int | float | str = 4,
|
|
@@ -43,22 +50,36 @@ class BandPassFilter:
|
|
|
43
50
|
verbose: bool | int | str | None = None,
|
|
44
51
|
) -> None:
|
|
45
52
|
filter_bank = []
|
|
46
|
-
# mne create_filter function only accepts str and int
|
|
53
|
+
# mne create_filter function only accepts str and int for filter_length
|
|
47
54
|
if isinstance(filter_length, float):
|
|
48
55
|
filter_length = int(filter_length)
|
|
49
56
|
|
|
57
|
+
if not isinstance(f_ranges[0], list):
|
|
58
|
+
f_ranges = [f_ranges]
|
|
59
|
+
|
|
50
60
|
for f_range in f_ranges:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
try:
|
|
62
|
+
filt = mne.filter.create_filter(
|
|
63
|
+
None,
|
|
64
|
+
sfreq,
|
|
65
|
+
l_freq=f_range[0],
|
|
66
|
+
h_freq=f_range[1],
|
|
67
|
+
fir_design="firwin",
|
|
68
|
+
l_trans_bandwidth=l_trans_bandwidth, # type: ignore
|
|
69
|
+
h_trans_bandwidth=h_trans_bandwidth, # type: ignore
|
|
70
|
+
filter_length=filter_length, # type: ignore
|
|
71
|
+
verbose=verbose,
|
|
72
|
+
)
|
|
73
|
+
except:
|
|
74
|
+
filt = mne.filter.create_filter(
|
|
75
|
+
None,
|
|
76
|
+
sfreq,
|
|
77
|
+
l_freq=f_range[0],
|
|
78
|
+
h_freq=f_range[1],
|
|
79
|
+
fir_design="firwin",
|
|
80
|
+
verbose=verbose,
|
|
81
|
+
# filter_length=filter_length,
|
|
82
|
+
)
|
|
62
83
|
filter_bank.append(filt)
|
|
63
84
|
self.filter_bank = np.vstack(filter_bank)
|
|
64
85
|
|
|
@@ -77,10 +98,6 @@ class BandPassFilter:
|
|
|
77
98
|
np.ndarray, shape (n_channels, n_fbands, n_samples)
|
|
78
99
|
Filtered data.
|
|
79
100
|
|
|
80
|
-
Raises
|
|
81
|
-
------
|
|
82
|
-
ValueError
|
|
83
|
-
If data.ndim > 2
|
|
84
101
|
"""
|
|
85
102
|
if data.ndim > 2:
|
|
86
103
|
raise ValueError(
|
|
@@ -89,15 +106,29 @@ class BandPassFilter:
|
|
|
89
106
|
)
|
|
90
107
|
if data.ndim == 1:
|
|
91
108
|
data = np.expand_dims(data, axis=0)
|
|
109
|
+
|
|
92
110
|
filtered = np.array(
|
|
93
111
|
[
|
|
94
112
|
[
|
|
95
|
-
np.convolve(
|
|
96
|
-
for
|
|
113
|
+
np.convolve(filt, chan, mode="same")
|
|
114
|
+
for filt in self.filter_bank
|
|
97
115
|
]
|
|
98
116
|
for chan in data
|
|
99
117
|
]
|
|
100
118
|
)
|
|
119
|
+
|
|
120
|
+
# ensure here that the output dimension matches the input dimension
|
|
121
|
+
if data.shape[1] != filtered.shape[-1]:
|
|
122
|
+
# select the middle part of the filtered data
|
|
123
|
+
middle_index = filtered.shape[-1] // 2
|
|
124
|
+
filtered = filtered[
|
|
125
|
+
:,
|
|
126
|
+
:,
|
|
127
|
+
middle_index
|
|
128
|
+
- data.shape[1] // 2 : middle_index
|
|
129
|
+
+ data.shape[1] // 2,
|
|
130
|
+
]
|
|
131
|
+
|
|
101
132
|
return filtered
|
|
102
133
|
|
|
103
134
|
|
|
@@ -125,7 +156,7 @@ class NotchFilter:
|
|
|
125
156
|
# Code is copied from filter.py notch_filter
|
|
126
157
|
if freqs.size == 0:
|
|
127
158
|
self.filter_bank = None
|
|
128
|
-
|
|
159
|
+
logger.warning(
|
|
129
160
|
"WARNING: notch_filter is activated but data is not being"
|
|
130
161
|
f" filtered. This may be due to a low sampling frequency or"
|
|
131
162
|
f" incorrect specifications. Make sure your settings are"
|
|
@@ -170,19 +201,19 @@ class NotchFilter:
|
|
|
170
201
|
phase="zero",
|
|
171
202
|
fir_window="hamming",
|
|
172
203
|
fir_design="firwin",
|
|
173
|
-
verbose=False
|
|
204
|
+
verbose=False,
|
|
174
205
|
)
|
|
175
206
|
|
|
176
207
|
def process(self, data: np.ndarray) -> np.ndarray:
|
|
177
208
|
if self.filter_bank is None:
|
|
178
209
|
return data
|
|
179
210
|
return _overlap_add_filter(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
x=data,
|
|
212
|
+
h=self.filter_bank,
|
|
213
|
+
n_fft=None,
|
|
214
|
+
phase="zero",
|
|
215
|
+
picks=None,
|
|
216
|
+
n_jobs=1,
|
|
217
|
+
copy=True,
|
|
218
|
+
pad="reflect_limited",
|
|
219
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from py_neuromodulation import nm_filter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PreprocessingFilter:
|
|
7
|
+
|
|
8
|
+
def __init__(self, settings: dict, sfreq: int | float) -> None:
|
|
9
|
+
self.s = settings
|
|
10
|
+
self.sfreq = sfreq
|
|
11
|
+
self.filters = []
|
|
12
|
+
|
|
13
|
+
if self.s["preprocessing_filter"]["bandstop_filter"] is True:
|
|
14
|
+
self.filters.append(
|
|
15
|
+
nm_filter.MNEFilter(
|
|
16
|
+
f_ranges=[
|
|
17
|
+
self.s["preprocessing_filter"][
|
|
18
|
+
"bandstop_filter_settings"
|
|
19
|
+
]["frequency_high_hz"],
|
|
20
|
+
self.s["preprocessing_filter"][
|
|
21
|
+
"bandstop_filter_settings"
|
|
22
|
+
]["frequency_low_hz"],
|
|
23
|
+
],
|
|
24
|
+
sfreq=self.sfreq,
|
|
25
|
+
filter_length=self.sfreq - 1,
|
|
26
|
+
verbose=False,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if self.s["preprocessing_filter"]["bandpass_filter"] is True:
|
|
31
|
+
self.filters.append(
|
|
32
|
+
nm_filter.MNEFilter(
|
|
33
|
+
f_ranges=[
|
|
34
|
+
self.s["preprocessing_filter"][
|
|
35
|
+
"bandpass_filter_settings"
|
|
36
|
+
]["frequency_low_hz"],
|
|
37
|
+
self.s["preprocessing_filter"][
|
|
38
|
+
"bandpass_filter_settings"
|
|
39
|
+
]["frequency_high_hz"],
|
|
40
|
+
],
|
|
41
|
+
sfreq=self.sfreq,
|
|
42
|
+
filter_length=self.sfreq - 1,
|
|
43
|
+
verbose=False,
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
if self.s["preprocessing_filter"]["lowpass_filter"] is True:
|
|
47
|
+
self.filters.append(
|
|
48
|
+
nm_filter.MNEFilter(
|
|
49
|
+
f_ranges=[
|
|
50
|
+
None,
|
|
51
|
+
self.s["preprocessing_filter"][
|
|
52
|
+
"lowpass_filter_settings"
|
|
53
|
+
]["frequency_cutoff_hz"],
|
|
54
|
+
],
|
|
55
|
+
sfreq=self.sfreq,
|
|
56
|
+
filter_length=self.sfreq - 1,
|
|
57
|
+
verbose=False,
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
if self.s["preprocessing_filter"]["highpass_filter"] is True:
|
|
61
|
+
self.filters.append(
|
|
62
|
+
nm_filter.MNEFilter(
|
|
63
|
+
f_ranges=[
|
|
64
|
+
self.s["preprocessing_filter"][
|
|
65
|
+
"highpass_filter_settings"
|
|
66
|
+
]["frequency_cutoff_hz"],
|
|
67
|
+
None,
|
|
68
|
+
],
|
|
69
|
+
sfreq=self.sfreq,
|
|
70
|
+
filter_length=self.sfreq - 1,
|
|
71
|
+
verbose=False,
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def process(self, data: np.ndarray) -> np.ndarray:
|
|
76
|
+
"""Preprocess data according to the initialized list of PreprocessingFilter objects
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
data (numpy ndarray) :
|
|
80
|
+
shape(n_channels, n_samples) - data to be preprocessed.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
preprocessed_data (numpy ndarray):
|
|
84
|
+
shape(n_channels, n_samples) - preprocessed data
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
for filter in self.filters:
|
|
88
|
+
data = filter.filter_data(
|
|
89
|
+
data if len(data.shape) == 2 else data[:, 0, :]
|
|
90
|
+
)
|
|
91
|
+
return data if len(data.shape) == 2 else data[:, 0, :]
|
py_neuromodulation/nm_fooof.py
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Iterable
|
|
3
|
+
|
|
1
4
|
import numpy as np
|
|
2
5
|
from fooof import FOOOF
|
|
3
6
|
from scipy import fft
|
|
4
|
-
from typing import Iterable
|
|
5
7
|
|
|
6
8
|
from py_neuromodulation import nm_features_abc
|
|
7
9
|
|
|
@@ -18,15 +20,6 @@ class FooofAnalyzer(nm_features_abc.Feature):
|
|
|
18
20
|
self.ap_mode = "knee" if self.settings_fooof["knee"] else "fixed"
|
|
19
21
|
self.max_n_peaks = self.settings_fooof["max_n_peaks"]
|
|
20
22
|
|
|
21
|
-
self.fm = FOOOF(
|
|
22
|
-
aperiodic_mode=self.ap_mode,
|
|
23
|
-
peak_width_limits=self.settings_fooof["peak_width_limits"],
|
|
24
|
-
max_n_peaks=self.settings_fooof["max_n_peaks"],
|
|
25
|
-
min_peak_height=self.settings_fooof["min_peak_height"],
|
|
26
|
-
peak_threshold=self.settings_fooof["peak_threshold"],
|
|
27
|
-
verbose=False,
|
|
28
|
-
)
|
|
29
|
-
|
|
30
23
|
self.num_samples = int(
|
|
31
24
|
self.settings_fooof["windowlength_ms"] * sfreq / 1000
|
|
32
25
|
)
|
|
@@ -75,44 +68,70 @@ class FooofAnalyzer(nm_features_abc.Feature):
|
|
|
75
68
|
spectrum = self._get_spectrum(data[ch_idx, :])
|
|
76
69
|
|
|
77
70
|
try:
|
|
78
|
-
|
|
71
|
+
fm = FOOOF(
|
|
72
|
+
aperiodic_mode=self.ap_mode,
|
|
73
|
+
peak_width_limits=self.settings_fooof["peak_width_limits"],
|
|
74
|
+
max_n_peaks=self.settings_fooof["max_n_peaks"],
|
|
75
|
+
min_peak_height=self.settings_fooof["min_peak_height"],
|
|
76
|
+
peak_threshold=self.settings_fooof["peak_threshold"],
|
|
77
|
+
verbose=False,
|
|
78
|
+
)
|
|
79
|
+
fm.fit(self.f_vec, spectrum, self.freq_range)
|
|
79
80
|
except Exception as e:
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
logging.critical(e, exc_info=True)
|
|
82
|
+
|
|
83
|
+
if fm.fooofed_spectrum_ is None:
|
|
84
|
+
FIT_PASSED = False
|
|
85
|
+
else:
|
|
86
|
+
FIT_PASSED = True
|
|
87
|
+
|
|
82
88
|
if self.settings_fooof["aperiodic"]["exponent"]:
|
|
83
89
|
features_compute[f"{ch_name}_fooof_a_exp"] = (
|
|
84
|
-
np.nan_to_num(
|
|
85
|
-
if
|
|
90
|
+
np.nan_to_num(fm.get_params("aperiodic_params", "exponent"))
|
|
91
|
+
if FIT_PASSED is True
|
|
86
92
|
else None
|
|
87
93
|
)
|
|
88
94
|
|
|
89
95
|
if self.settings_fooof["aperiodic"]["offset"]:
|
|
90
96
|
features_compute[f"{ch_name}_fooof_a_offset"] = (
|
|
91
|
-
np.nan_to_num(
|
|
92
|
-
if
|
|
97
|
+
np.nan_to_num(fm.get_params("aperiodic_params", "offset"))
|
|
98
|
+
if FIT_PASSED is True
|
|
93
99
|
else None
|
|
94
100
|
)
|
|
95
|
-
|
|
101
|
+
|
|
96
102
|
if self.settings_fooof["aperiodic"]["knee"]:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
103
|
+
if FIT_PASSED is False:
|
|
104
|
+
knee_freq = None
|
|
105
|
+
else:
|
|
106
|
+
if fm.get_params("aperiodic_params", "exponent") != 0:
|
|
107
|
+
knee_fooof = fm.get_params("aperiodic_params", "knee")
|
|
108
|
+
knee_freq = np.nan_to_num(
|
|
109
|
+
knee_fooof
|
|
110
|
+
** (
|
|
111
|
+
1
|
|
112
|
+
/ fm.get_params("aperiodic_params", "exponent")
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
knee_freq = None
|
|
117
|
+
|
|
118
|
+
features_compute[f"{ch_name}_fooof_a_knee_frequency"] = (
|
|
119
|
+
knee_freq
|
|
101
120
|
)
|
|
102
121
|
|
|
103
122
|
peaks_bw = (
|
|
104
|
-
|
|
105
|
-
if
|
|
123
|
+
fm.get_params("peak_params", "BW")
|
|
124
|
+
if FIT_PASSED is True
|
|
106
125
|
else None
|
|
107
126
|
)
|
|
108
127
|
peaks_cf = (
|
|
109
|
-
|
|
110
|
-
if
|
|
128
|
+
fm.get_params("peak_params", "CF")
|
|
129
|
+
if FIT_PASSED is True
|
|
111
130
|
else None
|
|
112
131
|
)
|
|
113
132
|
peaks_pw = (
|
|
114
|
-
|
|
115
|
-
if
|
|
133
|
+
fm.get_params("peak_params", "PW")
|
|
134
|
+
if FIT_PASSED is True
|
|
116
135
|
else None
|
|
117
136
|
)
|
|
118
137
|
|
|
@@ -122,7 +141,6 @@ class FooofAnalyzer(nm_features_abc.Feature):
|
|
|
122
141
|
peaks_pw = [peaks_pw]
|
|
123
142
|
|
|
124
143
|
for peak_idx in range(self.max_n_peaks):
|
|
125
|
-
|
|
126
144
|
if self.settings_fooof["periodic"]["band_width"]:
|
|
127
145
|
features_compute[f"{ch_name}_fooof_p_{peak_idx}_bw"] = (
|
|
128
146
|
peaks_bw[peak_idx] if peak_idx < len(peaks_bw) else None
|
|
@@ -59,7 +59,7 @@ class MNEConnectivity(nm_features_abc.Feature):
|
|
|
59
59
|
if epochs.events.shape[0] < 2:
|
|
60
60
|
raise Exception(
|
|
61
61
|
f"A minimum of 2 epochs is required for mne_connectivity,"
|
|
62
|
-
f" got only {epochs.events.shape[0]}. Increase settings['
|
|
62
|
+
f" got only {epochs.events.shape[0]}. Increase settings['segment_length_features_ms']"
|
|
63
63
|
)
|
|
64
64
|
return epochs
|
|
65
65
|
|