py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.5__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 (80) 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/FieldTrip.py +589 -589
  5. py_neuromodulation/__init__.py +74 -13
  6. py_neuromodulation/_write_example_dataset_helper.py +83 -65
  7. py_neuromodulation/data/README +6 -6
  8. py_neuromodulation/data/dataset_description.json +8 -8
  9. py_neuromodulation/data/participants.json +32 -32
  10. py_neuromodulation/data/participants.tsv +2 -2
  11. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
  12. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
  13. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
  14. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
  15. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
  16. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
  17. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
  18. py_neuromodulation/grid_cortex.tsv +40 -40
  19. py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
  20. py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
  21. py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
  22. py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
  23. py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
  24. py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
  25. py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
  26. py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
  27. py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
  28. py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
  29. py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
  30. py_neuromodulation/nm_IO.py +413 -417
  31. py_neuromodulation/nm_RMAP.py +496 -531
  32. py_neuromodulation/nm_analysis.py +993 -1074
  33. py_neuromodulation/nm_artifacts.py +30 -25
  34. py_neuromodulation/nm_bispectra.py +154 -168
  35. py_neuromodulation/nm_bursts.py +292 -198
  36. py_neuromodulation/nm_coherence.py +251 -205
  37. py_neuromodulation/nm_database.py +149 -0
  38. py_neuromodulation/nm_decode.py +918 -992
  39. py_neuromodulation/nm_define_nmchannels.py +300 -302
  40. py_neuromodulation/nm_features.py +144 -116
  41. py_neuromodulation/nm_filter.py +219 -219
  42. py_neuromodulation/nm_filter_preprocessing.py +79 -91
  43. py_neuromodulation/nm_fooof.py +139 -159
  44. py_neuromodulation/nm_generator.py +45 -37
  45. py_neuromodulation/nm_hjorth_raw.py +52 -73
  46. py_neuromodulation/nm_kalmanfilter.py +71 -58
  47. py_neuromodulation/nm_linelength.py +21 -33
  48. py_neuromodulation/nm_logger.py +66 -0
  49. py_neuromodulation/nm_mne_connectivity.py +149 -112
  50. py_neuromodulation/nm_mnelsl_generator.py +90 -0
  51. py_neuromodulation/nm_mnelsl_stream.py +116 -0
  52. py_neuromodulation/nm_nolds.py +96 -93
  53. py_neuromodulation/nm_normalization.py +173 -214
  54. py_neuromodulation/nm_oscillatory.py +423 -448
  55. py_neuromodulation/nm_plots.py +585 -612
  56. py_neuromodulation/nm_preprocessing.py +83 -0
  57. py_neuromodulation/nm_projection.py +370 -394
  58. py_neuromodulation/nm_rereference.py +97 -95
  59. py_neuromodulation/nm_resample.py +59 -50
  60. py_neuromodulation/nm_run_analysis.py +325 -435
  61. py_neuromodulation/nm_settings.py +289 -68
  62. py_neuromodulation/nm_settings.yaml +244 -0
  63. py_neuromodulation/nm_sharpwaves.py +423 -401
  64. py_neuromodulation/nm_stats.py +464 -480
  65. py_neuromodulation/nm_stream.py +398 -0
  66. py_neuromodulation/nm_stream_abc.py +166 -218
  67. py_neuromodulation/nm_types.py +193 -0
  68. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/METADATA +29 -26
  69. py_neuromodulation-0.0.5.dist-info/RECORD +83 -0
  70. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/WHEEL +1 -1
  71. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.5.dist-info}/licenses/LICENSE +21 -21
  72. py_neuromodulation/nm_EpochStream.py +0 -92
  73. py_neuromodulation/nm_across_patient_decoding.py +0 -927
  74. py_neuromodulation/nm_cohortwrapper.py +0 -435
  75. py_neuromodulation/nm_eval_timing.py +0 -239
  76. py_neuromodulation/nm_features_abc.py +0 -39
  77. py_neuromodulation/nm_settings.json +0 -338
  78. py_neuromodulation/nm_stream_offline.py +0 -359
  79. py_neuromodulation/utils/_logging.py +0 -24
  80. py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
@@ -1,435 +1,325 @@
1
- """This module contains the class to process a given batch of data."""
2
-
3
- from enum import Enum
4
- import math
5
- import os
6
- from time import time
7
- from typing import Protocol, Type
8
- import logging
9
-
10
- logger = logging.getLogger("PynmLogger")
11
-
12
- import numpy as np
13
- import pandas as pd
14
-
15
- from py_neuromodulation import (
16
- nm_features,
17
- nm_filter,
18
- nm_IO,
19
- nm_normalization,
20
- nm_projection,
21
- nm_rereference,
22
- nm_resample,
23
- nm_filter_preprocessing,
24
- )
25
-
26
- _PathLike = str | os.PathLike
27
-
28
-
29
- class Preprocessor(Protocol):
30
- def process(self, data: np.ndarray) -> np.ndarray:
31
- pass
32
-
33
- def test_settings(self, settings: dict): ...
34
-
35
-
36
- _PREPROCESSING_CONSTRUCTORS = [
37
- "notch_filter",
38
- "re_referencing",
39
- "raw_normalization",
40
- "raw_resample",
41
- ]
42
-
43
-
44
- class GRIDS(Enum):
45
- """Definition of possible projection grid types"""
46
-
47
- CORTEX = "cortex"
48
- SUBCORTEX = "subcortex"
49
-
50
-
51
- class DataProcessor:
52
- def __init__(
53
- self,
54
- sfreq: int | float,
55
- settings: dict | _PathLike,
56
- nm_channels: pd.DataFrame | _PathLike,
57
- coord_names: list | None = None,
58
- coord_list: list | None = None,
59
- line_noise: int | float | None = None,
60
- path_grids: _PathLike | None = None,
61
- verbose: bool = True,
62
- ) -> None:
63
- """Initialize run class.
64
-
65
- Parameters
66
- ----------
67
- features : features.py object
68
- Feature_df object (needs to be initialized beforehand)
69
- settings : dict
70
- dictionary of settings such as "seglengths" or "frequencyranges"
71
- reference : reference.py object
72
- Rereference object (needs to be initialized beforehand), by default None
73
- projection : projection.py object
74
- projection object (needs to be initialized beforehand), by default None
75
- resample : resample.py object
76
- Resample object (needs to be initialized beforehand), by default None
77
- notch_filter : nm_filter.NotchFilter,
78
- Notch Filter object, needs to be instantiated beforehand
79
- verbose : boolean
80
- if True, log signal processed and computation time
81
- """
82
- self.settings = self._load_settings(settings)
83
- self.nm_channels = self._load_nm_channels(nm_channels)
84
-
85
- self.sfreq_features = self.settings["sampling_rate_features_hz"]
86
- self._sfreq_raw_orig = sfreq
87
- self.sfreq_raw = math.floor(sfreq)
88
- self.line_noise = line_noise
89
- self.path_grids = path_grids
90
- self.verbose = verbose
91
-
92
- self.features_previous = None
93
-
94
- (self.ch_names_used, _, self.feature_idx, _) = self._get_ch_info()
95
-
96
- self.preprocessors: list[Preprocessor] = []
97
- for preprocessing_method in self.settings["preprocessing"]:
98
- settings_str = f"{preprocessing_method}_settings"
99
- match preprocessing_method:
100
- case "raw_resampling":
101
- preprocessor = nm_resample.Resampler(
102
- sfreq=self.sfreq_raw, **self.settings[settings_str]
103
- )
104
- self.sfreq_raw = preprocessor.sfreq_new
105
- self.preprocessors.append(preprocessor)
106
- case "notch_filter":
107
- preprocessor = nm_filter.NotchFilter(
108
- sfreq=self.sfreq_raw,
109
- line_noise=self.line_noise,
110
- **self.settings.get(settings_str, {}),
111
- )
112
- self.preprocessors.append(preprocessor)
113
- case "re_referencing":
114
- preprocessor = nm_rereference.ReReferencer(
115
- sfreq=self.sfreq_raw,
116
- nm_channels=self.nm_channels,
117
- )
118
- self.preprocessors.append(preprocessor)
119
- case "raw_normalization":
120
- preprocessor = nm_normalization.RawNormalizer(
121
- sfreq=self.sfreq_raw,
122
- sampling_rate_features_hz=self.sfreq_features,
123
- **self.settings.get(settings_str, {}),
124
- )
125
- self.preprocessors.append(preprocessor)
126
- case "preprocessing_filter":
127
- preprocessor = nm_filter_preprocessing.PreprocessingFilter(
128
- settings=self.settings,
129
- sfreq=self.sfreq_raw,
130
- )
131
- self.preprocessors.append(preprocessor)
132
- case _:
133
- raise ValueError(
134
- "Invalid preprocessing method. Must be one of"
135
- f" {_PREPROCESSING_CONSTRUCTORS}. Got"
136
- f" {preprocessing_method}"
137
- )
138
-
139
- if self.settings["postprocessing"]["feature_normalization"]:
140
- settings_str = "feature_normalization_settings"
141
- self.feature_normalizer = nm_normalization.FeatureNormalizer(
142
- sampling_rate_features_hz=self.sfreq_features,
143
- **self.settings.get(settings_str, {}),
144
- )
145
-
146
- self.features = nm_features.Features(
147
- s=self.settings,
148
- ch_names=self.ch_names_used,
149
- sfreq=self.sfreq_raw,
150
- )
151
-
152
- if coord_list is not None and coord_names is not None:
153
- self.coords = self._set_coords(
154
- coord_names=coord_names, coord_list=coord_list
155
- )
156
-
157
- self.projection = self._get_projection(self.settings, self.nm_channels)
158
-
159
- self.cnt_samples = 0
160
-
161
- @staticmethod
162
- def _add_coordinates(coord_names: list[str], coord_list: list) -> dict:
163
- """Write cortical and subcortical coordinate information in joint dictionary
164
-
165
- Parameters
166
- ----------
167
- coord_names : list[str]
168
- list of coordinate names
169
- coord_list : list
170
- list of list of 3D coordinates
171
-
172
- Returns
173
- -------
174
- dict with (sub)cortex_left and (sub)cortex_right ch_names and positions
175
- """
176
-
177
- def is_left_coord(val: int | float, coord_region: str) -> bool:
178
- if coord_region.split("_")[1] == "left":
179
- return val < 0
180
- return val > 0
181
-
182
- coords = {}
183
-
184
- for coord_region in [
185
- coord_loc + "_" + lat
186
- for coord_loc in ["cortex", "subcortex"]
187
- for lat in ["left", "right"]
188
- ]:
189
- coords[coord_region] = {}
190
-
191
- ch_type = (
192
- "ECOG" if "cortex" == coord_region.split("_")[0] else "LFP"
193
- )
194
-
195
- coords[coord_region]["ch_names"] = [
196
- coord_name
197
- for coord_name, ch in zip(coord_names, coord_list)
198
- if is_left_coord(ch[0], coord_region)
199
- and (ch_type in coord_name)
200
- ]
201
-
202
- # multiply by 1000 to get m instead of mm
203
- positions = []
204
- for coord, coord_name in zip(coord_list, coord_names):
205
- if is_left_coord(coord[0], coord_region) and (
206
- ch_type in coord_name
207
- ):
208
- positions.append(coord)
209
- positions = np.array(positions, dtype=np.float64) * 1000
210
- coords[coord_region]["positions"] = positions
211
-
212
- return coords
213
-
214
- def _get_ch_info(
215
- self,
216
- ) -> tuple[list[str], list[str], list[int], np.ndarray]:
217
- """Get used feature and label info from nm_channels"""
218
- nm_channels = self.nm_channels
219
- ch_names_used = nm_channels[nm_channels["used"] == 1][
220
- "new_name"
221
- ].tolist()
222
- ch_types_used = nm_channels[nm_channels["used"] == 1]["type"].tolist()
223
-
224
- # used channels for feature estimation
225
- feature_idx = np.where(nm_channels["used"] & ~nm_channels["target"])[
226
- 0
227
- ].tolist()
228
-
229
- # If multiple targets exist, select only the first
230
- label_idx = np.where(nm_channels["target"] == 1)[0]
231
-
232
- return ch_names_used, ch_types_used, feature_idx, label_idx
233
-
234
- @staticmethod
235
- def _get_grids(
236
- settings: dict,
237
- path_grids: str | _PathLike | None,
238
- grid_type: Type[GRIDS],
239
- ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
240
- """Read settings specified grids
241
-
242
- Parameters
243
- ----------
244
- settings : dict
245
- path_grids : str
246
- grid_type : GRIDS
247
-
248
- Returns
249
- -------
250
- Tuple
251
- grid_cortex, grid_subcortex,
252
- might be None if not specified in settings
253
- """
254
- if settings["postprocessing"]["project_cortex"] is True:
255
- grid_cortex = nm_IO.read_grid(path_grids, grid_type.CORTEX.name)
256
- else:
257
- grid_cortex = None
258
- if settings["postprocessing"]["project_subcortex"] is True:
259
- grid_subcortex = nm_IO.read_grid(
260
- path_grids, grid_type.SUBCORTEX.name
261
- )
262
- else:
263
- grid_subcortex = None
264
- return grid_cortex, grid_subcortex
265
-
266
- def _get_projection(
267
- self, settings: dict, nm_channels: pd.DataFrame
268
- ) -> nm_projection.Projection | None:
269
- """Return projection of used coordinated and grids"""
270
-
271
- if not any(
272
- (
273
- settings["postprocessing"]["project_cortex"],
274
- settings["postprocessing"]["project_subcortex"],
275
- )
276
- ):
277
- return None
278
-
279
- grid_cortex, grid_subcortex = self._get_grids(
280
- self.settings, self.path_grids, GRIDS
281
- )
282
- projection = nm_projection.Projection(
283
- settings=settings,
284
- grid_cortex=grid_cortex,
285
- grid_subcortex=grid_subcortex,
286
- coords=self.coords,
287
- nm_channels=nm_channels,
288
- plot_projection=False,
289
- )
290
- return projection
291
-
292
- @staticmethod
293
- def _load_nm_channels(
294
- nm_channels: pd.DataFrame | _PathLike,
295
- ) -> pd.DataFrame:
296
- if not isinstance(nm_channels, pd.DataFrame):
297
- return nm_IO.load_nm_channels(nm_channels)
298
- return nm_channels
299
-
300
- @staticmethod
301
- def _load_settings(settings: dict | _PathLike) -> dict:
302
- if not isinstance(settings, dict):
303
- return nm_IO.read_settings(str(settings))
304
- return settings
305
-
306
- def _set_coords(
307
- self, coord_names: list[str] | None, coord_list: list | None
308
- ) -> dict:
309
- if not any(
310
- (
311
- self.settings["postprocessing"]["project_cortex"],
312
- self.settings["postprocessing"]["project_subcortex"],
313
- )
314
- ):
315
- return {}
316
-
317
- if any((coord_list is None, coord_names is None)):
318
- raise ValueError(
319
- "No coordinates could be loaded. Please provide coord_list and"
320
- f" coord_names. Got: {coord_list=}, {coord_names=}."
321
- )
322
-
323
- return self._add_coordinates(
324
- coord_names=coord_names, coord_list=coord_list
325
- )
326
-
327
- def process(self, data: np.ndarray) -> pd.Series:
328
- """Given a new data batch, calculate and return features.
329
-
330
- Parameters
331
- ----------
332
- data : np.ndarray
333
- Current batch of raw data
334
-
335
- Returns
336
- -------
337
- pandas Series
338
- Features calculated from current data
339
- """
340
- start_time = time()
341
-
342
- nan_channels = np.isnan(data).any(axis=1)
343
-
344
- data = np.nan_to_num(data)[self.feature_idx, :]
345
-
346
- for processor in self.preprocessors:
347
- data = processor.process(data)
348
-
349
- # calculate features
350
- features_dict = self.features.estimate_features(data)
351
-
352
- # normalize features
353
- if self.settings["postprocessing"]["feature_normalization"]:
354
- normed_features = self.feature_normalizer.process(
355
- np.fromiter(features_dict.values(), dtype="float")
356
- )
357
- features_dict = {
358
- key: normed_features[idx]
359
- for idx, key in enumerate(features_dict.keys())
360
- }
361
-
362
- features_current = pd.Series(
363
- data=list(features_dict.values()),
364
- index=list(features_dict.keys()),
365
- dtype=np.float64,
366
- )
367
-
368
- # project features to grid
369
- if self.projection:
370
- features_current = self.projection.project_features(
371
- features_current
372
- )
373
-
374
- # check for all features, where the channel had a NaN, that the feature is also put to NaN
375
- if nan_channels.sum() > 0:
376
- for ch in list(np.array(self.ch_names_used)[nan_channels]):
377
- features_current.loc[
378
- features_current.index.str.contains(ch)
379
- ] = np.nan
380
-
381
- if self.verbose is True:
382
- logger.info(
383
- "Last batch took: "
384
- + str(np.round(time() - start_time, 2))
385
- + " seconds"
386
- )
387
-
388
- return features_current
389
-
390
- def save_sidecar(
391
- self,
392
- out_path_root: _PathLike,
393
- folder_name: str,
394
- additional_args: dict | None = None,
395
- ) -> None:
396
- """Save sidecar incuding fs, coords, sess_right to
397
- out_path_root and subfolder 'folder_name'.
398
- """
399
- sidecar = {
400
- "original_fs": self._sfreq_raw_orig,
401
- "final_fs": self.sfreq_raw,
402
- "sfreq": self.sfreq_features,
403
- }
404
- if self.projection:
405
- sidecar["coords"] = self.projection.coords
406
- if self.settings["postprocessing"]["project_cortex"]:
407
- sidecar["grid_cortex"] = self.projection.grid_cortex
408
- sidecar["proj_matrix_cortex"] = (
409
- self.projection.proj_matrix_cortex
410
- )
411
- if self.settings["postprocessing"]["project_subcortex"]:
412
- sidecar["grid_subcortex"] = self.projection.grid_subcortex
413
- sidecar["proj_matrix_subcortex"] = (
414
- self.projection.proj_matrix_subcortex
415
- )
416
- if additional_args is not None:
417
- sidecar = sidecar | additional_args
418
-
419
- nm_IO.save_sidecar(sidecar, out_path_root, folder_name)
420
-
421
- def save_settings(self, out_path_root: _PathLike, folder_name: str) -> None:
422
- nm_IO.save_settings(self.settings, out_path_root, folder_name)
423
-
424
- def save_nm_channels(
425
- self, out_path_root: _PathLike, folder_name: str
426
- ) -> None:
427
- nm_IO.save_nm_channels(self.nm_channels, out_path_root, folder_name)
428
-
429
- def save_features(
430
- self,
431
- out_path_root: _PathLike,
432
- folder_name: str,
433
- feature_arr: pd.DataFrame,
434
- ) -> None:
435
- nm_IO.save_features(feature_arr, out_path_root, folder_name)
1
+ """This module contains the class to process a given batch of data."""
2
+
3
+ from time import time
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ from py_neuromodulation import nm_IO, logger
8
+ from py_neuromodulation.nm_types import _PathLike
9
+ from py_neuromodulation.nm_features import FeatureProcessors
10
+ from py_neuromodulation.nm_preprocessing import NMPreprocessors
11
+ from py_neuromodulation.nm_projection import Projection
12
+ from py_neuromodulation.nm_settings import NMSettings
13
+
14
+
15
+ class DataProcessor:
16
+ def __init__(
17
+ self,
18
+ sfreq: float,
19
+ settings: NMSettings | _PathLike,
20
+ nm_channels: pd.DataFrame | _PathLike,
21
+ coord_names: list | None = None,
22
+ coord_list: list | None = None,
23
+ line_noise: float | None = None,
24
+ path_grids: _PathLike | None = None,
25
+ verbose: bool = True,
26
+ ) -> None:
27
+ """Initialize run class.
28
+
29
+ Parameters
30
+ ----------
31
+ settings : nm_settings.NMSettings object
32
+ nm_channels : pd.DataFrame | _PathLike
33
+ Initialized pd.DataFrame with channel specific information.
34
+ The path to a nm_channels.csv can be also passed.
35
+ coord_names : list | None
36
+ list of coordinate names
37
+ coord_list : list | None
38
+ list of list of 3D coordinates
39
+ path_grids : _PathLike | None
40
+ List to grid_cortex.tsv and grid_subcortex.tsv for grid point projection
41
+ verbose : boolean
42
+ if True, log signal processed and computation time
43
+ """
44
+ self.settings = NMSettings.load(settings)
45
+ self.nm_channels = self._load_nm_channels(nm_channels)
46
+
47
+ self.sfreq_features: float = self.settings.sampling_rate_features_hz
48
+ self._sfreq_raw_orig: float = sfreq
49
+ self.sfreq_raw: float = sfreq // 1
50
+ self.line_noise: float | None = line_noise
51
+ self.path_grids: _PathLike | None = path_grids
52
+ self.verbose: bool = verbose
53
+
54
+ self.features_previous = None
55
+
56
+ (self.ch_names_used, _, self.feature_idx, _) = self._get_ch_info()
57
+
58
+ self.preprocessors = NMPreprocessors(
59
+ settings=self.settings,
60
+ nm_channels=self.nm_channels,
61
+ sfreq=self.sfreq_raw,
62
+ line_noise=self.line_noise,
63
+ )
64
+
65
+ if self.settings.postprocessing.feature_normalization:
66
+ from py_neuromodulation.nm_normalization import FeatureNormalizer
67
+
68
+ self.feature_normalizer = FeatureNormalizer(self.settings)
69
+
70
+ self.features = FeatureProcessors(
71
+ settings=self.settings,
72
+ ch_names=self.ch_names_used,
73
+ sfreq=self.sfreq_raw,
74
+ )
75
+
76
+ if coord_list is not None and coord_names is not None:
77
+ self.coords = self._set_coords(
78
+ coord_names=coord_names, coord_list=coord_list
79
+ )
80
+
81
+ self.projection = self._get_projection(self.settings, self.nm_channels)
82
+
83
+ self.cnt_samples = 0
84
+
85
+ @staticmethod
86
+ def _add_coordinates(coord_names: list[str], coord_list: list) -> dict:
87
+ """Write cortical and subcortical coordinate information in joint dictionary
88
+
89
+ Parameters
90
+ ----------
91
+ coord_names : list[str]
92
+ list of coordinate names
93
+ coord_list : list
94
+ list of list of 3D coordinates
95
+
96
+ Returns
97
+ -------
98
+ dict with (sub)cortex_left and (sub)cortex_right ch_names and positions
99
+ """
100
+
101
+ def is_left_coord(val: float, coord_region: str) -> bool:
102
+ if coord_region.split("_")[1] == "left":
103
+ return val < 0
104
+ return val > 0
105
+
106
+ coords: dict[str, dict[str, list | np.ndarray]] = {}
107
+
108
+ for coord_region in [
109
+ coord_loc + "_" + lat
110
+ for coord_loc in ["cortex", "subcortex"]
111
+ for lat in ["left", "right"]
112
+ ]:
113
+ coords[coord_region] = {}
114
+
115
+ ch_type = "ECOG" if "cortex" == coord_region.split("_")[0] else "LFP"
116
+
117
+ coords[coord_region]["ch_names"] = [
118
+ coord_name
119
+ for coord_name, ch in zip(coord_names, coord_list)
120
+ if is_left_coord(ch[0], coord_region) and (ch_type in coord_name)
121
+ ]
122
+
123
+ # multiply by 1000 to get m instead of mm
124
+ positions = []
125
+ for coord, coord_name in zip(coord_list, coord_names):
126
+ if is_left_coord(coord[0], coord_region) and (ch_type in coord_name):
127
+ positions.append(coord)
128
+ coords[coord_region]["positions"] = (
129
+ np.array(positions, dtype=np.float64) * 1000
130
+ )
131
+
132
+ return coords
133
+
134
+ def _get_ch_info(
135
+ self,
136
+ ) -> tuple[list[str], list[str], list[int], np.ndarray]:
137
+ """Get used feature and label info from nm_channels"""
138
+ nm_channels = self.nm_channels
139
+ ch_names_used = nm_channels[nm_channels["used"] == 1]["new_name"].tolist()
140
+ ch_types_used = nm_channels[nm_channels["used"] == 1]["type"].tolist()
141
+
142
+ # used channels for feature estimation
143
+ feature_idx = np.where(nm_channels["used"] & ~nm_channels["target"])[0].tolist()
144
+
145
+ # If multiple targets exist, select only the first
146
+ label_idx = np.where(nm_channels["target"] == 1)[0]
147
+
148
+ return ch_names_used, ch_types_used, feature_idx, label_idx
149
+
150
+ @staticmethod
151
+ def _get_grids(
152
+ settings: "NMSettings",
153
+ path_grids: _PathLike | None,
154
+ ) -> tuple[pd.DataFrame | None, pd.DataFrame | None]:
155
+ """Read settings specified grids
156
+
157
+ Parameters
158
+ ----------
159
+ settings : nm_settings.NMSettings object
160
+ path_grids : _PathLike | str
161
+
162
+ Returns
163
+ -------
164
+ Tuple
165
+ grid_cortex, grid_subcortex,
166
+ might be None if not specified in settings
167
+ """
168
+ if settings.postprocessing.project_cortex:
169
+ grid_cortex = nm_IO.read_grid(path_grids, "cortex")
170
+ else:
171
+ grid_cortex = None
172
+ if settings.postprocessing.project_subcortex:
173
+ grid_subcortex = nm_IO.read_grid(path_grids, "subcortex")
174
+ else:
175
+ grid_subcortex = None
176
+ return grid_cortex, grid_subcortex
177
+
178
+ def _get_projection(
179
+ self, settings: "NMSettings", nm_channels: pd.DataFrame
180
+ ) -> Projection | None:
181
+ """Return projection of used coordinated and grids"""
182
+
183
+ if not any(
184
+ (
185
+ settings.postprocessing.project_cortex,
186
+ settings.postprocessing.project_subcortex,
187
+ )
188
+ ):
189
+ return None
190
+
191
+ grid_cortex, grid_subcortex = self._get_grids(self.settings, self.path_grids)
192
+ projection = Projection(
193
+ settings=settings,
194
+ grid_cortex=grid_cortex,
195
+ grid_subcortex=grid_subcortex,
196
+ coords=self.coords,
197
+ nm_channels=nm_channels,
198
+ plot_projection=False,
199
+ )
200
+ return projection
201
+
202
+ @staticmethod
203
+ def _load_nm_channels(
204
+ nm_channels: pd.DataFrame | _PathLike,
205
+ ) -> pd.DataFrame:
206
+ if not isinstance(nm_channels, pd.DataFrame):
207
+ return nm_IO.load_nm_channels(nm_channels)
208
+ return nm_channels
209
+
210
+ def _set_coords(
211
+ self, coord_names: list[str] | None, coord_list: list | None
212
+ ) -> dict:
213
+ if not any(
214
+ (
215
+ self.settings.postprocessing.project_cortex,
216
+ self.settings.postprocessing.project_subcortex,
217
+ )
218
+ ):
219
+ return {}
220
+
221
+ if any((coord_list is None, coord_names is None)):
222
+ raise ValueError(
223
+ "No coordinates could be loaded. Please provide coord_list and"
224
+ f" coord_names. Got: {coord_list=}, {coord_names=}."
225
+ )
226
+
227
+ return self._add_coordinates(
228
+ coord_names=coord_names,
229
+ coord_list=coord_list, # type: ignore # None case handled above
230
+ )
231
+
232
+ def process(self, data: np.ndarray) -> dict[str, float]:
233
+ """Given a new data batch, calculate and return features.
234
+
235
+ Parameters
236
+ ----------
237
+ data : np.ndarray
238
+ Current batch of raw data
239
+
240
+ Returns
241
+ -------
242
+ pandas Series
243
+ Features calculated from current data
244
+ """
245
+ start_time = time()
246
+
247
+ nan_channels = np.isnan(data).any(axis=1)
248
+
249
+ data = np.nan_to_num(data)[self.feature_idx, :]
250
+
251
+ data = self.preprocessors.process_data(data)
252
+
253
+ # calculate features
254
+ features_dict = self.features.estimate_features(data)
255
+
256
+ # normalize features
257
+ if self.settings.postprocessing.feature_normalization:
258
+ normed_features = self.feature_normalizer.process(
259
+ np.fromiter(features_dict.values(), dtype=np.float64)
260
+ )
261
+ features_dict = {
262
+ key: normed_features[idx]
263
+ for idx, key in enumerate(features_dict.keys())
264
+ }
265
+
266
+ # project features to grid
267
+ if self.projection:
268
+ self.projection.project_features(features_dict)
269
+
270
+ # check for all features, where the channel had a NaN, that the feature is also put to NaN
271
+ if nan_channels.sum() > 0:
272
+ # TONI: no need to do this if we store both old and new names for the channels
273
+ new_nan_channels = []
274
+ for ch in list(np.array(self.ch_names_used)[nan_channels]):
275
+ for key in features_dict.keys():
276
+ if ch in key:
277
+ new_nan_channels.append(key)
278
+
279
+ for ch in new_nan_channels:
280
+ features_dict[ch] = np.nan
281
+
282
+ if self.verbose:
283
+ logger.info("Last batch took: %.3f seconds to process", time() - start_time)
284
+
285
+ return features_dict
286
+
287
+ def save_sidecar(
288
+ self,
289
+ out_dir: _PathLike,
290
+ prefix: str = "",
291
+ additional_args: dict | None = None,
292
+ ) -> None:
293
+ """Save sidecar incuding fs, coords, sess_right to out_dir."""
294
+
295
+ sidecar: dict = {
296
+ "original_fs": self._sfreq_raw_orig,
297
+ "final_fs": self.sfreq_raw,
298
+ "sfreq": self.sfreq_features,
299
+ }
300
+ if self.projection:
301
+ sidecar["coords"] = self.projection.coords
302
+ if self.settings.postprocessing.project_cortex:
303
+ sidecar["grid_cortex"] = self.projection.grid_cortex
304
+ sidecar["proj_matrix_cortex"] = self.projection.proj_matrix_cortex
305
+ if self.settings.postprocessing.project_subcortex:
306
+ sidecar["grid_subcortex"] = self.projection.grid_subcortex
307
+ sidecar["proj_matrix_subcortex"] = self.projection.proj_matrix_subcortex
308
+ if additional_args is not None:
309
+ sidecar = sidecar | additional_args
310
+
311
+ nm_IO.save_sidecar(sidecar, out_dir, prefix)
312
+
313
+ def save_settings(self, out_dir: _PathLike, prefix: str = "") -> None:
314
+ self.settings.save(out_dir, prefix)
315
+
316
+ def save_nm_channels(self, out_dir: _PathLike, prefix: str = "") -> None:
317
+ nm_IO.save_nm_channels(self.nm_channels, out_dir, prefix)
318
+
319
+ def save_features(
320
+ self,
321
+ feature_arr: pd.DataFrame,
322
+ out_dir: _PathLike = "",
323
+ prefix: str = "",
324
+ ) -> None:
325
+ nm_IO.save_features(feature_arr, out_dir, prefix)