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
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from scipy import signal
|
|
3
|
-
from mne.filter import create_filter
|
|
4
|
-
from typing import Iterable
|
|
5
|
-
|
|
6
|
-
from py_neuromodulation import nm_features_abc
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class NoValidTroughException(Exception):
|
|
10
|
-
pass
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class SharpwaveAnalyzer(nm_features_abc.Feature):
|
|
14
|
-
def __init__(
|
|
15
|
-
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
16
|
-
) -> None:
|
|
17
|
-
|
|
18
|
-
self.sw_settings = settings["sharpwave_analysis_settings"]
|
|
19
|
-
self.sfreq = sfreq
|
|
20
|
-
self.ch_names = ch_names
|
|
21
|
-
self.list_filter = []
|
|
22
|
-
for filter_range in settings["sharpwave_analysis_settings"][
|
|
23
|
-
"filter_ranges_hz"
|
|
24
|
-
]:
|
|
25
|
-
|
|
26
|
-
if filter_range[0] is None:
|
|
27
|
-
self.list_filter.append(("no_filter", None))
|
|
28
|
-
else:
|
|
29
|
-
self.list_filter.append(
|
|
30
|
-
(
|
|
31
|
-
f"range_{filter_range[0]}_{filter_range[1]}",
|
|
32
|
-
create_filter(
|
|
33
|
-
None,
|
|
34
|
-
sfreq,
|
|
35
|
-
l_freq=filter_range[0],
|
|
36
|
-
h_freq=filter_range[1],
|
|
37
|
-
fir_design="firwin",
|
|
38
|
-
# l_trans_bandwidth=None,
|
|
39
|
-
# h_trans_bandwidth=None,
|
|
40
|
-
# filter_length=str(sfreq) + "ms",
|
|
41
|
-
verbose=False,
|
|
42
|
-
),
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
# initialize used features
|
|
47
|
-
self.used_features = list()
|
|
48
|
-
for feature_name, val in self.sw_settings["sharpwave_features"].items():
|
|
49
|
-
if val is True:
|
|
50
|
-
self.used_features.append(feature_name)
|
|
51
|
-
|
|
52
|
-
# initialize attributes
|
|
53
|
-
self._initialize_sw_features()
|
|
54
|
-
|
|
55
|
-
# initializing estimator functions, respecitive for all sharpwave features
|
|
56
|
-
fun_names = []
|
|
57
|
-
for used_feature in self.used_features:
|
|
58
|
-
estimator_list_feature = (
|
|
59
|
-
[]
|
|
60
|
-
) # one feature can have multiple estimators
|
|
61
|
-
for estimator, est_features in self.sw_settings[
|
|
62
|
-
"estimator"
|
|
63
|
-
].items():
|
|
64
|
-
if est_features is not None:
|
|
65
|
-
for est_feature in est_features:
|
|
66
|
-
if used_feature == est_feature:
|
|
67
|
-
estimator_list_feature.append(estimator)
|
|
68
|
-
fun_names.append(estimator_list_feature)
|
|
69
|
-
|
|
70
|
-
self.estimator_names = fun_names
|
|
71
|
-
self.estimator_functions = [
|
|
72
|
-
[
|
|
73
|
-
getattr(np, est_name)
|
|
74
|
-
for est_name in self.estimator_names[feature_idx]
|
|
75
|
-
]
|
|
76
|
-
for feature_idx in range(len(self.estimator_names))
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
def _initialize_sw_features(self) -> None:
|
|
80
|
-
"""Resets used attributes to empty lists"""
|
|
81
|
-
for feature_name in self.used_features:
|
|
82
|
-
setattr(self, feature_name, list())
|
|
83
|
-
if "trough" not in self.used_features:
|
|
84
|
-
# trough attribute is still necessary, even if it is not specified in settings
|
|
85
|
-
self.trough = list()
|
|
86
|
-
self.troughs_idx = list()
|
|
87
|
-
|
|
88
|
-
def _get_peaks_around(self, trough_ind, arr_ind_peaks, filtered_dat):
|
|
89
|
-
"""Find the closest peaks to the right and left side a given trough.
|
|
90
|
-
|
|
91
|
-
Parameters
|
|
92
|
-
----------
|
|
93
|
-
trough_ind (int): index of trough
|
|
94
|
-
arr_ind_peaks (np.ndarray): array of peak indices
|
|
95
|
-
filtered_dat (np.ndarray): raw data batch
|
|
96
|
-
|
|
97
|
-
Raises:
|
|
98
|
-
NoValidTroughException: Returned if no adjacent peak can be found
|
|
99
|
-
|
|
100
|
-
Returns
|
|
101
|
-
-------
|
|
102
|
-
peak_left_idx (np.ndarray): index of left peak
|
|
103
|
-
peak_right_idx (np.ndarray): index of right peak
|
|
104
|
-
peak_left_val (np.ndarray): value of left peak
|
|
105
|
-
peak_right_val (np.ndarray): value of righ peak
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
try: peak_right_idx = arr_ind_peaks[arr_ind_peaks > trough_ind][0]
|
|
109
|
-
except IndexError: raise NoValidTroughException("No valid trough")
|
|
110
|
-
|
|
111
|
-
try: peak_left_idx = arr_ind_peaks[arr_ind_peaks < trough_ind][-1]
|
|
112
|
-
except IndexError: raise NoValidTroughException("No valid trough")
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
peak_left_idx,
|
|
116
|
-
peak_right_idx,
|
|
117
|
-
filtered_dat[peak_left_idx],
|
|
118
|
-
filtered_dat[peak_right_idx],
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
def calc_feature(
|
|
122
|
-
self,
|
|
123
|
-
data: np.array,
|
|
124
|
-
features_compute: dict,
|
|
125
|
-
) -> dict:
|
|
126
|
-
"""Given a new data batch, the peaks, troughs and sharpwave features
|
|
127
|
-
are estimated. Importantly only new data is being analyzed here. In
|
|
128
|
-
steps of 1/settings["sampling_rate_features] analyzed and returned.
|
|
129
|
-
Pre-initialized filters are applied to each channel.
|
|
130
|
-
|
|
131
|
-
Parameters
|
|
132
|
-
----------
|
|
133
|
-
data (np.ndarray): 2d data array with shape [num_channels, samples]
|
|
134
|
-
features_compute (dict): Features.py estimated features
|
|
135
|
-
|
|
136
|
-
Returns
|
|
137
|
-
-------
|
|
138
|
-
features_compute (dict): set features for Features.py object
|
|
139
|
-
"""
|
|
140
|
-
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
141
|
-
for filter_name, filter in self.list_filter:
|
|
142
|
-
self.data_process_sw = (data[ch_idx, :]
|
|
143
|
-
if filter_name == "no_filter"
|
|
144
|
-
else signal.fftconvolve(data[ch_idx, :], filter, mode="same")
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
# check settings if troughs and peaks are analyzed
|
|
148
|
-
|
|
149
|
-
dict_ch_features = {}
|
|
150
|
-
|
|
151
|
-
for detect_troughs in [False, True]:
|
|
152
|
-
|
|
153
|
-
if detect_troughs is False:
|
|
154
|
-
if (
|
|
155
|
-
self.sw_settings["detect_peaks"]["estimate"]
|
|
156
|
-
is False
|
|
157
|
-
):
|
|
158
|
-
continue
|
|
159
|
-
key_name_pt = "Peak"
|
|
160
|
-
# the detect_troughs loop start with peaks, s.t. data does not
|
|
161
|
-
# need to be flipped
|
|
162
|
-
|
|
163
|
-
if detect_troughs is True:
|
|
164
|
-
if (
|
|
165
|
-
self.sw_settings["detect_troughs"]["estimate"]
|
|
166
|
-
is False
|
|
167
|
-
):
|
|
168
|
-
continue
|
|
169
|
-
key_name_pt = "Trough"
|
|
170
|
-
|
|
171
|
-
self.data_process_sw = -self.data_process_sw
|
|
172
|
-
|
|
173
|
-
self._initialize_sw_features() # reset sharpwave feature attriubtes to empty lists
|
|
174
|
-
self.analyze_waveform()
|
|
175
|
-
|
|
176
|
-
# for each feature take the respective fun.
|
|
177
|
-
for feature_idx, feature_name in enumerate(
|
|
178
|
-
self.used_features
|
|
179
|
-
):
|
|
180
|
-
for est_idx, estimator_name in enumerate(
|
|
181
|
-
self.estimator_names[feature_idx]
|
|
182
|
-
):
|
|
183
|
-
key_name = "_".join(
|
|
184
|
-
[
|
|
185
|
-
ch_name,
|
|
186
|
-
"Sharpwave",
|
|
187
|
-
self.estimator_names[feature_idx][
|
|
188
|
-
est_idx
|
|
189
|
-
].title(),
|
|
190
|
-
feature_name,
|
|
191
|
-
filter_name,
|
|
192
|
-
]
|
|
193
|
-
)
|
|
194
|
-
# zero check because no peaks can be also detected
|
|
195
|
-
val = (
|
|
196
|
-
self.estimator_functions[feature_idx][est_idx](
|
|
197
|
-
getattr(self, feature_name)
|
|
198
|
-
)
|
|
199
|
-
if len(getattr(self, feature_name)) != 0
|
|
200
|
-
else 0
|
|
201
|
-
)
|
|
202
|
-
if key_name not in dict_ch_features:
|
|
203
|
-
dict_ch_features[key_name] = {}
|
|
204
|
-
dict_ch_features[key_name][key_name_pt] = val
|
|
205
|
-
|
|
206
|
-
if self.sw_settings[
|
|
207
|
-
"apply_estimator_between_peaks_and_troughs"
|
|
208
|
-
]:
|
|
209
|
-
# apply between 'Trough' and 'Peak' the respective function again
|
|
210
|
-
# save only the 'est_fun' (e.g. max) between them
|
|
211
|
-
|
|
212
|
-
for idx, key_name in enumerate(dict_ch_features):
|
|
213
|
-
# the key_name stays, since the estimator function stays between peaks and troughs
|
|
214
|
-
# this array needs to be flattened
|
|
215
|
-
features_compute[key_name] = np.concatenate(
|
|
216
|
-
self.estimator_functions
|
|
217
|
-
)[idx](
|
|
218
|
-
[
|
|
219
|
-
list(dict_ch_features[key_name].values())[0],
|
|
220
|
-
list(dict_ch_features[key_name].values())[1],
|
|
221
|
-
]
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
else:
|
|
225
|
-
# otherwise, save all
|
|
226
|
-
# write all "flatted" key value pairs in features_
|
|
227
|
-
for key, value in dict_ch_features.items():
|
|
228
|
-
for key_sub, value_sub in dict_ch_features[key].items():
|
|
229
|
-
features_compute[
|
|
230
|
-
key + "_analyze_" + key_sub
|
|
231
|
-
] = value_sub
|
|
232
|
-
|
|
233
|
-
return features_compute
|
|
234
|
-
|
|
235
|
-
def analyze_waveform(self) -> None:
|
|
236
|
-
"""Given the scipy.signal.find_peaks trough/peak distance
|
|
237
|
-
settings specified sharpwave features are estimated.
|
|
238
|
-
|
|
239
|
-
Parameters
|
|
240
|
-
----------
|
|
241
|
-
|
|
242
|
-
Raises:
|
|
243
|
-
NoValidTroughException: Return if no adjacent peak can be found
|
|
244
|
-
NoValidTroughException: Return if no adjacent peak can be found
|
|
245
|
-
|
|
246
|
-
"""
|
|
247
|
-
|
|
248
|
-
peaks = signal.find_peaks(
|
|
249
|
-
self.data_process_sw,
|
|
250
|
-
distance=self.sw_settings["detect_troughs"]["distance_peaks_ms"],
|
|
251
|
-
)[0]
|
|
252
|
-
troughs = signal.find_peaks(
|
|
253
|
-
-self.data_process_sw,
|
|
254
|
-
distance=self.sw_settings["detect_troughs"]["distance_troughs_ms"],
|
|
255
|
-
)[0]
|
|
256
|
-
|
|
257
|
-
""" Find left and right peak indexes for each trough """
|
|
258
|
-
peak_pointer = 0
|
|
259
|
-
peak_idx_left = []
|
|
260
|
-
peak_idx_right = []
|
|
261
|
-
first_valid = last_valid = 0
|
|
262
|
-
|
|
263
|
-
for i, trough_idx in enumerate(troughs):
|
|
264
|
-
|
|
265
|
-
# Locate peak right of current trough
|
|
266
|
-
while peak_pointer < peaks.size and peaks[peak_pointer] < trough_idx:
|
|
267
|
-
peak_pointer += 1
|
|
268
|
-
|
|
269
|
-
if peak_pointer - 1 < 0:
|
|
270
|
-
# If trough has no peak to it's left, it's not valid
|
|
271
|
-
first_valid = i + 1 # Try with next one
|
|
272
|
-
continue
|
|
273
|
-
|
|
274
|
-
if peak_pointer == peaks.size:
|
|
275
|
-
# If we went past the end of the peaks list, trough had no peak to its right
|
|
276
|
-
continue
|
|
277
|
-
|
|
278
|
-
last_valid = i
|
|
279
|
-
peak_idx_left.append(peaks[peak_pointer - 1])
|
|
280
|
-
peak_idx_right.append(peaks[peak_pointer])
|
|
281
|
-
|
|
282
|
-
troughs = troughs[first_valid:last_valid + 1] # Remove non valid troughs
|
|
283
|
-
|
|
284
|
-
peak_idx_left = np.array(peak_idx_left, dtype=np.integer)
|
|
285
|
-
peak_idx_right = np.array(peak_idx_right, dtype=np.integer)
|
|
286
|
-
|
|
287
|
-
peak_left = self.data_process_sw[peak_idx_left]
|
|
288
|
-
peak_right = self.data_process_sw[peak_idx_right]
|
|
289
|
-
trough_values = self.data_process_sw[troughs]
|
|
290
|
-
|
|
291
|
-
# No need to store trough data as it is not used anywhere else in the program
|
|
292
|
-
# self.trough.append(trough)
|
|
293
|
-
# self.troughs_idx.append(trough_idx)
|
|
294
|
-
|
|
295
|
-
""" Calculate features (vectorized) """
|
|
296
|
-
|
|
297
|
-
if self.sw_settings["sharpwave_features"]["interval"]:
|
|
298
|
-
self.interval = np.concatenate(([0], np.diff(troughs))) * (1000 / self.sfreq)
|
|
299
|
-
|
|
300
|
-
if self.sw_settings["sharpwave_features"]["peak_left"]:
|
|
301
|
-
self.peak_left = peak_left
|
|
302
|
-
|
|
303
|
-
if self.sw_settings["sharpwave_features"]["peak_right"]:
|
|
304
|
-
self.peak_right = peak_right
|
|
305
|
-
|
|
306
|
-
if self.sw_settings["sharpwave_features"]["sharpness"]:
|
|
307
|
-
# sharpess is calculated on a +- 5 ms window
|
|
308
|
-
# valid troughs need 5 ms of margin on both siddes
|
|
309
|
-
troughs_valid = troughs[np.logical_and(
|
|
310
|
-
troughs - int(5 * (1000 / self.sfreq)) > 0,
|
|
311
|
-
troughs + int(5 * (1000 / self.sfreq)) < self.data_process_sw.shape[0])]
|
|
312
|
-
|
|
313
|
-
self.sharpness = (
|
|
314
|
-
(self.data_process_sw[troughs_valid] - self.data_process_sw[troughs_valid - int(5 * (1000 / self.sfreq))]) +
|
|
315
|
-
(self.data_process_sw[troughs_valid] - self.data_process_sw[troughs_valid + int(5 * (1000 / self.sfreq))])
|
|
316
|
-
) / 2
|
|
317
|
-
|
|
318
|
-
if (self.sw_settings["sharpwave_features"]["rise_steepness"] or
|
|
319
|
-
self.sw_settings["sharpwave_features"]["decay_steepness"]):
|
|
320
|
-
|
|
321
|
-
# steepness is calculated as the first derivative
|
|
322
|
-
steepness = np.concatenate(([0],np.diff(self.data_process_sw)))
|
|
323
|
-
|
|
324
|
-
if self.sw_settings["sharpwave_features"]["rise_steepness"]: # left peak -> trough
|
|
325
|
-
# + 1 due to python syntax, s.t. the last element is included
|
|
326
|
-
self.rise_steepness = np.array([
|
|
327
|
-
np.max(np.abs(steepness[peak_idx_left[i] : troughs[i] + 1]))
|
|
328
|
-
for i in range(trough_idx.size)
|
|
329
|
-
])
|
|
330
|
-
|
|
331
|
-
if self.sw_settings["sharpwave_features"]["decay_steepness"]: # trough -> right peak
|
|
332
|
-
self.decay_steepness = np.array([
|
|
333
|
-
np.max(np.abs(steepness[troughs[i] : peak_idx_right[i] + 1]))
|
|
334
|
-
for i in range(trough_idx.size)
|
|
335
|
-
])
|
|
336
|
-
|
|
337
|
-
if (self.sw_settings["sharpwave_features"]["rise_steepness"] and
|
|
338
|
-
self.sw_settings["sharpwave_features"]["decay_steepness"] and
|
|
339
|
-
self.sw_settings["sharpwave_features"]["slope_ratio"]):
|
|
340
|
-
self.slope_ratio = self.rise_steepness - self.decay_steepness
|
|
341
|
-
|
|
342
|
-
if self.sw_settings["sharpwave_features"]["prominence"]:
|
|
343
|
-
self.prominence = np.abs((peak_right + peak_left) / 2 - trough_values)
|
|
344
|
-
|
|
345
|
-
if self.sw_settings["sharpwave_features"]["decay_time"]:
|
|
346
|
-
self.decay_time = (peak_idx_left - troughs) * (1000 / self.sfreq) # ms
|
|
347
|
-
|
|
348
|
-
if self.sw_settings["sharpwave_features"]["rise_time"]:
|
|
349
|
-
self.rise_time = (peak_idx_right - troughs) * (1000 / self.sfreq) # ms
|
|
350
|
-
|
|
351
|
-
if self.sw_settings["sharpwave_features"]["width"]:
|
|
352
|
-
self.width = peak_idx_right - peak_idx_left # ms
|
|
353
|
-
|
|
354
|
-
@staticmethod
|
|
355
|
-
def test_settings(
|
|
356
|
-
s: dict,
|
|
357
|
-
ch_names: Iterable[str],
|
|
358
|
-
sfreq: int | float,
|
|
359
|
-
):
|
|
360
|
-
for filter_range in s["sharpwave_analysis_settings"][
|
|
361
|
-
"filter_ranges_hz"
|
|
362
|
-
]:
|
|
363
|
-
assert isinstance(
|
|
364
|
-
filter_range[0],
|
|
365
|
-
int,
|
|
366
|
-
), f"filter range needs to be of type int, got {filter_range[0]}"
|
|
367
|
-
assert isinstance(
|
|
368
|
-
filter_range[1],
|
|
369
|
-
int,
|
|
370
|
-
), f"filter range needs to be of type int, got {filter_range[1]}"
|
|
371
|
-
assert (
|
|
372
|
-
filter_range[1] > filter_range[0]
|
|
373
|
-
), f"second filter value needs to be higher than first one, got {filter_range}"
|
|
374
|
-
assert filter_range[0] < sfreq and filter_range[1] < sfreq, (
|
|
375
|
-
"filter range has to be smaller than sfreq, "
|
|
376
|
-
f"got sfreq {sfreq} and filter range {filter_range}"
|
|
377
|
-
)
|
|
378
|
-
# check if all features are also enbled via an estimator
|
|
379
|
-
used_features = list()
|
|
380
|
-
for feature_name, val in s["sharpwave_analysis_settings"][
|
|
381
|
-
"sharpwave_features"
|
|
382
|
-
].items():
|
|
383
|
-
assert isinstance(
|
|
384
|
-
val, bool
|
|
385
|
-
), f"sharpwave_feature type {feature_name} has to be of type bool, got {val}"
|
|
386
|
-
if val is True:
|
|
387
|
-
used_features.append(feature_name)
|
|
388
|
-
for used_feature in used_features:
|
|
389
|
-
estimator_list_feature = (
|
|
390
|
-
[]
|
|
391
|
-
) # one feature can have multiple estimators
|
|
392
|
-
for estimator, est_features in s["sharpwave_analysis_settings"][
|
|
393
|
-
"estimator"
|
|
394
|
-
].items():
|
|
395
|
-
if est_features is not None:
|
|
396
|
-
for est_feature in est_features:
|
|
397
|
-
if used_feature == est_feature:
|
|
398
|
-
estimator_list_feature.append(estimator)
|
|
399
|
-
assert (
|
|
400
|
-
len(estimator_list_feature) > 0
|
|
401
|
-
), f"add estimator key for {used_feature}"
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
"""Module that contains PNStream ABC."""
|
|
2
|
-
|
|
3
|
-
from abc import ABC, abstractmethod
|
|
4
|
-
import os
|
|
5
|
-
import pathlib
|
|
6
|
-
import _pickle as cPickle
|
|
7
|
-
|
|
8
|
-
from .utils import _logging # logger initialization
|
|
9
|
-
|
|
10
|
-
# Logger use in different modules: logger = logging.getLogger("PynmLogger")
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import pandas as pd
|
|
14
|
-
from sklearn import base
|
|
15
|
-
|
|
16
|
-
from py_neuromodulation import (
|
|
17
|
-
nm_features,
|
|
18
|
-
nm_IO,
|
|
19
|
-
nm_plots,
|
|
20
|
-
nm_run_analysis,
|
|
21
|
-
nm_settings,
|
|
22
|
-
)
|
|
23
|
-
|
|
24
|
-
_PathLike = str | os.PathLike
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class PNStream(ABC):
|
|
28
|
-
|
|
29
|
-
settings: dict
|
|
30
|
-
nm_channels: pd.DataFrame
|
|
31
|
-
run_analysis: nm_run_analysis.DataProcessor
|
|
32
|
-
features: nm_features.Features
|
|
33
|
-
coords: dict
|
|
34
|
-
sfreq: int | float
|
|
35
|
-
sfreq_feature: int | float = None
|
|
36
|
-
path_grids: _PathLike | None
|
|
37
|
-
model: base.BaseEstimator | None
|
|
38
|
-
sess_right: bool | None
|
|
39
|
-
verbose: bool
|
|
40
|
-
PATH_OUT: _PathLike | None
|
|
41
|
-
PATH_OUT_folder_name: _PathLike | None
|
|
42
|
-
|
|
43
|
-
def __init__(
|
|
44
|
-
self,
|
|
45
|
-
sfreq: int | float,
|
|
46
|
-
nm_channels: pd.DataFrame | _PathLike,
|
|
47
|
-
settings: dict | _PathLike | None = None,
|
|
48
|
-
line_noise: int | float | None = 50,
|
|
49
|
-
sampling_rate_features_hz: int | float | None = None,
|
|
50
|
-
path_grids: _PathLike | None = None,
|
|
51
|
-
coord_names: list | None = None,
|
|
52
|
-
coord_list: list | None = None,
|
|
53
|
-
verbose: bool = True,
|
|
54
|
-
) -> None:
|
|
55
|
-
"""Stream initialization
|
|
56
|
-
|
|
57
|
-
Parameters
|
|
58
|
-
----------
|
|
59
|
-
sfreq : int | float
|
|
60
|
-
sampling frequency of data in Hertz
|
|
61
|
-
nm_channels : pd.DataFrame | _PathLike
|
|
62
|
-
parametrization of channels (see nm_define_channels.py for initialization)
|
|
63
|
-
settings : dict | _PathLike | None, optional
|
|
64
|
-
features settings can be a dictionary or path to the nm_settings.json, by default the py_neuromodulation/nm_settings.json are read
|
|
65
|
-
line_noise : int | float | None, optional
|
|
66
|
-
line noise, by default 50
|
|
67
|
-
sampling_rate_features_hz : int | float | None, optional
|
|
68
|
-
feature sampling rate, by default None
|
|
69
|
-
path_grids : _PathLike | None, optional
|
|
70
|
-
path to grid_cortex.tsv and/or gird_subcortex.tsv, by default Non
|
|
71
|
-
coord_names : list | None, optional
|
|
72
|
-
coordinate name in the form [coord_1_name, coord_2_name, etc], by default None
|
|
73
|
-
coord_list : list | None, optional
|
|
74
|
-
coordinates in the form [[coord_1_x, coord_1_y, coord_1_z], [coord_2_x, coord_2_y, coord_2_z],], by default None
|
|
75
|
-
verbose : bool, optional
|
|
76
|
-
print out stream computation time information, by default True
|
|
77
|
-
"""
|
|
78
|
-
self.settings = self._load_settings(settings)
|
|
79
|
-
|
|
80
|
-
if sampling_rate_features_hz is not None:
|
|
81
|
-
self.settings["sampling_rate_features_hz"] = (
|
|
82
|
-
sampling_rate_features_hz
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
self.nm_channels = self._load_nm_channels(nm_channels)
|
|
86
|
-
if path_grids is None:
|
|
87
|
-
path_grids = pathlib.Path(__file__).parent.resolve()
|
|
88
|
-
self.path_grids = path_grids
|
|
89
|
-
self.verbose = verbose
|
|
90
|
-
self.sfreq = sfreq
|
|
91
|
-
self.line_noise = line_noise
|
|
92
|
-
self.coord_names = coord_names
|
|
93
|
-
self.coord_list = coord_list
|
|
94
|
-
self.sess_right = None
|
|
95
|
-
self.projection = None
|
|
96
|
-
self.model = None
|
|
97
|
-
|
|
98
|
-
self.run_analysis = nm_run_analysis.DataProcessor(
|
|
99
|
-
sfreq=self.sfreq,
|
|
100
|
-
settings=self.settings,
|
|
101
|
-
nm_channels=self.nm_channels,
|
|
102
|
-
path_grids=self.path_grids,
|
|
103
|
-
coord_names=coord_names,
|
|
104
|
-
coord_list=coord_list,
|
|
105
|
-
line_noise=line_noise,
|
|
106
|
-
verbose=self.verbose,
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
@abstractmethod
|
|
110
|
-
def run(self):
|
|
111
|
-
"""Reinitialize the stream
|
|
112
|
-
This might be handy in case the nm_channels or nm_settings changed
|
|
113
|
-
"""
|
|
114
|
-
|
|
115
|
-
self.run_analysis = nm_run_analysis.DataProcessor(
|
|
116
|
-
sfreq=self.sfreq,
|
|
117
|
-
settings=self.settings,
|
|
118
|
-
nm_channels=self.nm_channels,
|
|
119
|
-
path_grids=self.path_grids,
|
|
120
|
-
coord_names=self.coord_names,
|
|
121
|
-
coord_list=self.coord_list,
|
|
122
|
-
line_noise=self.line_noise,
|
|
123
|
-
verbose=self.verbose,
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
@abstractmethod
|
|
127
|
-
def _add_timestamp(
|
|
128
|
-
self, feature_series: pd.Series, idx: int | None = None
|
|
129
|
-
) -> pd.Series:
|
|
130
|
-
"""Add to feature_series "time" keyword
|
|
131
|
-
For Bids specify with fs_features, for real time analysis with current time stamp
|
|
132
|
-
"""
|
|
133
|
-
|
|
134
|
-
@staticmethod
|
|
135
|
-
def _get_sess_lat(coords: dict) -> bool:
|
|
136
|
-
if len(coords["cortex_left"]["positions"]) == 0:
|
|
137
|
-
return True
|
|
138
|
-
if len(coords["cortex_right"]["positions"]) == 0:
|
|
139
|
-
return False
|
|
140
|
-
raise ValueError(
|
|
141
|
-
"Either cortex_left or cortex_right positions must be provided."
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
@staticmethod
|
|
145
|
-
def _load_nm_channels(
|
|
146
|
-
nm_channels: pd.DataFrame | _PathLike,
|
|
147
|
-
) -> pd.DataFrame:
|
|
148
|
-
if not isinstance(nm_channels, pd.DataFrame):
|
|
149
|
-
nm_channels = nm_IO.load_nm_channels(nm_channels)
|
|
150
|
-
|
|
151
|
-
if nm_channels.query("used == 1 and target == 0").shape[0] == 0:
|
|
152
|
-
raise ValueError(
|
|
153
|
-
"No channels selected for analysis that have column 'used' = 1 and 'target' = 0. Please check your nm_channels"
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
return nm_channels
|
|
157
|
-
|
|
158
|
-
@staticmethod
|
|
159
|
-
def _load_settings(settings: dict | _PathLike | None) -> dict:
|
|
160
|
-
if isinstance(settings, dict):
|
|
161
|
-
return settings
|
|
162
|
-
if settings is None:
|
|
163
|
-
return nm_settings.get_default_settings()
|
|
164
|
-
return nm_IO.read_settings(str(settings))
|
|
165
|
-
|
|
166
|
-
def load_model(self, model_name: _PathLike) -> None:
|
|
167
|
-
"""Load sklearn model, that utilizes predict"""
|
|
168
|
-
with open(model_name, "rb") as fid:
|
|
169
|
-
self.model = cPickle.load(fid)
|
|
170
|
-
|
|
171
|
-
def save_after_stream(
|
|
172
|
-
self,
|
|
173
|
-
out_path_root: _PathLike | None = None,
|
|
174
|
-
folder_name: str = "sub",
|
|
175
|
-
feature_arr: pd.DataFrame | None = None,
|
|
176
|
-
) -> None:
|
|
177
|
-
"""Save features, settings, nm_channels and sidecar after run"""
|
|
178
|
-
|
|
179
|
-
if out_path_root is None:
|
|
180
|
-
out_path_root = os.getcwd()
|
|
181
|
-
# create derivate folder_name output folder if doesn't exist
|
|
182
|
-
if os.path.exists(os.path.join(out_path_root, folder_name)) is False:
|
|
183
|
-
os.makedirs(os.path.join(out_path_root, folder_name))
|
|
184
|
-
|
|
185
|
-
self.PATH_OUT = out_path_root
|
|
186
|
-
self.PATH_OUT_folder_name = folder_name
|
|
187
|
-
self.save_sidecar(out_path_root, folder_name)
|
|
188
|
-
|
|
189
|
-
if feature_arr is not None:
|
|
190
|
-
self.save_features(out_path_root, folder_name, feature_arr)
|
|
191
|
-
|
|
192
|
-
self.save_settings(out_path_root, folder_name)
|
|
193
|
-
|
|
194
|
-
self.save_nm_channels(out_path_root, folder_name)
|
|
195
|
-
|
|
196
|
-
def save_features(
|
|
197
|
-
self,
|
|
198
|
-
out_path_root: _PathLike,
|
|
199
|
-
folder_name: str,
|
|
200
|
-
feature_arr: pd.DataFrame,
|
|
201
|
-
) -> None:
|
|
202
|
-
nm_IO.save_features(feature_arr, out_path_root, folder_name)
|
|
203
|
-
|
|
204
|
-
def save_nm_channels(
|
|
205
|
-
self, out_path_root: _PathLike, folder_name: str
|
|
206
|
-
) -> None:
|
|
207
|
-
self.run_analysis.save_nm_channels(out_path_root, folder_name)
|
|
208
|
-
|
|
209
|
-
def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None:
|
|
210
|
-
self.run_analysis.save_settings(out_path_root, folder_name)
|
|
211
|
-
|
|
212
|
-
def save_sidecar(self, out_path_root: _PathLike, folder_name: str) -> None:
|
|
213
|
-
"""Save sidecar incduing fs, coords, sess_right to
|
|
214
|
-
out_path_root and subfolder 'folder_name'"""
|
|
215
|
-
additional_args = {"sess_right": self.sess_right}
|
|
216
|
-
self.run_analysis.save_sidecar(
|
|
217
|
-
out_path_root, folder_name, additional_args
|
|
218
|
-
)
|