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.
Files changed (109) hide show
  1. py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -34
  2. py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
  3. py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
  4. py_neuromodulation/__init__.py +80 -13
  5. py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +496 -531
  6. py_neuromodulation/analysis/__init__.py +4 -0
  7. py_neuromodulation/{nm_decode.py → analysis/decode.py} +918 -992
  8. py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +994 -1074
  9. py_neuromodulation/{nm_plots.py → analysis/plots.py} +627 -612
  10. py_neuromodulation/{nm_stats.py → analysis/stats.py} +458 -480
  11. py_neuromodulation/data/README +6 -6
  12. py_neuromodulation/data/dataset_description.json +8 -8
  13. py_neuromodulation/data/participants.json +32 -32
  14. py_neuromodulation/data/participants.tsv +2 -2
  15. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
  16. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
  17. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
  18. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
  19. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
  20. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
  21. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
  22. py_neuromodulation/default_settings.yaml +241 -0
  23. py_neuromodulation/features/__init__.py +31 -0
  24. py_neuromodulation/features/bandpower.py +165 -0
  25. py_neuromodulation/features/bispectra.py +157 -0
  26. py_neuromodulation/features/bursts.py +297 -0
  27. py_neuromodulation/features/coherence.py +255 -0
  28. py_neuromodulation/features/feature_processor.py +121 -0
  29. py_neuromodulation/features/fooof.py +142 -0
  30. py_neuromodulation/features/hjorth_raw.py +57 -0
  31. py_neuromodulation/features/linelength.py +21 -0
  32. py_neuromodulation/features/mne_connectivity.py +148 -0
  33. py_neuromodulation/features/nolds.py +94 -0
  34. py_neuromodulation/features/oscillatory.py +249 -0
  35. py_neuromodulation/features/sharpwaves.py +432 -0
  36. py_neuromodulation/filter/__init__.py +3 -0
  37. py_neuromodulation/filter/kalman_filter.py +67 -0
  38. py_neuromodulation/filter/kalman_filter_external.py +1890 -0
  39. py_neuromodulation/filter/mne_filter.py +128 -0
  40. py_neuromodulation/filter/notch_filter.py +93 -0
  41. py_neuromodulation/grid_cortex.tsv +40 -40
  42. py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
  43. py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
  44. py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
  45. py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
  46. py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
  47. py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
  48. py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
  49. py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
  50. py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
  51. py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
  52. py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
  53. py_neuromodulation/processing/__init__.py +10 -0
  54. py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +29 -25
  55. py_neuromodulation/processing/data_preprocessor.py +77 -0
  56. py_neuromodulation/processing/filter_preprocessing.py +78 -0
  57. py_neuromodulation/processing/normalization.py +175 -0
  58. py_neuromodulation/{nm_projection.py → processing/projection.py} +370 -394
  59. py_neuromodulation/{nm_rereference.py → processing/rereference.py} +97 -95
  60. py_neuromodulation/{nm_resample.py → processing/resample.py} +56 -50
  61. py_neuromodulation/stream/__init__.py +3 -0
  62. py_neuromodulation/stream/data_processor.py +325 -0
  63. py_neuromodulation/stream/generator.py +53 -0
  64. py_neuromodulation/stream/mnelsl_player.py +94 -0
  65. py_neuromodulation/stream/mnelsl_stream.py +120 -0
  66. py_neuromodulation/stream/settings.py +292 -0
  67. py_neuromodulation/stream/stream.py +427 -0
  68. py_neuromodulation/utils/__init__.py +2 -0
  69. py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +305 -302
  70. py_neuromodulation/utils/database.py +149 -0
  71. py_neuromodulation/utils/io.py +378 -0
  72. py_neuromodulation/utils/keyboard.py +52 -0
  73. py_neuromodulation/utils/logging.py +66 -0
  74. py_neuromodulation/utils/types.py +251 -0
  75. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/METADATA +28 -33
  76. py_neuromodulation-0.0.6.dist-info/RECORD +89 -0
  77. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/WHEEL +1 -1
  78. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/licenses/LICENSE +21 -21
  79. py_neuromodulation/FieldTrip.py +0 -589
  80. py_neuromodulation/_write_example_dataset_helper.py +0 -65
  81. py_neuromodulation/nm_EpochStream.py +0 -92
  82. py_neuromodulation/nm_IO.py +0 -417
  83. py_neuromodulation/nm_across_patient_decoding.py +0 -927
  84. py_neuromodulation/nm_bispectra.py +0 -168
  85. py_neuromodulation/nm_bursts.py +0 -198
  86. py_neuromodulation/nm_coherence.py +0 -205
  87. py_neuromodulation/nm_cohortwrapper.py +0 -435
  88. py_neuromodulation/nm_eval_timing.py +0 -239
  89. py_neuromodulation/nm_features.py +0 -116
  90. py_neuromodulation/nm_features_abc.py +0 -39
  91. py_neuromodulation/nm_filter.py +0 -219
  92. py_neuromodulation/nm_filter_preprocessing.py +0 -91
  93. py_neuromodulation/nm_fooof.py +0 -159
  94. py_neuromodulation/nm_generator.py +0 -37
  95. py_neuromodulation/nm_hjorth_raw.py +0 -73
  96. py_neuromodulation/nm_kalmanfilter.py +0 -58
  97. py_neuromodulation/nm_linelength.py +0 -33
  98. py_neuromodulation/nm_mne_connectivity.py +0 -112
  99. py_neuromodulation/nm_nolds.py +0 -93
  100. py_neuromodulation/nm_normalization.py +0 -214
  101. py_neuromodulation/nm_oscillatory.py +0 -448
  102. py_neuromodulation/nm_run_analysis.py +0 -435
  103. py_neuromodulation/nm_settings.json +0 -338
  104. py_neuromodulation/nm_settings.py +0 -68
  105. py_neuromodulation/nm_sharpwaves.py +0 -401
  106. py_neuromodulation/nm_stream_abc.py +0 -218
  107. py_neuromodulation/nm_stream_offline.py +0 -359
  108. py_neuromodulation/utils/_logging.py +0 -24
  109. py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
@@ -1,1074 +1,994 @@
1
- import os
2
- from pathlib import Path
3
- from re import VERBOSE
4
- import re
5
- from typing import Optional
6
-
7
- import _pickle as cPickle
8
- import numpy as np
9
- import pandas as pd
10
- from sklearn import base, linear_model, metrics, model_selection
11
- from scipy import stats
12
-
13
- from py_neuromodulation import nm_decode, nm_IO, nm_plots
14
-
15
- target_filter_str = {
16
- "CLEAN",
17
- "SQUARED_EMG",
18
- "SQUARED_INTERPOLATED_EMG",
19
- "SQUARED_ROTAWHEEL",
20
- "SQUARED_ROTATION" "rota_squared",
21
- }
22
- features_reverse_order_plotting = {"stft", "fft", "bandpass"}
23
-
24
-
25
- class Feature_Reader:
26
-
27
- feature_dir: str
28
- feature_list: list[str]
29
- settings: dict
30
- sidecar: dict
31
- sfreq: int
32
- line_noise: int
33
- nm_channels: pd.DataFrame
34
- feature_arr: pd.DataFrame
35
- ch_names: list[str]
36
- ch_names_ECOG: list[str]
37
- decoder: nm_decode.Decoder = None
38
-
39
- def __init__(
40
- self, feature_dir: str, feature_file: str, binarize_label: bool = True
41
- ) -> None:
42
- """Feature_Reader enables analysis methods on top of NM_reader and NM_Decoder
43
-
44
- Parameters
45
- ----------
46
- feature_dir : str, optional
47
- Path to py_neuromodulation estimated feature runs, where each feature is a folder,
48
- feature_file : str, optional
49
- specific feature run, if None it is set to the first feature folder in feature_dir
50
- binarize_label : bool
51
- binarize label, by default True
52
-
53
- """
54
- self.feature_dir = feature_dir
55
- self.feature_list = nm_IO.get_run_list_indir(self.feature_dir)
56
- if feature_file is None:
57
- self.feature_file = self.feature_list[0]
58
- else:
59
- self.feature_file = feature_file
60
-
61
- FILE_BASENAME = Path(self.feature_file).stem
62
- PATH_READ_FILE = str(
63
- Path(self.feature_dir, FILE_BASENAME, FILE_BASENAME)
64
- )
65
-
66
- self.settings = nm_IO.read_settings(PATH_READ_FILE)
67
- self.sidecar = nm_IO.read_sidecar(PATH_READ_FILE)
68
- if self.sidecar["sess_right"] is None:
69
- if "coords" in self.sidecar:
70
- if len(self.sidecar["coords"]["cortex_left"]["ch_names"]) > 0:
71
- self.sidecar["sess_right"] = False
72
- if len(self.sidecar["coords"]["cortex_right"]["ch_names"]) > 0:
73
- self.sidecar["sess_right"] = True
74
- self.sfreq = self.sidecar["sfreq"]
75
- self.nm_channels = nm_IO.read_nm_channels(PATH_READ_FILE)
76
- self.feature_arr = nm_IO.read_features(PATH_READ_FILE)
77
-
78
- self.ch_names = self.nm_channels.new_name
79
- self.used_chs = list(
80
- self.nm_channels[
81
- (self.nm_channels["target"] == 0)
82
- & (self.nm_channels["used"] == 1)
83
- ]["new_name"]
84
- )
85
- self.ch_names_ECOG = self.nm_channels.query(
86
- '(type=="ecog") and (used == 1) and (status=="good")'
87
- ).new_name.to_list()
88
-
89
- # init plotter
90
- self.nmplotter = nm_plots.NM_Plot()
91
- if self.nm_channels["target"].sum() > 0:
92
- self.label_name = self._get_target_ch()
93
- self.label = self.read_target_ch(
94
- self.feature_arr,
95
- self.label_name,
96
- binarize=binarize_label,
97
- binarize_th=0.3,
98
- )
99
-
100
- def _get_target_ch(self) -> str:
101
- target_names = list(
102
- self.nm_channels[self.nm_channels["target"] == 1]["name"]
103
- )
104
- target_clean = [
105
- target_name
106
- for target_name in target_names
107
- for filter_str in target_filter_str
108
- if filter_str.lower() in target_name.lower()
109
- ]
110
-
111
- if len(target_clean) == 0:
112
- if "ARTIFACT" not in target_names[0]:
113
- target = target_names[0]
114
- elif len(target_names) > 1:
115
- target = target_names[1]
116
- else:
117
- target = target_names[0]
118
- else:
119
- for target_ in target_clean:
120
- # try to select contralateral label
121
- if self.sidecar["sess_right"] is True and "LEFT" in target_:
122
- target = target_
123
- continue
124
- elif self.sidecar["sess_right"] is False and "RIGHT" in target_:
125
- target = target_
126
- continue
127
- if target_ == target_clean[-1]:
128
- target = target_clean[0] # set label to last element
129
- return target
130
-
131
- @staticmethod
132
- def read_target_ch(
133
- feature_arr: pd.DataFrame,
134
- label_name: str,
135
- binarize: bool = True,
136
- binarize_th: float = 0.3,
137
- ) -> None:
138
- """_summary_
139
-
140
- Parameters
141
- ----------
142
- feature_arr : pd.DataFrame
143
- _description_
144
- label_name : str
145
- _description_
146
- binarize : bool, optional
147
- _description_, by default True
148
- binarize_th : float, optional
149
- _description_, by default 0.3
150
-
151
- Returns
152
- -------
153
- _type_
154
- _description_
155
- """
156
-
157
- label = np.nan_to_num(np.array(feature_arr[label_name]))
158
- if binarize:
159
- label = label > binarize_th
160
- return label
161
-
162
- @staticmethod
163
- def filter_features(
164
- feature_columns: list,
165
- ch_name: str = None,
166
- list_feature_keywords: list[str] = None,
167
- ) -> list:
168
- """filters read features by ch_name and/or modality
169
-
170
- Parameters
171
- ----------
172
- feature_columns : list
173
- [description]
174
- ch_name : str, optional
175
- [description], by default None
176
- list_feature_keywords : list[str], optional
177
- list of feature strings that need to be in the columns, by default None
178
-
179
- Returns
180
- -------
181
- features : list
182
- column list that suffice the ch_name and list_feature_keywords
183
- """
184
-
185
- if ch_name is not None:
186
- feature_select = [i for i in list(feature_columns) if ch_name in i]
187
- else:
188
- feature_select = feature_columns
189
-
190
- if list_feature_keywords is not None:
191
- feature_select = [
192
- f
193
- for f in feature_select
194
- if any(x in f for x in list_feature_keywords)
195
- ]
196
-
197
- if (
198
- len(
199
- [
200
- mod
201
- for mod in features_reverse_order_plotting
202
- if mod in list_feature_keywords
203
- ]
204
- )
205
- > 0
206
- ):
207
- # flip list s.t. theta band is lowest in subsequent plot
208
- feature_select = feature_select[::-1]
209
-
210
- return feature_select
211
-
212
- def set_target_ch(self, ch_name: str) -> None:
213
- self.label = ch_name
214
-
215
- def normalize_features(
216
- self,
217
- ) -> pd.DataFrame:
218
- """Normalize feature_arr feature columns
219
-
220
- Returns:
221
- pd.DataFrame: z-scored feature_arr
222
- """
223
- cols_norm = [c for c in self.feature_arr.columns if "time" not in c]
224
- feature_arr_norm = stats.zscore(self.feature_arr[cols_norm], nan_policy="omit")
225
- feature_arr_norm["time"] = self.feature_arr["time"]
226
- return feature_arr_norm
227
-
228
- def plot_cort_projection(self) -> None:
229
- """_summary_
230
- """
231
-
232
- if self.sidecar["sess_right"]:
233
- ecog_strip = np.array(
234
- self.sidecar["coords"]["cortex_right"]["positions"]
235
- )
236
- else:
237
- ecog_strip = np.array(
238
- self.sidecar["coords"]["cortex_left"]["positions"]
239
- )
240
- self.nmplotter.plot_cortex(
241
- grid_cortex=np.array(self.sidecar["grid_cortex"])
242
- if "grid_cortex" in self.sidecar
243
- else None,
244
- ecog_strip=ecog_strip,
245
- grid_color=np.array(self.sidecar["proj_matrix_cortex"]).sum(axis=1)
246
- if "grid_cortex" in self.sidecar
247
- else None,
248
- set_clim=False,
249
- )
250
-
251
- def plot_target_avg_all_channels(
252
- self,
253
- ch_names_ECOG=None,
254
- list_feature_keywords: list[str] = ["stft"],
255
- epoch_len: int = 4,
256
- threshold: float = 0.1,
257
- ):
258
- """Wrapper that call plot_features_per_channel
259
- for every given ECoG channel
260
-
261
- Parameters
262
- ----------
263
- ch_names_ECOG : list, optional
264
- list of ECoG channel to plot features for, by default None
265
- list_feature_keywords : list[str], optional
266
- keywords to plot, by default ["stft"]
267
- epoch_len : int, optional
268
- epoch length in seconds, by default 4
269
- threshold : float, optional
270
- threshold for event detection, by default 0.1
271
- """
272
-
273
- if ch_names_ECOG is None:
274
- ch_names_ECOG = self.ch_names_ECOG
275
- for ch_name_ECOG in ch_names_ECOG:
276
- self.plot_target_averaged_channel(
277
- ch=ch_name_ECOG,
278
- list_feature_keywords=list_feature_keywords,
279
- epoch_len=epoch_len,
280
- threshold=threshold,
281
- )
282
-
283
- def plot_target_averaged_channel(
284
- self,
285
- ch: str = None,
286
- list_feature_keywords: Optional[list[str]] = None,
287
- features_to_plt: list = None,
288
- epoch_len: int = 4,
289
- threshold: float = 0.1,
290
- normalize_data: bool = True,
291
- show_plot: bool = True,
292
- title: str = "Movement aligned features",
293
- ytick_labelsize=None,
294
- figsize_x: float = 8,
295
- figsize_y: float = 8,
296
- ) -> None:
297
- """_summary_
298
-
299
- Parameters
300
- ----------
301
- ch : str, optional
302
- _description_, by default None
303
- list_feature_keywords : Optional[list[str]], optional
304
- _description_, by default None
305
- features_to_plt : list, optional
306
- _description_, by default None
307
- epoch_len : int, optional
308
- _description_, by default 4
309
- threshold : float, optional
310
- _description_, by default 0.1
311
- normalize_data : bool, optional
312
- _description_, by default True
313
- show_plot : bool, optional
314
- _description_, by default True
315
- title : str, optional
316
- _description_, by default "Movement aligned features"
317
- ytick_labelsize : _type_, optional
318
- _description_, by default None
319
- figsize_x : float, optional
320
- _description_, by default 8
321
- figsize_y : float, optional
322
- _description_, by default 8
323
- """
324
-
325
- # TODO: This does not work properly when we have bipolar rereferencing
326
-
327
- if features_to_plt is None:
328
-
329
- filtered_df = self.feature_arr[
330
- self.filter_features(
331
- self.feature_arr.columns, ch, list_feature_keywords
332
- )[::-1]
333
- ]
334
- else:
335
- filtered_df = self.feature_arr[features_to_plt]
336
-
337
- data = np.expand_dims(np.array(filtered_df), axis=1)
338
-
339
- X_epoch, y_epoch = self.get_epochs(
340
- data,
341
- self.label,
342
- epoch_len=epoch_len,
343
- sfreq=self.settings["sampling_rate_features_hz"],
344
- threshold=threshold,
345
- )
346
-
347
- nm_plots.plot_epochs_avg(
348
- X_epoch=X_epoch,
349
- y_epoch=y_epoch,
350
- epoch_len=epoch_len,
351
- sfreq=self.settings["sampling_rate_features_hz"],
352
- feature_names=list(filtered_df.columns),
353
- feature_str_add="_".join(list_feature_keywords)
354
- if list_feature_keywords is not None
355
- else "all",
356
- cut_ch_name_cols=True,
357
- ch_name=ch if ch is not None else None,
358
- label_name=self.label_name,
359
- normalize_data=normalize_data,
360
- show_plot=show_plot,
361
- save=True,
362
- OUT_PATH=self.feature_dir,
363
- feature_file=self.feature_file,
364
- str_title=title,
365
- ytick_labelsize=ytick_labelsize,
366
- figsize_x=figsize_x,
367
- figsize_y=figsize_y
368
- )
369
-
370
- def plot_all_features(
371
- self,
372
- ch_used: str = None,
373
- time_limit_low_s: float = None,
374
- time_limit_high_s: float = None,
375
- normalize: bool = True,
376
- save: bool = False,
377
- title="all_feature_plt.pdf",
378
- ytick_labelsize: int = 10,
379
- clim_low: float = None,
380
- clim_high: float = None,
381
- ):
382
- """_summary_
383
-
384
- Parameters
385
- ----------
386
- ch_used : str, optional
387
- _description_, by default None
388
- time_limit_low_s : float, optional
389
- _description_, by default None
390
- time_limit_high_s : float, optional
391
- _description_, by default None
392
- normalize : bool, optional
393
- _description_, by default True
394
- save : bool, optional
395
- _description_, by default False
396
- title : str, optional
397
- _description_, by default "all_feature_plt.pdf"
398
- ytick_labelsize : int, optional
399
- _description_, by default 10
400
- clim_low : float, optional
401
- _description_, by default None
402
- clim_high : float, optional
403
- _description_, by default None
404
- """
405
-
406
- if ch_used is not None:
407
- col_used = [
408
- c
409
- for c in self.feature_arr.columns
410
- if c.startswith(ch_used)
411
- or c == "time"
412
- or "LABEL" in c
413
- or "MOV" in c
414
- ]
415
- df = self.feature_arr[col_used[::-1]]
416
- else:
417
- df = self.feature_arr[self.feature_arr.columns[::-1]]
418
-
419
- nm_plots.plot_all_features(
420
- df=df,
421
- time_limit_low_s=time_limit_low_s,
422
- time_limit_high_s=time_limit_high_s,
423
- normalize=normalize,
424
- save=save,
425
- title=title,
426
- ytick_labelsize=ytick_labelsize,
427
- feature_file=self.feature_file,
428
- OUT_PATH=self.feature_dir,
429
- clim_low=clim_low,
430
- clim_high=clim_high,
431
- )
432
-
433
- @staticmethod
434
- def get_performace_sub_strip(performance_sub: dict, plt_grid: bool = False):
435
- """_summary_
436
-
437
- Parameters
438
- ----------
439
- performance_sub : dict
440
- _description_
441
- plt_grid : bool, optional
442
- _description_, by default False
443
-
444
- Returns
445
- -------
446
- _type_
447
- _description_
448
- """
449
-
450
- ecog_strip_performance = []
451
- ecog_coords_strip = []
452
- cortex_grid = []
453
- grid_performance = []
454
-
455
- channels_ = performance_sub.keys()
456
-
457
- for ch in channels_:
458
- if "grid" not in ch and "combined" not in ch:
459
- ecog_coords_strip.append(performance_sub[ch]["coord"])
460
- ecog_strip_performance.append(
461
- performance_sub[ch]["performance_test"]
462
- )
463
- elif plt_grid is True and "gridcortex_" in ch:
464
- cortex_grid.append(performance_sub[ch]["coord"])
465
- grid_performance.append(performance_sub[ch]["performance_test"])
466
-
467
- if len(ecog_coords_strip) > 0:
468
- ecog_coords_strip = np.vstack(ecog_coords_strip)
469
-
470
- return (
471
- ecog_strip_performance,
472
- ecog_coords_strip,
473
- cortex_grid,
474
- grid_performance,
475
- )
476
-
477
- def plot_across_subject_grd_ch_performance(
478
- self,
479
- performance_dict=None,
480
- plt_grid=False,
481
- feature_str_add="performance_allch_allgrid",
482
- ):
483
- ecog_strip_performance = []
484
- ecog_coords_strip = []
485
- grid_performance = []
486
- for sub in performance_dict.keys():
487
- (
488
- ecog_strip_performance_sub,
489
- ecog_coords_strip_sub,
490
- _,
491
- grid_performance_sub,
492
- ) = self.get_performace_sub_strip(
493
- performance_dict[sub], plt_grid=plt_grid
494
- )
495
- ecog_strip_performance.extend(ecog_strip_performance_sub)
496
- ecog_coords_strip.extend(ecog_coords_strip_sub)
497
- grid_performance.append(grid_performance_sub)
498
- grid_performance = list(np.vstack(grid_performance).mean(axis=0))
499
- coords_all = np.array(ecog_coords_strip)
500
- coords_all[:, 0] = np.abs(coords_all[:, 0])
501
-
502
- self.nmplotter.plot_cortex(
503
- grid_cortex=np.array(self.sidecar["grid_cortex"])
504
- if "grid_cortex" in self.sidecar
505
- else None,
506
- ecog_strip=coords_all if len(ecog_coords_strip) > 0 else None,
507
- grid_color=grid_performance if len(grid_performance) > 0 else None,
508
- strip_color=np.array(ecog_strip_performance)
509
- if len(ecog_strip_performance) > 0
510
- else None,
511
- sess_right=self.sidecar["sess_right"],
512
- save=True,
513
- OUT_PATH=self.feature_dir,
514
- feature_file=self.feature_file,
515
- feature_str_add=feature_str_add,
516
- show_plot=True,
517
- )
518
-
519
- def plot_subject_grid_ch_performance(
520
- self,
521
- subject_name=None,
522
- performance_dict=None,
523
- plt_grid=False,
524
- feature_str_add="performance_allch_allgrid",
525
- ):
526
- """plot subject specific performance for individual channeal and optional grid points
527
-
528
- Parameters
529
- ----------
530
- subject_name : string, optional
531
- used subject, by default None
532
- performance_dict : dict, optional
533
- [description], by default None
534
- plt_grid : bool, optional
535
- True to plot grid performances, by default False
536
- feature_str_add : string, optional
537
- figure output_name
538
- """
539
-
540
- ecog_strip_performance = []
541
- ecog_coords_strip = []
542
- cortex_grid = []
543
- grid_performance = []
544
-
545
- if subject_name is None:
546
- subject_name = self.feature_file[
547
- self.feature_file.find("sub-") : self.feature_file.find("_ses")
548
- ][4:]
549
-
550
- (
551
- ecog_strip_performance,
552
- ecog_coords_strip,
553
- cortex_grid,
554
- grid_performance,
555
- ) = self.get_performace_sub_strip(
556
- performance_dict[subject_name], plt_grid=plt_grid
557
- )
558
-
559
- self.nmplotter.plot_cortex(
560
- grid_cortex=np.array(self.sidecar["grid_cortex"])
561
- if "grid_cortex" in self.sidecar
562
- else None,
563
- ecog_strip=ecog_coords_strip
564
- if len(ecog_coords_strip) > 0
565
- else None,
566
- grid_color=grid_performance if len(grid_performance) > 0 else None,
567
- strip_color=ecog_strip_performance
568
- if len(ecog_strip_performance) > 0
569
- else None,
570
- sess_right=self.sidecar["sess_right"],
571
- save=True,
572
- OUT_PATH=self.feature_dir,
573
- feature_file=self.feature_file,
574
- feature_str_add=feature_str_add,
575
- show_plot=True,
576
- )
577
-
578
- def plot_feature_series_time(
579
- self,
580
- ):
581
- self.nmplotter.plot_feature_series_time(self.feature_arr)
582
-
583
- def plot_corr_matrix(
584
- self,
585
- ):
586
- return nm_plots.plot_corr_matrix(
587
- self.feature_arr,
588
- )
589
-
590
- @staticmethod
591
- def get_epochs(data, y_, epoch_len, sfreq, threshold=0) -> (np.ndarray, np.ndarray):
592
- """Return epoched data.
593
-
594
- Parameters
595
- ----------
596
- data : np.ndarray
597
- array of extracted features of shape (n_samples, n_channels, n_features)
598
- y_ : np.ndarray
599
- array of labels e.g. ones for movement and zeros for
600
- no movement or baseline corr. rotameter data
601
- epoch_len : int
602
- length of epoch in seconds
603
- sfreq : int/float
604
- sampling frequency of data
605
- threshold : int/float
606
- (Optional) threshold to be used for identifying events
607
- (default=0 for y_tr with only ones
608
- and zeros)
609
-
610
- Returns
611
- -------
612
- epoch_ : np.ndarray
613
- array of epoched ieeg data with shape (epochs,samples,channels,features)
614
- y_arr : np.ndarray
615
- array of epoched event label data with shape (epochs,samples)
616
- """
617
-
618
- epoch_lim = int(epoch_len * sfreq)
619
-
620
- ind_mov = np.where(np.diff(np.array(y_ > threshold) * 1) == 1)[0]
621
-
622
- low_limit = ind_mov > epoch_lim / 2
623
- up_limit = ind_mov < y_.shape[0] - epoch_lim / 2
624
-
625
- ind_mov = ind_mov[low_limit & up_limit]
626
-
627
- epoch_ = np.zeros(
628
- [ind_mov.shape[0], epoch_lim, data.shape[1], data.shape[2]]
629
- )
630
-
631
- y_arr = np.zeros([ind_mov.shape[0], int(epoch_lim)])
632
-
633
- for idx, i in enumerate(ind_mov):
634
-
635
- epoch_[idx, :, :, :] = data[
636
- i - epoch_lim // 2 : i + epoch_lim // 2, :, :
637
- ]
638
-
639
- y_arr[idx, :] = y_[i - epoch_lim // 2 : i + epoch_lim // 2]
640
-
641
- return epoch_, y_arr
642
-
643
- def set_decoder(
644
- self,
645
- decoder: nm_decode.Decoder = None,
646
- TRAIN_VAL_SPLIT=False,
647
- RUN_BAY_OPT=False,
648
- save_coef=False,
649
- model: base.BaseEstimator = linear_model.LogisticRegression,
650
- eval_method=metrics.r2_score,
651
- cv_method: model_selection.BaseCrossValidator = model_selection.KFold(
652
- n_splits=3, shuffle=False
653
- ),
654
- get_movement_detection_rate: bool = False,
655
- mov_detection_threshold=0.5,
656
- min_consequent_count=3,
657
- threshold_score=True,
658
- bay_opt_param_space: list = [],
659
- STACK_FEATURES_N_SAMPLES=False,
660
- time_stack_n_samples=5,
661
- use_nested_cv=False,
662
- VERBOSE=False,
663
- undersampling=False,
664
- oversampling=False,
665
- mrmr_select=False,
666
- pca=False,
667
- cca=False,
668
- ):
669
- if decoder is not None:
670
- self.decoder = decoder
671
- else:
672
-
673
- self.decoder = nm_decode.Decoder(
674
- features=self.feature_arr,
675
- label=self.label,
676
- label_name=self.label_name,
677
- used_chs=self.used_chs,
678
- model=model,
679
- eval_method=eval_method,
680
- cv_method=cv_method,
681
- threshold_score=threshold_score,
682
- TRAIN_VAL_SPLIT=TRAIN_VAL_SPLIT,
683
- RUN_BAY_OPT=RUN_BAY_OPT,
684
- save_coef=save_coef,
685
- get_movement_detection_rate=get_movement_detection_rate,
686
- min_consequent_count=min_consequent_count,
687
- mov_detection_threshold=mov_detection_threshold,
688
- bay_opt_param_space=bay_opt_param_space,
689
- STACK_FEATURES_N_SAMPLES=STACK_FEATURES_N_SAMPLES,
690
- time_stack_n_samples=time_stack_n_samples,
691
- VERBOSE=VERBOSE,
692
- use_nested_cv=use_nested_cv,
693
- undersampling=undersampling,
694
- oversampling=oversampling,
695
- mrmr_select=mrmr_select,
696
- sfreq=self.sfreq,
697
- pca=pca,
698
- cca=cca,
699
- )
700
-
701
- def run_ML_model(
702
- self,
703
- feature_file: str = None,
704
- estimate_gridpoints: bool = False,
705
- estimate_channels: bool = True,
706
- estimate_all_channels_combined: bool = False,
707
- output_name: str = "LM",
708
- save_results: bool = True,
709
- ):
710
- """machine learning model evaluation for ECoG strip channels and/or grid points
711
-
712
- Parameters
713
- ----------
714
- feature_file : string, optional
715
- [description], by default None
716
- estimate_gridpoints : bool, optional
717
- run ML analysis for grid points, by default True
718
- estimate_channels : bool, optional
719
- run ML analysis for ECoG strip channel, by default True
720
- estimate_all_channels_combined : bool, optional
721
- run ML analysis features of all channels concatenated, by default False
722
- model : sklearn model, optional
723
- ML model, needs to obtain fit and predict functions,
724
- by default linear_model.LogisticRegression(class_weight="balanced")
725
- eval_method : sklearn.metrics, optional
726
- evaluation performance metric, by default metrics.balanced_accuracy_score
727
- cv_method : sklearn.model_selection, optional
728
- valdation strategy, by default model_selection.KFold(n_splits=3, shuffle=False)
729
- output_name : str, optional
730
- saving name, by default "LM"
731
- save_results : boolean
732
- if true, save model._coef trained coefficients
733
- """
734
- if feature_file is None:
735
- feature_file = self.feature_file
736
-
737
- if estimate_gridpoints:
738
- self.decoder.set_data_grid_points()
739
- _ = self.decoder.run_CV_caller("grid_points")
740
- if estimate_channels:
741
- self.decoder.set_data_ind_channels()
742
- _ = self.decoder.run_CV_caller("ind_channels")
743
- if estimate_all_channels_combined:
744
- _ = self.decoder.run_CV_caller("all_channels_combined")
745
-
746
- if save_results:
747
- self.decoder.save(
748
- self.feature_dir,
749
- self.feature_file
750
- if ".vhdr" in self.feature_file
751
- else self.feature_file,
752
- output_name,
753
- )
754
-
755
- return self.read_results(
756
- read_grid_points=estimate_gridpoints,
757
- read_all_combined=estimate_all_channels_combined,
758
- read_channels=estimate_channels,
759
- ML_model_name=output_name,
760
- read_mov_detection_rates=self.decoder.get_movement_detection_rate,
761
- read_bay_opt_params=self.decoder.RUN_BAY_OPT,
762
- read_mrmr=self.decoder.mrmr_select,
763
- model_save=self.decoder.model_save,
764
- )
765
-
766
- def read_results(
767
- self,
768
- performance_dict: dict = {},
769
- subject_name: str = None,
770
- DEFAULT_PERFORMANCE: float = 0.5,
771
- read_grid_points: bool = True,
772
- read_channels: bool = True,
773
- read_all_combined: bool = False,
774
- ML_model_name: str = "LM",
775
- read_mov_detection_rates: bool = False,
776
- read_bay_opt_params: bool = False,
777
- read_mrmr: bool = False,
778
- model_save: bool = False,
779
- save_results: bool = False,
780
- PATH_OUT: str = None,
781
- folder_name: str = None,
782
- str_add: str = None,
783
- ):
784
- """Save performances of a given patient into performance_dict from saved nm_decoder
785
-
786
- Parameters
787
- ----------
788
- performance_dict : dictionary
789
- dictionary including decoding performances, by default dictionary
790
- subject_name : string, optional
791
- subject name, by default None
792
- DEFAULT_PERFORMANCE : float, optional
793
- chance performance, by default 0.5
794
- read_grid_points : bool, optional
795
- true if grid point performances are read, by default True
796
- read_channels : bool, optional
797
- true if channels performances are read, by default True
798
- read_all_combined : bool, optional
799
- true if all combined channel performances are read, by default False
800
- ML_model_name : str, optional
801
- machine learning model name, by default 'LM'
802
- read_mov_detection_rates : boolean, by defaulte False
803
- if True, read movement detection rates, as well as fpr's and tpr's
804
- read_bay_opt_params : boolean, by default False
805
- read_mrmr : boolean, by default False
806
- model_save : boolean, by default False
807
- save_results : boolean, by default False
808
- PATH_OUT : string, by default None
809
- folder_name : string, by default None
810
- str_add : string, by default None
811
-
812
- Returns
813
- -------
814
- performance_dict : dictionary
815
-
816
- """
817
-
818
- if ".vhdr" in self.feature_file:
819
- feature_file = self.feature_file[: -len(".vhdr")]
820
- else:
821
- feature_file = self.feature_file
822
-
823
- if subject_name is None:
824
- subject_name = feature_file[
825
- feature_file.find("sub-") : feature_file.find("_ses")
826
- ][4:]
827
-
828
- PATH_ML_ = os.path.join(
829
- self.feature_dir,
830
- feature_file,
831
- feature_file + "_" + ML_model_name + "_ML_RES.p",
832
- )
833
-
834
- # read ML results
835
- with open(PATH_ML_, "rb") as input:
836
- ML_res = cPickle.load(input)
837
- if self.decoder is None:
838
- self.decoder = ML_res
839
-
840
- performance_dict[subject_name] = {}
841
-
842
- def write_CV_res_in_performance_dict(
843
- obj_read,
844
- obj_write,
845
- read_mov_detection_rates=read_mov_detection_rates,
846
- read_bay_opt_params=False,
847
- ):
848
- def transform_list_of_dicts_into_dict_of_lists(l_):
849
- dict_out = {}
850
- for key_, _ in l_[0].items():
851
- key_l = []
852
- for dict_ in l_:
853
- key_l.append(dict_[key_])
854
- dict_out[key_] = key_l
855
- return dict_out
856
-
857
- def read_ML_performances(
858
- obj_read, obj_write, set_inner_CV_res: bool = False
859
- ):
860
- def set_score(
861
- key_set: str,
862
- key_get: str,
863
- take_mean: bool = True,
864
- val=None,
865
- ):
866
- if set_inner_CV_res is True:
867
- key_set = "InnerCV_" + key_set
868
- key_get = "InnerCV_" + key_get
869
- if take_mean is True:
870
- val = np.mean(obj_read[key_get])
871
- obj_write[key_set] = val
872
-
873
- set_score(
874
- key_set="performance_test",
875
- key_get="score_test",
876
- take_mean=True,
877
- )
878
- set_score(
879
- key_set="performance_train",
880
- key_get="score_train",
881
- take_mean=True,
882
- )
883
-
884
- if "coef" in obj_read:
885
- set_score(
886
- key_set="coef",
887
- key_get="coef",
888
- take_mean=False,
889
- val=np.concatenate(obj_read["coef"]),
890
- )
891
-
892
- if read_mov_detection_rates:
893
- set_score(
894
- key_set="mov_detection_rates_test",
895
- key_get="mov_detection_rates_test",
896
- take_mean=True,
897
- )
898
- set_score(
899
- key_set="mov_detection_rates_train",
900
- key_get="mov_detection_rates_train",
901
- take_mean=True,
902
- )
903
- set_score(
904
- key_set="fprate_test",
905
- key_get="fprate_test",
906
- take_mean=True,
907
- )
908
- set_score(
909
- key_set="fprate_train",
910
- key_get="fprate_train",
911
- take_mean=True,
912
- )
913
- set_score(
914
- key_set="tprate_test",
915
- key_get="tprate_test",
916
- take_mean=True,
917
- )
918
- set_score(
919
- key_set="tprate_train",
920
- key_get="tprate_train",
921
- take_mean=True,
922
- )
923
-
924
- if read_bay_opt_params is True:
925
- # transform dict into keys for json saving
926
- dict_to_save = transform_list_of_dicts_into_dict_of_lists(
927
- obj_read["best_bay_opt_params"]
928
- )
929
- set_score(
930
- key_set="bay_opt_best_params",
931
- key_get=None,
932
- take_mean=False,
933
- val=dict_to_save,
934
- )
935
-
936
- if read_mrmr is True:
937
- # transform dict into keys for json saving
938
-
939
- set_score(
940
- key_set="mrmr_select",
941
- key_get=None,
942
- take_mean=False,
943
- val=obj_read["mrmr_select"],
944
- )
945
- if model_save is True:
946
- set_score(
947
- key_set="model_save",
948
- key_get=None,
949
- take_mean=False,
950
- val=obj_read["model_save"],
951
- )
952
-
953
- read_ML_performances(obj_read, obj_write)
954
-
955
- if (
956
- len([key_ for key_ in obj_read.keys() if "InnerCV_" in key_])
957
- > 0
958
- ):
959
- read_ML_performances(obj_read, obj_write, set_inner_CV_res=True)
960
-
961
- if read_channels:
962
-
963
- ch_to_use = self.ch_names_ECOG
964
- ch_to_use = self.decoder.used_chs
965
- for ch in ch_to_use:
966
-
967
- performance_dict[subject_name][ch] = {}
968
-
969
- if "coords" in self.sidecar:
970
- if (
971
- len(self.sidecar["coords"]) > 0
972
- ): # check if coords are empty
973
-
974
- coords_exist = False
975
- for cortex_loc in self.sidecar["coords"].keys():
976
- for ch_name_coord_idx, ch_name_coord in enumerate(
977
- self.sidecar["coords"][cortex_loc]["ch_names"]
978
- ):
979
- if ch.startswith(ch_name_coord):
980
- coords = self.sidecar["coords"][cortex_loc][
981
- "positions"
982
- ][ch_name_coord_idx]
983
- coords_exist = True # optimally break out of the two loops...
984
- if coords_exist is False:
985
- coords = None
986
- performance_dict[subject_name][ch]["coord"] = coords
987
- write_CV_res_in_performance_dict(
988
- ML_res.ch_ind_results[ch],
989
- performance_dict[subject_name][ch],
990
- read_mov_detection_rates=read_mov_detection_rates,
991
- read_bay_opt_params=read_bay_opt_params,
992
- )
993
-
994
- if read_all_combined:
995
- performance_dict[subject_name]["all_ch_combined"] = {}
996
- write_CV_res_in_performance_dict(
997
- ML_res.all_ch_results,
998
- performance_dict[subject_name]["all_ch_combined"],
999
- read_mov_detection_rates=read_mov_detection_rates,
1000
- read_bay_opt_params=read_bay_opt_params,
1001
- )
1002
-
1003
- if read_grid_points:
1004
- performance_dict[subject_name][
1005
- "active_gridpoints"
1006
- ] = ML_res.active_gridpoints
1007
-
1008
- for project_settings, grid_type in zip(
1009
- ["project_cortex", "project_subcortex"],
1010
- ["gridcortex_", "gridsubcortex_"],
1011
- ):
1012
- if self.settings["postprocessing"][project_settings] is False:
1013
- continue
1014
-
1015
- # the sidecar keys are grid_cortex and subcortex_grid
1016
- for grid_point in range(
1017
- len(self.sidecar["grid_" + project_settings.split("_")[1]])
1018
- ):
1019
-
1020
- gp_str = grid_type + str(grid_point)
1021
-
1022
- performance_dict[subject_name][gp_str] = {}
1023
- performance_dict[subject_name][gp_str][
1024
- "coord"
1025
- ] = self.sidecar["grid_" + project_settings.split("_")[1]][
1026
- grid_point
1027
- ]
1028
-
1029
- if gp_str in ML_res.active_gridpoints:
1030
- write_CV_res_in_performance_dict(
1031
- ML_res.gridpoint_ind_results[gp_str],
1032
- performance_dict[subject_name][gp_str],
1033
- read_mov_detection_rates=read_mov_detection_rates,
1034
- read_bay_opt_params=read_bay_opt_params,
1035
- )
1036
- else:
1037
- # set non interpolated grid point to default performance
1038
- performance_dict[subject_name][gp_str][
1039
- "performance_test"
1040
- ] = DEFAULT_PERFORMANCE
1041
- performance_dict[subject_name][gp_str][
1042
- "performance_train"
1043
- ] = DEFAULT_PERFORMANCE
1044
-
1045
- if save_results:
1046
- nm_IO.save_general_dict(
1047
- dict_=performance_dict,
1048
- path_out=PATH_OUT,
1049
- str_add=str_add,
1050
- folder_name=folder_name,
1051
- )
1052
- return performance_dict
1053
-
1054
- @staticmethod
1055
- def get_dataframe_performances(p: dict) -> pd.DataFrame:
1056
- performances = []
1057
- for sub in p.keys():
1058
- for ch in p[sub].keys():
1059
- if "active_gridpoints" in ch:
1060
- continue
1061
- dict_add = p[sub][ch].copy()
1062
- dict_add["sub"] = sub
1063
- dict_add["ch"] = ch
1064
-
1065
- if "all_ch_" in ch:
1066
- dict_add["ch_type"] = "all ch combinded"
1067
- elif "gridcortex" in ch:
1068
- dict_add["ch_type"] = "cortex grid"
1069
- else:
1070
- dict_add["ch_type"] = "electrode ch"
1071
- performances.append(dict_add)
1072
- df = pd.DataFrame(performances)
1073
-
1074
- return df
1
+ from pathlib import PurePath
2
+
3
+ import pickle
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ from sklearn.linear_model import LogisticRegression
8
+ from sklearn.metrics import r2_score
9
+ from sklearn.model_selection import KFold
10
+
11
+ from scipy.stats import zscore as scipy_zscore
12
+
13
+ from py_neuromodulation.utils import io
14
+ from py_neuromodulation.analysis import plots
15
+ from py_neuromodulation.analysis.decode import Decoder
16
+ from py_neuromodulation.utils.types import _PathLike
17
+ from py_neuromodulation.stream.settings import NMSettings
18
+
19
+
20
+ target_filter_str = {
21
+ "CLEAN",
22
+ "SQUARED_EMG",
23
+ "SQUARED_INTERPOLATED_EMG",
24
+ "SQUARED_ROTAWHEEL",
25
+ "SQUARED_ROTATION" "rota_squared",
26
+ }
27
+ features_reverse_order_plotting = {"stft", "fft", "bandpass"}
28
+
29
+
30
+ class FeatureReader:
31
+ def __init__(
32
+ self,
33
+ feature_dir: _PathLike,
34
+ feature_file: _PathLike = "",
35
+ binarize_label: bool = True,
36
+ ) -> None:
37
+ """Feature_Reader enables analysis methods on top of NM_reader and NM_Decoder
38
+
39
+ Parameters
40
+ ----------
41
+ feature_dir : str, optional
42
+ Path to py_neuromodulation estimated feature runs, where each feature is a folder,
43
+ feature_file : str, optional
44
+ specific feature run, if None it is set to the first feature folder in feature_dir
45
+ binarize_label : bool
46
+ binarize label, by default True
47
+
48
+ """
49
+ self.feature_dir = feature_dir
50
+ self.feature_list: list[str] = io.get_run_list_indir(self.feature_dir)
51
+ self.feature_file = feature_file if feature_file else self.feature_list[0]
52
+
53
+ FILE_BASENAME = PurePath(self.feature_file).stem
54
+ PATH_READ_FILE = str(PurePath(self.feature_dir, FILE_BASENAME, FILE_BASENAME))
55
+
56
+ self.settings = NMSettings.from_file(PATH_READ_FILE)
57
+ self.sidecar = io.read_sidecar(PATH_READ_FILE)
58
+ if self.sidecar["sess_right"] is None:
59
+ if "coords" in self.sidecar:
60
+ if len(self.sidecar["coords"]["cortex_left"]["ch_names"]) > 0:
61
+ self.sidecar["sess_right"] = False
62
+ if len(self.sidecar["coords"]["cortex_right"]["ch_names"]) > 0:
63
+ self.sidecar["sess_right"] = True
64
+ self.sfreq = self.sidecar["sfreq"]
65
+ self.channels = io.read_channels(PATH_READ_FILE)
66
+ self.feature_arr = io.read_features(PATH_READ_FILE)
67
+
68
+ self.ch_names = self.channels.new_name
69
+ self.used_chs = list(
70
+ self.channels[
71
+ (self.channels["target"] == 0) & (self.channels["used"] == 1)
72
+ ]["new_name"]
73
+ )
74
+ self.ch_names_ECOG = self.channels.query(
75
+ '(type=="ecog") and (used == 1) and (status=="good")'
76
+ ).new_name.to_list()
77
+
78
+ # init plotter
79
+ self.nmplotter = plots.NM_Plot()
80
+ if self.channels["target"].sum() > 0:
81
+ self.label_name = self._get_target_ch()
82
+ self.label = self.read_target_ch(
83
+ self.feature_arr,
84
+ self.label_name,
85
+ binarize=binarize_label,
86
+ binarize_th=0.3,
87
+ )
88
+
89
+ def _get_target_ch(self) -> str:
90
+ target_names = list(self.channels[self.channels["target"] == 1]["name"])
91
+ target_clean = [
92
+ target_name
93
+ for target_name in target_names
94
+ for filter_str in target_filter_str
95
+ if filter_str.lower() in target_name.lower()
96
+ ]
97
+
98
+ if len(target_clean) == 0:
99
+ if "ARTIFACT" not in target_names[0]:
100
+ target = target_names[0]
101
+ elif len(target_names) > 1:
102
+ target = target_names[1]
103
+ else:
104
+ target = target_names[0]
105
+ else:
106
+ for target_ in target_clean:
107
+ # try to select contralateral label
108
+ if self.sidecar["sess_right"] and "LEFT" in target_:
109
+ target = target_
110
+ continue
111
+ elif not self.sidecar["sess_right"] and "RIGHT" in target_:
112
+ target = target_
113
+ continue
114
+ if target_ == target_clean[-1]:
115
+ target = target_clean[0] # set label to last element
116
+ return target
117
+
118
+ @staticmethod
119
+ def read_target_ch(
120
+ feature_arr: "pd.DataFrame",
121
+ label_name: str,
122
+ binarize: bool = True,
123
+ binarize_th: float = 0.3,
124
+ ) -> np.ndarray:
125
+ """_summary_
126
+
127
+ Parameters
128
+ ----------
129
+ feature_arr : pd.DataFrame
130
+ _description_
131
+ label_name : str
132
+ _description_
133
+ binarize : bool, optional
134
+ _description_, by default True
135
+ binarize_th : float, optional
136
+ _description_, by default 0.3
137
+
138
+ Returns
139
+ -------
140
+ _type_
141
+ _description_
142
+ """
143
+
144
+ label = np.nan_to_num(np.array(feature_arr[label_name]))
145
+ if binarize:
146
+ label = label > binarize_th
147
+ return label
148
+
149
+ @staticmethod
150
+ def filter_features(
151
+ feature_columns: list,
152
+ ch_name: str | None = None,
153
+ list_feature_keywords: list[str] | None = None,
154
+ ) -> list:
155
+ """filters read features by ch_name and/or modality
156
+
157
+ Parameters
158
+ ----------
159
+ feature_columns : list
160
+ ch_name : str, optional
161
+ list_feature_keywords : list[str], optional
162
+ list of feature strings that need to be in the columns, by default None
163
+
164
+ Returns
165
+ -------
166
+ features : list
167
+ column list that suffice the ch_name and list_feature_keywords
168
+ """
169
+
170
+ if ch_name is not None:
171
+ feature_select = [i for i in list(feature_columns) if ch_name in i]
172
+ else:
173
+ feature_select = feature_columns
174
+
175
+ if list_feature_keywords is not None:
176
+ feature_select = [
177
+ f for f in feature_select if any(x in f for x in list_feature_keywords)
178
+ ]
179
+
180
+ if (
181
+ len(
182
+ [
183
+ mod
184
+ for mod in features_reverse_order_plotting
185
+ if mod in list_feature_keywords
186
+ ]
187
+ )
188
+ > 0
189
+ ):
190
+ # flip list s.t. theta band is lowest in subsequent plot
191
+ feature_select = feature_select[::-1]
192
+
193
+ return feature_select
194
+
195
+ def set_target_ch(self, ch_name: str) -> None:
196
+ self.label_name = ch_name
197
+
198
+ def normalize_features(
199
+ self,
200
+ ) -> "pd.DataFrame":
201
+ """Normalize feature_arr feature columns
202
+
203
+ Returns:
204
+ pd.DataFrame: z-scored feature_arr
205
+ """
206
+ cols_norm = [c for c in self.feature_arr.columns if "time" not in c]
207
+ feature_arr_norm = scipy_zscore(self.feature_arr[cols_norm], nan_policy="omit")
208
+ feature_arr_norm["time"] = self.feature_arr["time"]
209
+ return feature_arr_norm
210
+
211
+ def plot_cort_projection(self) -> None:
212
+ """_summary_"""
213
+
214
+ if self.sidecar["sess_right"]:
215
+ ecog_strip = np.array(self.sidecar["coords"]["cortex_right"]["positions"])
216
+ else:
217
+ ecog_strip = np.array(self.sidecar["coords"]["cortex_left"]["positions"])
218
+ self.nmplotter.plot_cortex(
219
+ grid_cortex=np.array(self.sidecar["grid_cortex"])
220
+ if "grid_cortex" in self.sidecar
221
+ else None,
222
+ ecog_strip=ecog_strip,
223
+ grid_color=np.array(self.sidecar["proj_matrix_cortex"]).sum(axis=1)
224
+ if "grid_cortex" in self.sidecar
225
+ else None,
226
+ set_clim=False,
227
+ )
228
+
229
+ def plot_target_avg_all_channels(
230
+ self,
231
+ ch_names_ECOG=None,
232
+ list_feature_keywords: list[str] = ["stft"],
233
+ epoch_len: int = 4,
234
+ threshold: float = 0.1,
235
+ ):
236
+ """Wrapper that call plot_features_per_channel
237
+ for every given ECoG channel
238
+
239
+ Parameters
240
+ ----------
241
+ ch_names_ECOG : list, optional
242
+ list of ECoG channel to plot features for, by default None
243
+ list_feature_keywords : list[str], optional
244
+ keywords to plot, by default ["stft"]
245
+ epoch_len : int, optional
246
+ epoch length in seconds, by default 4
247
+ threshold : float, optional
248
+ threshold for event detection, by default 0.1
249
+ """
250
+
251
+ if ch_names_ECOG is None:
252
+ ch_names_ECOG = self.ch_names_ECOG
253
+ for ch_name_ECOG in ch_names_ECOG:
254
+ self.plot_target_averaged_channel(
255
+ ch=ch_name_ECOG,
256
+ list_feature_keywords=list_feature_keywords,
257
+ epoch_len=epoch_len,
258
+ threshold=threshold,
259
+ )
260
+
261
+ def plot_target_averaged_channel(
262
+ self,
263
+ ch: str = "",
264
+ list_feature_keywords: list[str] | None = None,
265
+ features_to_plt: list | None = None,
266
+ epoch_len: int = 4,
267
+ threshold: float = 0.1,
268
+ normalize_data: bool = True,
269
+ show_plot: bool = True,
270
+ title: str = "Movement aligned features",
271
+ ytick_labelsize=None,
272
+ figsize_x: float = 8,
273
+ figsize_y: float = 8,
274
+ ) -> None:
275
+ """_summary_
276
+
277
+ Parameters
278
+ ----------
279
+ ch : str, optional
280
+ list_feature_keywords : Optional[list[str]], optional
281
+ features_to_plt : list, optional
282
+ epoch_len : int, optional
283
+ threshold : float, optional
284
+ normalize_data : bool, optional
285
+ show_plot : bool, optional
286
+ title : str, optional
287
+ by default "Movement aligned features"
288
+ ytick_labelsize : _type_, optional
289
+ figsize_x : float, optional
290
+ by default 8
291
+ figsize_y : float, optional
292
+ by default 8
293
+ """
294
+
295
+ # TODO: This does not work properly when we have bipolar rereferencing
296
+
297
+ if features_to_plt is None:
298
+ filtered_df = self.feature_arr[
299
+ self.filter_features(
300
+ self.feature_arr.columns, ch, list_feature_keywords
301
+ )[::-1]
302
+ ]
303
+ else:
304
+ filtered_df = self.feature_arr[features_to_plt]
305
+
306
+ data = np.expand_dims(np.array(filtered_df), axis=1)
307
+
308
+ X_epoch, y_epoch = self.get_epochs(
309
+ data,
310
+ self.label,
311
+ epoch_len=epoch_len,
312
+ sfreq=self.settings.sampling_rate_features_hz,
313
+ threshold=threshold,
314
+ )
315
+
316
+ plots.plot_epochs_avg(
317
+ X_epoch=X_epoch,
318
+ y_epoch=y_epoch,
319
+ epoch_len=epoch_len,
320
+ sfreq=self.settings.sampling_rate_features_hz,
321
+ feature_names=list(filtered_df.columns),
322
+ feature_str_add="_".join(list_feature_keywords)
323
+ if list_feature_keywords is not None
324
+ else "all",
325
+ cut_ch_name_cols=True,
326
+ ch_name=ch,
327
+ label_name=self.label_name,
328
+ normalize_data=normalize_data,
329
+ show_plot=show_plot,
330
+ save=True,
331
+ OUT_PATH=self.feature_dir,
332
+ feature_file=self.feature_file,
333
+ str_title=title,
334
+ ytick_labelsize=ytick_labelsize,
335
+ figsize_x=figsize_x,
336
+ figsize_y=figsize_y,
337
+ )
338
+
339
+ def plot_all_features(
340
+ self,
341
+ ch_used: str | None = None,
342
+ time_limit_low_s: float | None = None,
343
+ time_limit_high_s: float | None = None,
344
+ normalize: bool = True,
345
+ save: bool = False,
346
+ title="all_feature_plt.pdf",
347
+ ytick_labelsize: int = 10,
348
+ clim_low: float | None = None,
349
+ clim_high: float | None = None,
350
+ ):
351
+ """_summary_
352
+
353
+ Parameters
354
+ ----------
355
+ ch_used : str, optional
356
+ time_limit_low_s : float, optional
357
+ time_limit_high_s : float, optional
358
+ normalize : bool, optional
359
+ save : bool, optional
360
+ title : str, optional
361
+ default "all_feature_plt.pdf"
362
+ ytick_labelsize : int, optional
363
+ by default 10
364
+ clim_low : float, optional
365
+ by default None
366
+ clim_high : float, optional
367
+ by default None
368
+ """
369
+
370
+ if ch_used is not None:
371
+ col_used = [
372
+ c
373
+ for c in self.feature_arr.columns
374
+ if c.startswith(ch_used) or c == "time" or "LABEL" in c or "MOV" in c
375
+ ]
376
+ df = self.feature_arr[col_used[::-1]]
377
+ else:
378
+ df = self.feature_arr[self.feature_arr.columns[::-1]]
379
+
380
+ plots.plot_all_features(
381
+ df=df,
382
+ time_limit_low_s=time_limit_low_s,
383
+ time_limit_high_s=time_limit_high_s,
384
+ normalize=normalize,
385
+ save=save,
386
+ title=title,
387
+ ytick_labelsize=ytick_labelsize,
388
+ feature_file=self.feature_file,
389
+ OUT_PATH=self.feature_dir,
390
+ clim_low=clim_low,
391
+ clim_high=clim_high,
392
+ )
393
+
394
+ @staticmethod
395
+ def get_performace_sub_strip(performance_sub: dict, plt_grid: bool = False):
396
+ ecog_strip_performance = []
397
+ ecog_coords_strip = []
398
+ cortex_grid = []
399
+ grid_performance = []
400
+
401
+ channels_ = performance_sub.keys()
402
+
403
+ for ch in channels_:
404
+ if "grid" not in ch and "combined" not in ch:
405
+ ecog_coords_strip.append(performance_sub[ch]["coord"])
406
+ ecog_strip_performance.append(performance_sub[ch]["performance_test"])
407
+ elif plt_grid and "gridcortex_" in ch:
408
+ cortex_grid.append(performance_sub[ch]["coord"])
409
+ grid_performance.append(performance_sub[ch]["performance_test"])
410
+
411
+ if len(ecog_coords_strip) > 0:
412
+ ecog_coords_strip = np.vstack(ecog_coords_strip)
413
+
414
+ return (
415
+ ecog_strip_performance,
416
+ ecog_coords_strip,
417
+ cortex_grid,
418
+ grid_performance,
419
+ )
420
+
421
+ def plot_across_subject_grd_ch_performance(
422
+ self,
423
+ performance_dict=None,
424
+ plt_grid=False,
425
+ feature_str_add="performance_allch_allgrid",
426
+ ):
427
+ ecog_strip_performance = []
428
+ ecog_coords_strip = []
429
+ grid_performance = []
430
+ for sub in performance_dict.keys():
431
+ (
432
+ ecog_strip_performance_sub,
433
+ ecog_coords_strip_sub,
434
+ _,
435
+ grid_performance_sub,
436
+ ) = self.get_performace_sub_strip(performance_dict[sub], plt_grid=plt_grid)
437
+ ecog_strip_performance.extend(ecog_strip_performance_sub)
438
+ ecog_coords_strip.extend(ecog_coords_strip_sub)
439
+ grid_performance.append(grid_performance_sub)
440
+ grid_performance = list(np.vstack(grid_performance).mean(axis=0))
441
+ coords_all = np.array(ecog_coords_strip)
442
+ coords_all[:, 0] = np.abs(coords_all[:, 0])
443
+
444
+ self.nmplotter.plot_cortex(
445
+ grid_cortex=np.array(self.sidecar["grid_cortex"])
446
+ if "grid_cortex" in self.sidecar
447
+ else None,
448
+ ecog_strip=coords_all if len(ecog_coords_strip) > 0 else None,
449
+ grid_color=grid_performance if len(grid_performance) > 0 else None,
450
+ strip_color=np.array(ecog_strip_performance)
451
+ if len(ecog_strip_performance) > 0
452
+ else None,
453
+ sess_right=self.sidecar["sess_right"],
454
+ save=True,
455
+ OUT_PATH=self.feature_dir,
456
+ feature_file=self.feature_file,
457
+ feature_str_add=feature_str_add,
458
+ show_plot=True,
459
+ )
460
+
461
+ def plot_subject_grid_ch_performance(
462
+ self,
463
+ subject_name=None,
464
+ performance_dict=None,
465
+ plt_grid=False,
466
+ feature_str_add="performance_allch_allgrid",
467
+ ):
468
+ """plot subject specific performance for individual channeal and optional grid points
469
+
470
+ Parameters
471
+ ----------
472
+ subject_name : string, optional
473
+ used subject, by default None
474
+ performance_dict : dict, optional
475
+ by default None
476
+ plt_grid : bool, optional
477
+ True to plot grid performances, by default False
478
+ feature_str_add : string, optional
479
+ figure output_name
480
+ """
481
+
482
+ ecog_strip_performance = []
483
+ ecog_coords_strip = []
484
+ cortex_grid = []
485
+ grid_performance = []
486
+
487
+ if subject_name is None:
488
+ subject_name = self.feature_file[
489
+ self.feature_file.find("sub-") : self.feature_file.find("_ses")
490
+ ][4:]
491
+
492
+ (
493
+ ecog_strip_performance,
494
+ ecog_coords_strip,
495
+ cortex_grid,
496
+ grid_performance,
497
+ ) = self.get_performace_sub_strip(
498
+ performance_dict[subject_name], plt_grid=plt_grid
499
+ )
500
+
501
+ self.nmplotter.plot_cortex(
502
+ grid_cortex=np.array(self.sidecar["grid_cortex"])
503
+ if "grid_cortex" in self.sidecar
504
+ else None,
505
+ ecog_strip=ecog_coords_strip if len(ecog_coords_strip) > 0 else None,
506
+ grid_color=grid_performance if len(grid_performance) > 0 else None,
507
+ strip_color=ecog_strip_performance
508
+ if len(ecog_strip_performance) > 0
509
+ else None,
510
+ sess_right=self.sidecar["sess_right"],
511
+ save=True,
512
+ OUT_PATH=self.feature_dir,
513
+ feature_file=self.feature_file,
514
+ feature_str_add=feature_str_add,
515
+ show_plot=True,
516
+ )
517
+
518
+ def plot_feature_series_time(
519
+ self,
520
+ ):
521
+ plots.plot_feature_series_time(self.feature_arr)
522
+
523
+ def plot_corr_matrix(
524
+ self,
525
+ ):
526
+ return plots.plot_corr_matrix(
527
+ self.feature_arr,
528
+ )
529
+
530
+ @staticmethod
531
+ def get_epochs(
532
+ data, y_, epoch_len, sfreq, threshold=0
533
+ ) -> tuple[np.ndarray, np.ndarray]:
534
+ """Return epoched data.
535
+
536
+ Parameters
537
+ ----------
538
+ data : np.ndarray
539
+ array of extracted features of shape (n_samples, n_channels, n_features)
540
+ y_ : np.ndarray
541
+ array of labels e.g. ones for movement and zeros for
542
+ no movement or baseline corr. rotameter data
543
+ epoch_len : int
544
+ length of epoch in seconds
545
+ sfreq : int/float
546
+ sampling frequency of data
547
+ threshold : int/float
548
+ (Optional) threshold to be used for identifying events
549
+ (default=0 for y_tr with only ones
550
+ and zeros)
551
+
552
+ Returns
553
+ -------
554
+ epoch_ : np.ndarray
555
+ array of epoched ieeg data with shape (epochs,samples,channels,features)
556
+ y_arr : np.ndarray
557
+ array of epoched event label data with shape (epochs,samples)
558
+ """
559
+
560
+ epoch_lim = int(epoch_len * sfreq)
561
+
562
+ ind_mov = np.where(np.diff(np.array(y_ > threshold) * 1) == 1)[0]
563
+
564
+ low_limit = ind_mov > epoch_lim / 2
565
+ up_limit = ind_mov < y_.shape[0] - epoch_lim / 2
566
+
567
+ ind_mov = ind_mov[low_limit & up_limit]
568
+
569
+ epoch_ = np.zeros([ind_mov.shape[0], epoch_lim, data.shape[1], data.shape[2]])
570
+
571
+ y_arr = np.zeros([ind_mov.shape[0], int(epoch_lim)])
572
+
573
+ for idx, i in enumerate(ind_mov):
574
+ epoch_[idx, :, :, :] = data[i - epoch_lim // 2 : i + epoch_lim // 2, :, :]
575
+
576
+ y_arr[idx, :] = y_[i - epoch_lim // 2 : i + epoch_lim // 2]
577
+
578
+ return epoch_, y_arr
579
+
580
+ def set_decoder(
581
+ self,
582
+ decoder: Decoder | None = None,
583
+ TRAIN_VAL_SPLIT=False,
584
+ RUN_BAY_OPT=False,
585
+ save_coef=False,
586
+ model=LogisticRegression,
587
+ eval_method=r2_score,
588
+ cv_method=KFold(n_splits=3, shuffle=False),
589
+ get_movement_detection_rate: bool = False,
590
+ mov_detection_threshold=0.5,
591
+ min_consequent_count=3,
592
+ threshold_score=True,
593
+ bay_opt_param_space: list = [],
594
+ STACK_FEATURES_N_SAMPLES=False,
595
+ time_stack_n_samples=5,
596
+ use_nested_cv=False,
597
+ VERBOSE=False,
598
+ undersampling=False,
599
+ oversampling=False,
600
+ mrmr_select=False,
601
+ pca=False,
602
+ cca=False,
603
+ ):
604
+ if decoder is not None:
605
+ self.decoder = decoder
606
+ else:
607
+ self.decoder = Decoder(
608
+ features=self.feature_arr,
609
+ label=self.label,
610
+ label_name=self.label_name,
611
+ used_chs=self.used_chs,
612
+ model=model,
613
+ eval_method=eval_method,
614
+ cv_method=cv_method,
615
+ threshold_score=threshold_score,
616
+ TRAIN_VAL_SPLIT=TRAIN_VAL_SPLIT,
617
+ RUN_BAY_OPT=RUN_BAY_OPT,
618
+ save_coef=save_coef,
619
+ get_movement_detection_rate=get_movement_detection_rate,
620
+ min_consequent_count=min_consequent_count,
621
+ mov_detection_threshold=mov_detection_threshold,
622
+ bay_opt_param_space=bay_opt_param_space,
623
+ STACK_FEATURES_N_SAMPLES=STACK_FEATURES_N_SAMPLES,
624
+ time_stack_n_samples=time_stack_n_samples,
625
+ VERBOSE=VERBOSE,
626
+ use_nested_cv=use_nested_cv,
627
+ undersampling=undersampling,
628
+ oversampling=oversampling,
629
+ mrmr_select=mrmr_select,
630
+ sfreq=self.sfreq,
631
+ pca=pca,
632
+ cca=cca,
633
+ )
634
+
635
+ def run_ML_model(
636
+ self,
637
+ feature_file: str | None = None,
638
+ estimate_gridpoints: bool = False,
639
+ estimate_channels: bool = True,
640
+ estimate_all_channels_combined: bool = False,
641
+ output_name: str = "LM",
642
+ save_results: bool = True,
643
+ ):
644
+ """machine learning model evaluation for ECoG strip channels and/or grid points
645
+
646
+ Parameters
647
+ ----------
648
+ feature_file : string, optional
649
+ estimate_gridpoints : bool, optional
650
+ run ML analysis for grid points, by default True
651
+ estimate_channels : bool, optional
652
+ run ML analysis for ECoG strip channel, by default True
653
+ estimate_all_channels_combined : bool, optional
654
+ run ML analysis features of all channels concatenated, by default False
655
+ model : sklearn model, optional
656
+ ML model, needs to obtain fit and predict functions,
657
+ by default linear_model.LogisticRegression(class_weight="balanced")
658
+ eval_method : sklearn.metrics, optional
659
+ evaluation performance metric, by default metrics.balanced_accuracy_score
660
+ cv_method : sklearn.model_selection, optional
661
+ valdation strategy, by default model_selection.KFold(n_splits=3, shuffle=False)
662
+ output_name : str, optional
663
+ saving name, by default "LM"
664
+ save_results : boolean
665
+ if true, save model._coef trained coefficients
666
+ """
667
+ if feature_file is None:
668
+ feature_file = self.feature_file
669
+
670
+ if estimate_gridpoints:
671
+ self.decoder.set_data_grid_points()
672
+ _ = self.decoder.run_CV_caller("grid_points")
673
+ if estimate_channels:
674
+ self.decoder.set_data_ind_channels()
675
+ _ = self.decoder.run_CV_caller("ind_channels")
676
+ if estimate_all_channels_combined:
677
+ _ = self.decoder.run_CV_caller("all_channels_combined")
678
+
679
+ if save_results:
680
+ self.decoder.save(
681
+ self.feature_dir,
682
+ self.feature_file
683
+ if ".vhdr" in self.feature_file
684
+ else self.feature_file,
685
+ output_name,
686
+ )
687
+
688
+ return self.read_results(
689
+ read_grid_points=estimate_gridpoints,
690
+ read_all_combined=estimate_all_channels_combined,
691
+ read_channels=estimate_channels,
692
+ ML_model_name=output_name,
693
+ read_mov_detection_rates=self.decoder.get_movement_detection_rate,
694
+ read_bay_opt_params=self.decoder.RUN_BAY_OPT,
695
+ read_mrmr=self.decoder.mrmr_select,
696
+ model_save=self.decoder.model_save,
697
+ )
698
+
699
+ def read_results(
700
+ self,
701
+ performance_dict: dict = {},
702
+ subject_name: str | None = None,
703
+ DEFAULT_PERFORMANCE: float = 0.5,
704
+ read_grid_points: bool = True,
705
+ read_channels: bool = True,
706
+ read_all_combined: bool = False,
707
+ ML_model_name: str = "LM",
708
+ read_mov_detection_rates: bool = False,
709
+ read_bay_opt_params: bool = False,
710
+ read_mrmr: bool = False,
711
+ model_save: bool = False,
712
+ save_results: bool = False,
713
+ PATH_OUT: str = "", # Removed None default, save_general_dict does not handle None anyway
714
+ folder_name: str = "",
715
+ str_add: str = "",
716
+ ):
717
+ """Save performances of a given patient into performance_dict from saved nm_decoder
718
+
719
+ Parameters
720
+ ----------
721
+ performance_dict : dictionary
722
+ dictionary including decoding performances, by default dictionary
723
+ subject_name : string, optional
724
+ subject name, by default None
725
+ DEFAULT_PERFORMANCE : float, optional
726
+ chance performance, by default 0.5
727
+ read_grid_points : bool, optional
728
+ true if grid point performances are read, by default True
729
+ read_channels : bool, optional
730
+ true if channels performances are read, by default True
731
+ read_all_combined : bool, optional
732
+ true if all combined channel performances are read, by default False
733
+ ML_model_name : str, optional
734
+ machine learning model name, by default 'LM'
735
+ read_mov_detection_rates : boolean, by defaulte False
736
+ if True, read movement detection rates, as well as fpr's and tpr's
737
+ read_bay_opt_params : boolean, by default False
738
+ read_mrmr : boolean, by default False
739
+ model_save : boolean, by default False
740
+ save_results : boolean, by default False
741
+ PATH_OUT : string, by default None
742
+ folder_name : string, by default None
743
+ str_add : string, by default None
744
+
745
+ Returns
746
+ -------
747
+ performance_dict : dictionary
748
+
749
+ """
750
+
751
+ if ".vhdr" in self.feature_file:
752
+ feature_file = self.feature_file[: -len(".vhdr")]
753
+ else:
754
+ feature_file = self.feature_file
755
+
756
+ if subject_name is None:
757
+ subject_name = feature_file[
758
+ feature_file.find("sub-") : feature_file.find("_ses")
759
+ ][4:]
760
+
761
+ PATH_ML_ = PurePath(
762
+ self.feature_dir,
763
+ feature_file,
764
+ feature_file + "_" + ML_model_name + "_ML_RES.p",
765
+ )
766
+
767
+ # read ML results
768
+ with open(PATH_ML_, "rb") as input:
769
+ ML_res = pickle.load(input)
770
+ if self.decoder is None:
771
+ self.decoder = ML_res
772
+
773
+ performance_dict[subject_name] = {}
774
+
775
+ def write_CV_res_in_performance_dict(
776
+ obj_read,
777
+ obj_write,
778
+ read_mov_detection_rates=read_mov_detection_rates,
779
+ read_bay_opt_params=False,
780
+ ):
781
+ def transform_list_of_dicts_into_dict_of_lists(l_):
782
+ dict_out = {}
783
+ for key_, _ in l_[0].items():
784
+ key_l = []
785
+ for dict_ in l_:
786
+ key_l.append(dict_[key_])
787
+ dict_out[key_] = key_l
788
+ return dict_out
789
+
790
+ def read_ML_performances(
791
+ obj_read, obj_write, set_inner_CV_res: bool = False
792
+ ):
793
+ def set_score(
794
+ key_set: str = "",
795
+ key_get: str = "",
796
+ take_mean: bool = True,
797
+ val=None,
798
+ ):
799
+ if set_inner_CV_res:
800
+ key_set = "InnerCV_" + key_set
801
+ key_get = "InnerCV_" + key_get
802
+ if take_mean:
803
+ val = np.mean(obj_read[key_get])
804
+ obj_write[key_set] = val
805
+
806
+ set_score(
807
+ key_set="performance_test",
808
+ key_get="score_test",
809
+ take_mean=True,
810
+ )
811
+ set_score(
812
+ key_set="performance_train",
813
+ key_get="score_train",
814
+ take_mean=True,
815
+ )
816
+
817
+ if "coef" in obj_read:
818
+ set_score(
819
+ key_set="coef",
820
+ key_get="coef",
821
+ take_mean=False,
822
+ val=np.concatenate(obj_read["coef"]),
823
+ )
824
+
825
+ if read_mov_detection_rates:
826
+ set_score(
827
+ key_set="mov_detection_rates_test",
828
+ key_get="mov_detection_rates_test",
829
+ take_mean=True,
830
+ )
831
+ set_score(
832
+ key_set="mov_detection_rates_train",
833
+ key_get="mov_detection_rates_train",
834
+ take_mean=True,
835
+ )
836
+ set_score(
837
+ key_set="fprate_test",
838
+ key_get="fprate_test",
839
+ take_mean=True,
840
+ )
841
+ set_score(
842
+ key_set="fprate_train",
843
+ key_get="fprate_train",
844
+ take_mean=True,
845
+ )
846
+ set_score(
847
+ key_set="tprate_test",
848
+ key_get="tprate_test",
849
+ take_mean=True,
850
+ )
851
+ set_score(
852
+ key_set="tprate_train",
853
+ key_get="tprate_train",
854
+ take_mean=True,
855
+ )
856
+
857
+ if read_bay_opt_params:
858
+ # transform dict into keys for json saving
859
+ dict_to_save = transform_list_of_dicts_into_dict_of_lists(
860
+ obj_read["best_bay_opt_params"]
861
+ )
862
+ set_score(
863
+ key_set="bay_opt_best_params",
864
+ take_mean=False,
865
+ val=dict_to_save,
866
+ )
867
+
868
+ if read_mrmr:
869
+ # transform dict into keys for json saving
870
+
871
+ set_score(
872
+ key_set="mrmr_select",
873
+ take_mean=False,
874
+ val=obj_read["mrmr_select"],
875
+ )
876
+ if model_save:
877
+ set_score(
878
+ key_set="model_save",
879
+ take_mean=False,
880
+ val=obj_read["model_save"],
881
+ )
882
+
883
+ read_ML_performances(obj_read, obj_write)
884
+
885
+ if len([key_ for key_ in obj_read.keys() if "InnerCV_" in key_]) > 0:
886
+ read_ML_performances(obj_read, obj_write, set_inner_CV_res=True)
887
+
888
+ if read_channels:
889
+ ch_to_use = self.ch_names_ECOG
890
+ ch_to_use = self.decoder.used_chs
891
+ for ch in ch_to_use:
892
+ performance_dict[subject_name][ch] = {}
893
+
894
+ if "coords" in self.sidecar:
895
+ if len(self.sidecar["coords"]) > 0: # check if coords are empty
896
+ coords_exist = False
897
+ for cortex_loc in self.sidecar["coords"].keys():
898
+ for ch_name_coord_idx, ch_name_coord in enumerate(
899
+ self.sidecar["coords"][cortex_loc]["ch_names"]
900
+ ):
901
+ if ch.startswith(ch_name_coord):
902
+ coords = self.sidecar["coords"][cortex_loc][
903
+ "positions"
904
+ ][ch_name_coord_idx]
905
+ coords_exist = (
906
+ True # optimally break out of the two loops...
907
+ )
908
+ if not coords_exist:
909
+ coords = None
910
+ performance_dict[subject_name][ch]["coord"] = coords
911
+ write_CV_res_in_performance_dict(
912
+ ML_res.ch_ind_results[ch],
913
+ performance_dict[subject_name][ch],
914
+ read_mov_detection_rates=read_mov_detection_rates,
915
+ read_bay_opt_params=read_bay_opt_params,
916
+ )
917
+
918
+ if read_all_combined:
919
+ performance_dict[subject_name]["all_ch_combined"] = {}
920
+ write_CV_res_in_performance_dict(
921
+ ML_res.all_ch_results,
922
+ performance_dict[subject_name]["all_ch_combined"],
923
+ read_mov_detection_rates=read_mov_detection_rates,
924
+ read_bay_opt_params=read_bay_opt_params,
925
+ )
926
+
927
+ if read_grid_points:
928
+ performance_dict[subject_name]["active_gridpoints"] = (
929
+ ML_res.active_gridpoints
930
+ )
931
+
932
+ for project_settings, grid_type in zip(
933
+ ["project_cortex", "project_subcortex"],
934
+ ["gridcortex_", "gridsubcortex_"],
935
+ ):
936
+ if not self.settings.postprocessing[project_settings]:
937
+ continue
938
+
939
+ # the sidecar keys are grid_cortex and subcortex_grid
940
+ for grid_point in range(
941
+ len(self.sidecar["grid_" + project_settings.split("_")[1]])
942
+ ):
943
+ gp_str = grid_type + str(grid_point)
944
+
945
+ performance_dict[subject_name][gp_str] = {}
946
+ performance_dict[subject_name][gp_str]["coord"] = self.sidecar[
947
+ "grid_" + project_settings.split("_")[1]
948
+ ][grid_point]
949
+
950
+ if gp_str in ML_res.active_gridpoints:
951
+ write_CV_res_in_performance_dict(
952
+ ML_res.gridpoint_ind_results[gp_str],
953
+ performance_dict[subject_name][gp_str],
954
+ read_mov_detection_rates=read_mov_detection_rates,
955
+ read_bay_opt_params=read_bay_opt_params,
956
+ )
957
+ else:
958
+ # set non interpolated grid point to default performance
959
+ performance_dict[subject_name][gp_str]["performance_test"] = (
960
+ DEFAULT_PERFORMANCE
961
+ )
962
+ performance_dict[subject_name][gp_str]["performance_train"] = (
963
+ DEFAULT_PERFORMANCE
964
+ )
965
+
966
+ if save_results:
967
+ io.save_general_dict(
968
+ dict_=performance_dict,
969
+ path_out=PATH_OUT,
970
+ prefix=folder_name,
971
+ str_add=str_add,
972
+ )
973
+ return performance_dict
974
+
975
+ @staticmethod
976
+ def get_dataframe_performances(p: dict) -> "pd.DataFrame":
977
+ performances = []
978
+ for sub in p.keys():
979
+ for ch in p[sub].keys():
980
+ if "active_gridpoints" in ch:
981
+ continue
982
+ dict_add = p[sub][ch].copy()
983
+ dict_add["sub"] = sub
984
+ dict_add["ch"] = ch
985
+
986
+ if "all_ch_" in ch:
987
+ dict_add["ch_type"] = "all ch combinded"
988
+ elif "gridcortex" in ch:
989
+ dict_add["ch_type"] = "cortex grid"
990
+ else:
991
+ dict_add["ch_type"] = "electrode ch"
992
+ performances.append(dict_add)
993
+
994
+ return pd.DataFrame(performances)