py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.6__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/_get_grid_hull.m +34 -34
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
- py_neuromodulation/__init__.py +80 -13
- py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +496 -531
- py_neuromodulation/analysis/__init__.py +4 -0
- py_neuromodulation/{nm_decode.py → analysis/decode.py} +918 -992
- py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +994 -1074
- py_neuromodulation/{nm_plots.py → analysis/plots.py} +627 -612
- py_neuromodulation/{nm_stats.py → analysis/stats.py} +458 -480
- py_neuromodulation/data/README +6 -6
- py_neuromodulation/data/dataset_description.json +8 -8
- py_neuromodulation/data/participants.json +32 -32
- py_neuromodulation/data/participants.tsv +2 -2
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
- py_neuromodulation/default_settings.yaml +241 -0
- py_neuromodulation/features/__init__.py +31 -0
- py_neuromodulation/features/bandpower.py +165 -0
- py_neuromodulation/features/bispectra.py +157 -0
- py_neuromodulation/features/bursts.py +297 -0
- py_neuromodulation/features/coherence.py +255 -0
- py_neuromodulation/features/feature_processor.py +121 -0
- py_neuromodulation/features/fooof.py +142 -0
- py_neuromodulation/features/hjorth_raw.py +57 -0
- py_neuromodulation/features/linelength.py +21 -0
- py_neuromodulation/features/mne_connectivity.py +148 -0
- py_neuromodulation/features/nolds.py +94 -0
- py_neuromodulation/features/oscillatory.py +249 -0
- py_neuromodulation/features/sharpwaves.py +432 -0
- py_neuromodulation/filter/__init__.py +3 -0
- py_neuromodulation/filter/kalman_filter.py +67 -0
- py_neuromodulation/filter/kalman_filter_external.py +1890 -0
- py_neuromodulation/filter/mne_filter.py +128 -0
- py_neuromodulation/filter/notch_filter.py +93 -0
- py_neuromodulation/grid_cortex.tsv +40 -40
- py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
- py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
- py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
- py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/processing/__init__.py +10 -0
- py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +29 -25
- py_neuromodulation/processing/data_preprocessor.py +77 -0
- py_neuromodulation/processing/filter_preprocessing.py +78 -0
- py_neuromodulation/processing/normalization.py +175 -0
- py_neuromodulation/{nm_projection.py → processing/projection.py} +370 -394
- py_neuromodulation/{nm_rereference.py → processing/rereference.py} +97 -95
- py_neuromodulation/{nm_resample.py → processing/resample.py} +56 -50
- py_neuromodulation/stream/__init__.py +3 -0
- py_neuromodulation/stream/data_processor.py +325 -0
- py_neuromodulation/stream/generator.py +53 -0
- py_neuromodulation/stream/mnelsl_player.py +94 -0
- py_neuromodulation/stream/mnelsl_stream.py +120 -0
- py_neuromodulation/stream/settings.py +292 -0
- py_neuromodulation/stream/stream.py +427 -0
- py_neuromodulation/utils/__init__.py +2 -0
- py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +305 -302
- py_neuromodulation/utils/database.py +149 -0
- py_neuromodulation/utils/io.py +378 -0
- py_neuromodulation/utils/keyboard.py +52 -0
- py_neuromodulation/utils/logging.py +66 -0
- py_neuromodulation/utils/types.py +251 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/METADATA +28 -33
- py_neuromodulation-0.0.6.dist-info/RECORD +89 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/WHEEL +1 -1
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/licenses/LICENSE +21 -21
- py_neuromodulation/FieldTrip.py +0 -589
- py_neuromodulation/_write_example_dataset_helper.py +0 -65
- py_neuromodulation/nm_EpochStream.py +0 -92
- py_neuromodulation/nm_IO.py +0 -417
- py_neuromodulation/nm_across_patient_decoding.py +0 -927
- py_neuromodulation/nm_bispectra.py +0 -168
- py_neuromodulation/nm_bursts.py +0 -198
- py_neuromodulation/nm_coherence.py +0 -205
- py_neuromodulation/nm_cohortwrapper.py +0 -435
- py_neuromodulation/nm_eval_timing.py +0 -239
- py_neuromodulation/nm_features.py +0 -116
- py_neuromodulation/nm_features_abc.py +0 -39
- py_neuromodulation/nm_filter.py +0 -219
- py_neuromodulation/nm_filter_preprocessing.py +0 -91
- py_neuromodulation/nm_fooof.py +0 -159
- py_neuromodulation/nm_generator.py +0 -37
- py_neuromodulation/nm_hjorth_raw.py +0 -73
- py_neuromodulation/nm_kalmanfilter.py +0 -58
- py_neuromodulation/nm_linelength.py +0 -33
- py_neuromodulation/nm_mne_connectivity.py +0 -112
- py_neuromodulation/nm_nolds.py +0 -93
- py_neuromodulation/nm_normalization.py +0 -214
- py_neuromodulation/nm_oscillatory.py +0 -448
- py_neuromodulation/nm_run_analysis.py +0 -435
- py_neuromodulation/nm_settings.json +0 -338
- py_neuromodulation/nm_settings.py +0 -68
- py_neuromodulation/nm_sharpwaves.py +0 -401
- py_neuromodulation/nm_stream_abc.py +0 -218
- py_neuromodulation/nm_stream_offline.py +0 -359
- py_neuromodulation/utils/_logging.py +0 -24
- py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Annotated
|
|
6
|
+
from pydantic import Field, field_validator
|
|
7
|
+
|
|
8
|
+
from py_neuromodulation.utils.types import (
|
|
9
|
+
NMFeature,
|
|
10
|
+
BoolSelector,
|
|
11
|
+
FrequencyRange,
|
|
12
|
+
NMBaseModel,
|
|
13
|
+
)
|
|
14
|
+
from py_neuromodulation import logger
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from py_neuromodulation import NMSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CoherenceMethods(BoolSelector):
|
|
21
|
+
coh: bool = True
|
|
22
|
+
icoh: bool = True
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CoherenceFeatures(BoolSelector):
|
|
26
|
+
mean_fband: bool = True
|
|
27
|
+
max_fband: bool = True
|
|
28
|
+
max_allfbands: bool = True
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
ListOfTwoStr = Annotated[list[str], Field(min_length=2, max_length=2)]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CoherenceSettings(NMBaseModel):
|
|
35
|
+
features: CoherenceFeatures = CoherenceFeatures()
|
|
36
|
+
method: CoherenceMethods = CoherenceMethods()
|
|
37
|
+
channels: list[ListOfTwoStr] = []
|
|
38
|
+
frequency_bands: list[str] = Field(default=["high_beta"], min_length=1)
|
|
39
|
+
|
|
40
|
+
@field_validator("frequency_bands")
|
|
41
|
+
def fbands_spaces_to_underscores(cls, frequency_bands):
|
|
42
|
+
return [f.replace(" ", "_") for f in frequency_bands]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CoherenceObject:
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
sfreq: float,
|
|
49
|
+
window: str,
|
|
50
|
+
fbands: list[FrequencyRange],
|
|
51
|
+
fband_names: list[str],
|
|
52
|
+
ch_1_name: str,
|
|
53
|
+
ch_2_name: str,
|
|
54
|
+
ch_1_idx: int,
|
|
55
|
+
ch_2_idx: int,
|
|
56
|
+
coh: bool,
|
|
57
|
+
icoh: bool,
|
|
58
|
+
features_coh: CoherenceFeatures,
|
|
59
|
+
) -> None:
|
|
60
|
+
self.sfreq = sfreq
|
|
61
|
+
self.window = window
|
|
62
|
+
self.fbands = fbands
|
|
63
|
+
self.fband_names = fband_names
|
|
64
|
+
self.ch_1 = ch_1_name
|
|
65
|
+
self.ch_2 = ch_2_name
|
|
66
|
+
self.ch_1_idx = ch_1_idx
|
|
67
|
+
self.ch_2_idx = ch_2_idx
|
|
68
|
+
self.coh = coh
|
|
69
|
+
self.icoh = icoh
|
|
70
|
+
self.features_coh = features_coh
|
|
71
|
+
|
|
72
|
+
self.Pxx = None
|
|
73
|
+
self.Pyy = None
|
|
74
|
+
self.Pxy = None
|
|
75
|
+
self.f = None
|
|
76
|
+
self.coh_val = None
|
|
77
|
+
self.icoh_val = None
|
|
78
|
+
|
|
79
|
+
def get_coh(self, feature_results, x, y):
|
|
80
|
+
from scipy.signal import welch, csd
|
|
81
|
+
|
|
82
|
+
self.f, self.Pxx = welch(x, self.sfreq, self.window, nperseg=128)
|
|
83
|
+
self.Pyy = welch(y, self.sfreq, self.window, nperseg=128)[1]
|
|
84
|
+
self.Pxy = csd(x, y, self.sfreq, self.window, nperseg=128)[1]
|
|
85
|
+
|
|
86
|
+
if self.coh:
|
|
87
|
+
self.coh_val = np.abs(self.Pxy**2) / (self.Pxx * self.Pyy)
|
|
88
|
+
if self.icoh:
|
|
89
|
+
self.icoh_val = np.array(self.Pxy / (self.Pxx * self.Pyy)).imag
|
|
90
|
+
|
|
91
|
+
for coh_idx, coh_type in enumerate([self.coh, self.icoh]):
|
|
92
|
+
if coh_type:
|
|
93
|
+
if coh_idx == 0:
|
|
94
|
+
coh_val = self.coh_val
|
|
95
|
+
coh_name = "coh"
|
|
96
|
+
else:
|
|
97
|
+
coh_val = self.icoh_val
|
|
98
|
+
coh_name = "icoh"
|
|
99
|
+
|
|
100
|
+
for idx, fband in enumerate(self.fbands):
|
|
101
|
+
if self.features_coh.mean_fband:
|
|
102
|
+
feature_calc = np.mean(
|
|
103
|
+
coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])]
|
|
104
|
+
)
|
|
105
|
+
feature_name = "_".join(
|
|
106
|
+
[
|
|
107
|
+
coh_name,
|
|
108
|
+
self.ch_1,
|
|
109
|
+
"to",
|
|
110
|
+
self.ch_2,
|
|
111
|
+
"mean_fband",
|
|
112
|
+
self.fband_names[idx],
|
|
113
|
+
]
|
|
114
|
+
)
|
|
115
|
+
feature_results[feature_name] = feature_calc
|
|
116
|
+
if self.features_coh.max_fband:
|
|
117
|
+
feature_calc = np.max(
|
|
118
|
+
coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])]
|
|
119
|
+
)
|
|
120
|
+
feature_name = "_".join(
|
|
121
|
+
[
|
|
122
|
+
coh_name,
|
|
123
|
+
self.ch_1,
|
|
124
|
+
"to",
|
|
125
|
+
self.ch_2,
|
|
126
|
+
"max_fband",
|
|
127
|
+
self.fband_names[idx],
|
|
128
|
+
]
|
|
129
|
+
)
|
|
130
|
+
feature_results[feature_name] = feature_calc
|
|
131
|
+
if self.features_coh.max_allfbands:
|
|
132
|
+
feature_calc = self.f[np.argmax(coh_val)]
|
|
133
|
+
feature_name = "_".join(
|
|
134
|
+
[
|
|
135
|
+
coh_name,
|
|
136
|
+
self.ch_1,
|
|
137
|
+
"to",
|
|
138
|
+
self.ch_2,
|
|
139
|
+
"max_allfbands",
|
|
140
|
+
self.fband_names[idx],
|
|
141
|
+
]
|
|
142
|
+
)
|
|
143
|
+
feature_results[feature_name] = feature_calc
|
|
144
|
+
return feature_results
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class Coherence(NMFeature):
|
|
148
|
+
def __init__(
|
|
149
|
+
self, settings: "NMSettings", ch_names: list[str], sfreq: float
|
|
150
|
+
) -> None:
|
|
151
|
+
self.settings = settings.coherence_settings
|
|
152
|
+
self.frequency_ranges_hz = settings.frequency_ranges_hz
|
|
153
|
+
self.sfreq = sfreq
|
|
154
|
+
self.ch_names = ch_names
|
|
155
|
+
self.coherence_objects: Iterable[CoherenceObject] = []
|
|
156
|
+
|
|
157
|
+
self.test_settings(settings, ch_names, sfreq)
|
|
158
|
+
|
|
159
|
+
for idx_coh in range(len(self.settings.channels)):
|
|
160
|
+
fband_names = self.settings.frequency_bands
|
|
161
|
+
fband_specs = []
|
|
162
|
+
for band_name in fband_names:
|
|
163
|
+
fband_specs.append(self.frequency_ranges_hz[band_name])
|
|
164
|
+
|
|
165
|
+
ch_1_name = self.settings.channels[idx_coh][0]
|
|
166
|
+
ch_1_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_1_name)][
|
|
167
|
+
0
|
|
168
|
+
]
|
|
169
|
+
ch_1_idx = self.ch_names.index(ch_1_name_reref)
|
|
170
|
+
|
|
171
|
+
ch_2_name = self.settings.channels[idx_coh][1]
|
|
172
|
+
ch_2_name_reref = [ch for ch in self.ch_names if ch.startswith(ch_2_name)][
|
|
173
|
+
0
|
|
174
|
+
]
|
|
175
|
+
ch_2_idx = self.ch_names.index(ch_2_name_reref)
|
|
176
|
+
|
|
177
|
+
self.coherence_objects.append(
|
|
178
|
+
CoherenceObject(
|
|
179
|
+
sfreq,
|
|
180
|
+
"hann",
|
|
181
|
+
fband_specs,
|
|
182
|
+
fband_names,
|
|
183
|
+
ch_1_name,
|
|
184
|
+
ch_2_name,
|
|
185
|
+
ch_1_idx,
|
|
186
|
+
ch_2_idx,
|
|
187
|
+
self.settings.method.coh,
|
|
188
|
+
self.settings.method.icoh,
|
|
189
|
+
self.settings.features,
|
|
190
|
+
)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def test_settings(
|
|
195
|
+
settings: "NMSettings",
|
|
196
|
+
ch_names: Iterable[str],
|
|
197
|
+
sfreq: float,
|
|
198
|
+
):
|
|
199
|
+
flat_channels = [
|
|
200
|
+
ch for ch_pair in settings.coherence_settings.channels for ch in ch_pair
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
valid_coh_channel = [
|
|
204
|
+
sum(ch.startswith(ch_coh) for ch in ch_names) for ch_coh in flat_channels
|
|
205
|
+
]
|
|
206
|
+
for ch_idx, ch_coh in enumerate(flat_channels):
|
|
207
|
+
if valid_coh_channel[ch_idx] == 0:
|
|
208
|
+
raise RuntimeError(
|
|
209
|
+
f"Coherence selected channel {ch_coh} does not match any channel name: \n"
|
|
210
|
+
f" - settings.coherence_settings.channels: {settings.coherence_settings.channels}\n"
|
|
211
|
+
f" - ch_names: {ch_names} \n"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
if valid_coh_channel[ch_idx] > 1:
|
|
215
|
+
raise RuntimeError(
|
|
216
|
+
f"Coherence selected channel {ch_coh} is ambigous and matches more than one channel name: \n"
|
|
217
|
+
f" - settings.coherence_settings.channels: {settings.coherence_settings.channels}\n"
|
|
218
|
+
f" - ch_names: {ch_names} \n"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
assert all(
|
|
222
|
+
f_band_coh in settings.frequency_ranges_hz
|
|
223
|
+
for f_band_coh in settings.coherence_settings.frequency_bands
|
|
224
|
+
), (
|
|
225
|
+
"coherence selected frequency bands don't match the ones"
|
|
226
|
+
"specified in s['frequency_ranges_hz']"
|
|
227
|
+
f"coherence frequency bands: {settings.coherence_settings.frequency_bands}"
|
|
228
|
+
f"specified frequency_ranges_hz: {settings.frequency_ranges_hz}"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
assert all(
|
|
232
|
+
settings.frequency_ranges_hz[fb][0] < sfreq / 2
|
|
233
|
+
and settings.frequency_ranges_hz[fb][1] < sfreq / 2
|
|
234
|
+
for fb in settings.coherence_settings.frequency_bands
|
|
235
|
+
), (
|
|
236
|
+
"the coherence frequency band ranges need to be smaller than the Nyquist frequency"
|
|
237
|
+
f"got sfreq = {sfreq} and fband ranges {settings.coherence_settings.frequency_bands}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if not settings.coherence_settings.method.get_enabled():
|
|
241
|
+
logger.warn(
|
|
242
|
+
"feature coherence enabled, but no coherence['method'] selected"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
246
|
+
feature_results = {}
|
|
247
|
+
|
|
248
|
+
for coh_obj in self.coherence_objects:
|
|
249
|
+
feature_results = coh_obj.get_coh(
|
|
250
|
+
feature_results,
|
|
251
|
+
data[coh_obj.ch_1_idx, :],
|
|
252
|
+
data[coh_obj.ch_2_idx, :],
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
return feature_results
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import Type, TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from py_neuromodulation.utils.types import NMFeature, FeatureName
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
import numpy as np
|
|
7
|
+
from py_neuromodulation import NMSettings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
FEATURE_DICT: dict[FeatureName | str, str] = {
|
|
11
|
+
"raw_hjorth": "Hjorth",
|
|
12
|
+
"return_raw": "Raw",
|
|
13
|
+
"bandpass_filter": "BandPower",
|
|
14
|
+
"stft": "STFT",
|
|
15
|
+
"fft": "FFT",
|
|
16
|
+
"welch": "Welch",
|
|
17
|
+
"sharpwave_analysis": "SharpwaveAnalyzer",
|
|
18
|
+
"fooof": "FooofAnalyzer",
|
|
19
|
+
"nolds": "Nolds",
|
|
20
|
+
"coherence": "Coherence",
|
|
21
|
+
"bursts": "Bursts",
|
|
22
|
+
"linelength": "LineLength",
|
|
23
|
+
"mne_connectivity": "MNEConnectivity",
|
|
24
|
+
"bispectrum": "Bispectra",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FeatureProcessors:
|
|
29
|
+
"""Class for storing NMFeature objects and calculating features during processing"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self, settings: "NMSettings", ch_names: list[str], sfreq: float
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize FeatureProcessors object with settings, channel names and sampling frequency.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
settings (NMSettings): PyNM settings object
|
|
38
|
+
ch_names (list[str]): list of channel names
|
|
39
|
+
sfreq (float): sampling frequency in Hz
|
|
40
|
+
"""
|
|
41
|
+
from py_neuromodulation import user_features
|
|
42
|
+
from importlib import import_module
|
|
43
|
+
|
|
44
|
+
# Accept 'str' for custom features
|
|
45
|
+
self.features: dict[FeatureName | str, NMFeature] = {
|
|
46
|
+
feature_name: getattr(
|
|
47
|
+
import_module("py_neuromodulation.features"), FEATURE_DICT[feature_name]
|
|
48
|
+
)(settings, ch_names, sfreq)
|
|
49
|
+
for feature_name in settings.features.get_enabled()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for feature_name, feature in user_features.items():
|
|
53
|
+
self.features[feature_name] = feature(settings, ch_names, sfreq)
|
|
54
|
+
|
|
55
|
+
def register_new_feature(self, feature_name: str, feature: NMFeature) -> None:
|
|
56
|
+
"""Register new feature.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
feature : features_abc.Feature
|
|
61
|
+
New feature to add to feature list
|
|
62
|
+
"""
|
|
63
|
+
self.features[feature_name] = feature # type: ignore
|
|
64
|
+
|
|
65
|
+
def estimate_features(self, data: "np.ndarray") -> dict:
|
|
66
|
+
"""Calculate features, as defined in settings.json
|
|
67
|
+
Features are based on bandpower, raw Hjorth parameters and sharp wave
|
|
68
|
+
characteristics.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
data (np array) : (channels, time)
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
dat (dict): naming convention : channel_method_feature_(f_band)
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
feature_results: dict = {}
|
|
80
|
+
|
|
81
|
+
for feature in self.features.values():
|
|
82
|
+
feature_results.update(feature.calc_feature(data))
|
|
83
|
+
|
|
84
|
+
return feature_results
|
|
85
|
+
|
|
86
|
+
def get_feature(self, fname: FeatureName) -> NMFeature:
|
|
87
|
+
return self.features[fname]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def add_custom_feature(feature_name: str, new_feature: Type[NMFeature]):
|
|
91
|
+
"""Add a custom feature to the dictionary of user-defined features.
|
|
92
|
+
The feature will be automatically enabled in the settings,
|
|
93
|
+
and computed when the Stream.run() method is called.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
feature_name (str): A name for the feature that will be used to
|
|
97
|
+
enable/disable the feature in settings and to store the feature
|
|
98
|
+
class instance in the DataProcessor
|
|
99
|
+
|
|
100
|
+
new_feature (NMFeature): Class that implements the user-defined
|
|
101
|
+
feature. It should implement the NMSettings protocol (defined
|
|
102
|
+
in this file).
|
|
103
|
+
"""
|
|
104
|
+
from py_neuromodulation import user_features
|
|
105
|
+
from py_neuromodulation import NMSettings
|
|
106
|
+
|
|
107
|
+
user_features[feature_name] = new_feature
|
|
108
|
+
NMSettings._add_feature(feature_name)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def remove_custom_feature(feature_name: str):
|
|
112
|
+
"""Remove a custom feature from the dictionary of user-defined features.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
feature_name (str): Name of the feature to remove
|
|
116
|
+
"""
|
|
117
|
+
from py_neuromodulation import user_features
|
|
118
|
+
from py_neuromodulation import NMSettings
|
|
119
|
+
|
|
120
|
+
user_features.pop(feature_name)
|
|
121
|
+
NMSettings._remove_feature(feature_name)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from py_neuromodulation.utils.types import (
|
|
7
|
+
NMBaseModel,
|
|
8
|
+
NMFeature,
|
|
9
|
+
BoolSelector,
|
|
10
|
+
FrequencyRange,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from py_neuromodulation import NMSettings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FooofAperiodicSettings(BoolSelector):
|
|
18
|
+
exponent: bool = True
|
|
19
|
+
offset: bool = True
|
|
20
|
+
knee: bool = True
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FooofPeriodicSettings(BoolSelector):
|
|
24
|
+
center_frequency: bool = False
|
|
25
|
+
band_width: bool = False
|
|
26
|
+
height_over_ap: bool = False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FooofSettings(NMBaseModel):
|
|
30
|
+
aperiodic: FooofAperiodicSettings = FooofAperiodicSettings()
|
|
31
|
+
periodic: FooofPeriodicSettings = FooofPeriodicSettings()
|
|
32
|
+
windowlength_ms: float = 800
|
|
33
|
+
peak_width_limits: FrequencyRange = FrequencyRange(0.5, 12)
|
|
34
|
+
max_n_peaks: int = 3
|
|
35
|
+
min_peak_height: float = 0
|
|
36
|
+
peak_threshold: float = 2
|
|
37
|
+
freq_range_hz: FrequencyRange = FrequencyRange(2, 40)
|
|
38
|
+
knee: bool = True
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FooofAnalyzer(NMFeature):
|
|
42
|
+
feat_name_map = {
|
|
43
|
+
"exponent": "exp",
|
|
44
|
+
"offset": "offset",
|
|
45
|
+
"knee": "knee_frequency",
|
|
46
|
+
"center_frequency": "cf",
|
|
47
|
+
"band_width": "bw",
|
|
48
|
+
"height_over_ap": "pw",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self, settings: "NMSettings", ch_names: Iterable[str], sfreq: float
|
|
53
|
+
) -> None:
|
|
54
|
+
self.settings = settings.fooof_settings
|
|
55
|
+
self.sfreq = sfreq
|
|
56
|
+
self.ch_names = ch_names
|
|
57
|
+
|
|
58
|
+
self.ap_mode = "knee" if self.settings.knee else "fixed"
|
|
59
|
+
|
|
60
|
+
self.num_samples = int(self.settings.windowlength_ms * sfreq / 1000)
|
|
61
|
+
|
|
62
|
+
self.f_vec = np.arange(0, int(self.num_samples / 2) + 1, 1)
|
|
63
|
+
|
|
64
|
+
assert (
|
|
65
|
+
self.settings.windowlength_ms <= settings.segment_length_features_ms
|
|
66
|
+
), f"fooof windowlength_ms ({settings.fooof.windowlength_ms}) needs to be smaller equal than segment_length_features_ms ({settings.segment_length_features_ms})."
|
|
67
|
+
|
|
68
|
+
assert (
|
|
69
|
+
self.settings.freq_range_hz[0] < sfreq
|
|
70
|
+
and self.settings.freq_range_hz[1] < sfreq
|
|
71
|
+
), f"fooof frequency range needs to be below sfreq, got {settings.fooof.freq_range_hz}"
|
|
72
|
+
|
|
73
|
+
from fooof import FOOOFGroup
|
|
74
|
+
|
|
75
|
+
self.fm = FOOOFGroup(
|
|
76
|
+
aperiodic_mode=self.ap_mode,
|
|
77
|
+
peak_width_limits=tuple(self.settings.peak_width_limits),
|
|
78
|
+
max_n_peaks=self.settings.max_n_peaks,
|
|
79
|
+
min_peak_height=self.settings.min_peak_height,
|
|
80
|
+
peak_threshold=self.settings.peak_threshold,
|
|
81
|
+
verbose=False,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
85
|
+
from scipy.fft import rfft
|
|
86
|
+
|
|
87
|
+
spectra = np.abs(rfft(data[:, -self.num_samples :])) # type: ignore
|
|
88
|
+
|
|
89
|
+
self.fm.fit(self.f_vec, spectra, self.settings.freq_range_hz)
|
|
90
|
+
|
|
91
|
+
if not self.fm.has_model or self.fm.null_inds_ is None:
|
|
92
|
+
raise RuntimeError("FOOOF failed to fit model to data.")
|
|
93
|
+
|
|
94
|
+
failed_fits: list[int] = self.fm.null_inds_
|
|
95
|
+
|
|
96
|
+
feature_results = {}
|
|
97
|
+
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
98
|
+
FIT_PASSED = ch_idx not in failed_fits
|
|
99
|
+
exp = self.fm.get_params("aperiodic_params", "exponent")[ch_idx]
|
|
100
|
+
|
|
101
|
+
for feat in self.settings.aperiodic.get_enabled():
|
|
102
|
+
f_name = f"{ch_name}_fooof_a_{self.feat_name_map[feat]}"
|
|
103
|
+
|
|
104
|
+
if not FIT_PASSED:
|
|
105
|
+
feature_results[f_name] = None
|
|
106
|
+
|
|
107
|
+
elif feat == "knee" and exp == 0:
|
|
108
|
+
feature_results[f_name] = None
|
|
109
|
+
|
|
110
|
+
else:
|
|
111
|
+
params = self.fm.get_params("aperiodic_params", feat)[ch_idx]
|
|
112
|
+
if feat == "knee":
|
|
113
|
+
# If knee parameter is negative, set knee frequency to 0
|
|
114
|
+
if params < 0:
|
|
115
|
+
params = 0
|
|
116
|
+
else:
|
|
117
|
+
params = params ** (1 / exp)
|
|
118
|
+
|
|
119
|
+
feature_results[f_name] = np.nan_to_num(params)
|
|
120
|
+
|
|
121
|
+
peaks_dict: dict[str, np.ndarray | None] = {
|
|
122
|
+
"bw": self.fm.get_params("peak_params", "BW") if FIT_PASSED else None,
|
|
123
|
+
"cf": self.fm.get_params("peak_params", "CF") if FIT_PASSED else None,
|
|
124
|
+
"pw": self.fm.get_params("peak_params", "PW") if FIT_PASSED else None,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if type(peaks_dict["bw"]) is np.float64 or peaks_dict["bw"] is None:
|
|
128
|
+
peaks_dict["bw"] = [peaks_dict["bw"]]
|
|
129
|
+
peaks_dict["cf"] = [peaks_dict["cf"]]
|
|
130
|
+
peaks_dict["pw"] = [peaks_dict["pw"]]
|
|
131
|
+
|
|
132
|
+
for peak_idx in range(self.settings.max_n_peaks):
|
|
133
|
+
for feat in self.settings.periodic.get_enabled():
|
|
134
|
+
f_name = f"{ch_name}_fooof_p_{peak_idx}_{self.feat_name_map[feat]}"
|
|
135
|
+
|
|
136
|
+
feature_results[f_name] = (
|
|
137
|
+
peaks_dict[self.feat_name_map[feat]][peak_idx]
|
|
138
|
+
if peak_idx < len(peaks_dict[self.feat_name_map[feat]])
|
|
139
|
+
else None
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return feature_results
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reference: B Hjorth
|
|
3
|
+
EEG analysis based on time domain properties
|
|
4
|
+
Electroencephalogr Clin Neurophysiol. 1970 Sep;29(3):306-10.
|
|
5
|
+
DOI: 10.1016/0013-4694(70)90143-4
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
import numpy as np
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
from py_neuromodulation.utils.types import NMFeature
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from py_neuromodulation import NMSettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Hjorth(NMFeature):
|
|
19
|
+
def __init__(
|
|
20
|
+
self, settings: "NMSettings", ch_names: Sequence[str], sfreq: float
|
|
21
|
+
) -> None:
|
|
22
|
+
self.ch_names = ch_names
|
|
23
|
+
|
|
24
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
25
|
+
var = np.var(data, axis=-1)
|
|
26
|
+
deriv1 = np.diff(data, axis=-1)
|
|
27
|
+
deriv2 = np.diff(deriv1, axis=-1)
|
|
28
|
+
deriv1_var = np.var(deriv1, axis=-1)
|
|
29
|
+
deriv2_var = np.var(deriv2, axis=-1)
|
|
30
|
+
deriv1_mobility = np.sqrt(deriv2_var / deriv1_var)
|
|
31
|
+
|
|
32
|
+
activity = np.nan_to_num(var)
|
|
33
|
+
mobility = np.nan_to_num(np.sqrt(deriv1_var / var))
|
|
34
|
+
complexity = np.nan_to_num(deriv1_mobility / mobility)
|
|
35
|
+
|
|
36
|
+
feature_results = {}
|
|
37
|
+
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
38
|
+
feature_results[f"{ch_name}_RawHjorth_Activity"] = activity[ch_idx]
|
|
39
|
+
feature_results[f"{ch_name}_RawHjorth_Mobility"] = mobility[ch_idx]
|
|
40
|
+
feature_results[f"{ch_name}_RawHjorth_Complexity"] = complexity[ch_idx]
|
|
41
|
+
|
|
42
|
+
return feature_results
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class Raw(NMFeature):
|
|
46
|
+
def __init__(
|
|
47
|
+
self, settings: "NMSettings", ch_names: Sequence[str], sfreq: float
|
|
48
|
+
) -> None:
|
|
49
|
+
self.ch_names = ch_names
|
|
50
|
+
|
|
51
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
52
|
+
feature_results = {}
|
|
53
|
+
|
|
54
|
+
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
55
|
+
feature_results["_".join([ch_name, "raw"])] = data[ch_idx, -1]
|
|
56
|
+
|
|
57
|
+
return feature_results
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
|
|
4
|
+
from py_neuromodulation.features.feature_processor import NMFeature
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class LineLength(NMFeature):
|
|
8
|
+
def __init__(self, settings: dict, ch_names: Sequence[str], sfreq: float) -> None:
|
|
9
|
+
self.ch_names = ch_names
|
|
10
|
+
|
|
11
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
12
|
+
|
|
13
|
+
line_length = np.mean(
|
|
14
|
+
np.abs(np.diff(data, axis=-1)) / (data.shape[1] - 1), axis=-1
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
feature_results = {}
|
|
18
|
+
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
19
|
+
feature_results[f"{ch_name}_LineLength"] = line_length[ch_idx]
|
|
20
|
+
|
|
21
|
+
return feature_results
|