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,394 +1,370 @@
1
- from typing import Optional, Union
2
-
3
- import numpy as np
4
- import pandas as pd
5
-
6
- from py_neuromodulation import nm_plots
7
-
8
-
9
- class Projection:
10
- def __init__(
11
- self,
12
- settings: dict,
13
- grid_cortex: pd.DataFrame,
14
- grid_subcortex: pd.DataFrame,
15
- coords: dict,
16
- nm_channels: pd.DataFrame,
17
- plot_projection: bool = False,
18
- ) -> None:
19
-
20
- self.test_settings(settings)
21
-
22
- self.grid_cortex = grid_cortex
23
- self.grid_subcortex = grid_subcortex
24
- self.coords = coords
25
- self.nm_channels = nm_channels
26
- self.project_cortex = settings["postprocessing"]["project_cortex"]
27
- self.project_subcortex = settings["postprocessing"][
28
- "project_subcortex"
29
- ]
30
- self.max_dist_cortex = settings["project_cortex_settings"][
31
- "max_dist_mm"
32
- ]
33
- self.max_dist_subcortex = settings["project_subcortex_settings"][
34
- "max_dist_mm"
35
- ]
36
- self.ecog_channels: Optional[list] = None
37
- self.lfp_channels: Optional[list] = None
38
-
39
- self.idx_chs_ecog: list = (
40
- []
41
- ) # feature series indexes for ecog channels
42
- self.names_chs_ecog: list = [] # feature series name of ecog features
43
- self.idx_chs_lfp: list = [] # feature series indexes for lfp channels
44
- self.names_chs_lfp: list = [] # feature series name of lfp features
45
- self.feature_names: Optional[list] = None
46
- self.initialized: bool = False
47
-
48
- self.remove_not_used_ch_from_coords() # remove beforehand non used channels from coords
49
-
50
- if len(self.coords["cortex_left"]["positions"]) == 0:
51
- self.sess_right = True
52
- self.ecog_strip = self.coords["cortex_right"]["positions"]
53
- self.ecog_strip_names = self.coords["cortex_right"]["ch_names"]
54
- elif len(self.coords["cortex_right"]["positions"]) == 0:
55
- self.sess_right = False
56
- self.ecog_strip = self.coords["cortex_left"]["positions"]
57
- self.ecog_strip_names = self.coords["cortex_left"]["ch_names"]
58
-
59
- if (
60
- self.sess_right is True
61
- and len(self.coords["subcortex_right"]["positions"]) > 0
62
- ):
63
- self.lfp_elec = self.coords["subcortex_right"]["positions"]
64
- self.lfp_elec_names = self.coords["subcortex_right"]["ch_names"]
65
- elif (
66
- self.sess_right is False
67
- and len(self.coords["subcortex_left"]["positions"]) > 0
68
- ):
69
- self.lfp_elec = self.coords["subcortex_left"]["positions"]
70
- self.lfp_elec_names = self.coords["subcortex_left"]["ch_names"]
71
-
72
- self._initialize_channels()
73
-
74
- (
75
- self.proj_matrix_cortex,
76
- self.proj_matrix_subcortex,
77
- ) = self.calc_projection_matrix()
78
-
79
- if self.project_cortex:
80
- self.active_cortex_gridpoints = np.nonzero(
81
- self.proj_matrix_cortex.sum(axis=1)
82
- )[0]
83
- if self.project_subcortex:
84
- self.active_subcortex_gridpoints = np.nonzero(
85
- self.proj_matrix_subcortex.sum(axis=1)
86
- )[0]
87
-
88
- if plot_projection is True:
89
- nmplotter = nm_plots.NM_Plot(
90
- ecog_strip=self.ecog_strip,
91
- grid_cortex=self.grid_cortex.to_numpy(),
92
- grid_subcortex=self.grid_subcortex.to_numpy(),
93
- sess_right=self.sess_right,
94
- proj_matrix_cortex=self.proj_matrix_cortex,
95
- )
96
- nmplotter.plot_cortex()
97
-
98
- @staticmethod
99
- def test_settings(s: dict):
100
- if s["postprocessing"]["project_cortex"] is True:
101
- assert isinstance(
102
- s["project_cortex_settings"]["max_dist_mm"], (float, int)
103
- )
104
- if s["postprocessing"]["project_subcortex"] is True:
105
- assert isinstance(
106
- s["project_subcortex_settings"]["max_dist_mm"], (float, int)
107
- )
108
-
109
- def remove_not_used_ch_from_coords(self):
110
- ch_not_used = self.nm_channels.query(
111
- '(used==0) or (status=="bad")'
112
- ).name
113
- if len(ch_not_used) > 0:
114
- for ch in ch_not_used:
115
- for key_ in self.coords:
116
- for idx, ch_coords in enumerate(
117
- self.coords[key_]["ch_names"]
118
- ):
119
- if ch.startswith(ch_coords):
120
- # delete index
121
- self.coords[key_]["positions"] = np.delete(
122
- self.coords[key_]["positions"], idx, axis=0
123
- )
124
- self.coords[key_]["ch_names"].remove(ch)
125
-
126
- def calc_proj_matrix(
127
- self,
128
- max_dist: Union[int, float],
129
- grid: np.ndarray,
130
- coord_array: np.ndarray,
131
- ) -> np.ndarray:
132
- """Calculate projection matrix."""
133
-
134
- channels = coord_array.shape[0]
135
- distance_matrix = np.zeros([grid.shape[1], channels])
136
-
137
- for project_point in range(grid.shape[1]):
138
- for channel in range(coord_array.shape[0]):
139
- distance_matrix[project_point, channel] = np.linalg.norm(
140
- grid[:, project_point] - coord_array[channel, :]
141
- )
142
-
143
- proj_matrix = np.zeros(distance_matrix.shape)
144
- for grid_point in range(distance_matrix.shape[0]):
145
- used_channels = np.where(
146
- distance_matrix[grid_point, :] < max_dist
147
- )[0]
148
-
149
- rec_distances = distance_matrix[grid_point, used_channels]
150
- sum_distances = np.sum(1 / rec_distances)
151
-
152
- for _, used_channel in enumerate(used_channels):
153
- proj_matrix[grid_point, used_channel] = (
154
- 1 / distance_matrix[grid_point, used_channel]
155
- ) / sum_distances
156
- return proj_matrix
157
-
158
- def calc_projection_matrix(self):
159
- """Calculates a projection matrix based on the used coordinate arrays
160
-
161
- Returns
162
- -------
163
- proj_matrix_cortex (np.array)
164
- cortical projection_matrix in shape [grid contacts, channel contact] defaults to None
165
- proj_matrix_subcortex (np.array)
166
- subcortical projection_matrix in shape [grid contacts, channel contact] defaults to None
167
- """
168
-
169
- proj_matrix_run = np.empty(2, dtype=object)
170
-
171
- if self.sess_right is True:
172
-
173
- if self.project_cortex:
174
- cortex_grid_right = np.copy(self.grid_cortex)
175
- cortex_grid_right[:, 0] = cortex_grid_right[:, 0] * -1
176
- self.cortex_grid_right = np.array(cortex_grid_right.T)
177
- else:
178
- self.cortex_grid_right = None
179
-
180
- if self.project_subcortex:
181
- subcortex_grid_right = np.copy(self.grid_subcortex)
182
- subcortex_grid_right[:, 0] = subcortex_grid_right[:, 0] * -1
183
- self.subcortex_grid_right = np.array(subcortex_grid_right).T
184
- else:
185
- self.subcortex_grid_right = None
186
-
187
- grid_session = [self.cortex_grid_right, self.subcortex_grid_right]
188
-
189
- else:
190
- if self.project_cortex:
191
- self.cortex_grid_left = np.array(self.grid_cortex.T)
192
- else:
193
- self.cortex_grid_left = None
194
- if self.project_subcortex:
195
- self.subcortex_grid_left = np.array(self.grid_subcortex.T)
196
- else:
197
- self.subcortex_grid_left = None
198
-
199
- grid_session = [self.cortex_grid_left, self.subcortex_grid_left]
200
-
201
- coord_array = [
202
- self.ecog_strip if grid_session[0] is not None else None,
203
- self.lfp_elec if grid_session[1] is not None else None,
204
- ]
205
-
206
- for loc_, grid in enumerate(grid_session):
207
- if loc_ == 0: # cortex
208
- max_dist = self.max_dist_cortex
209
- elif loc_ == 1: # subcortex
210
- max_dist = self.max_dist_subcortex
211
-
212
- if grid_session[loc_] is not None:
213
- proj_matrix_run[loc_] = self.calc_proj_matrix(
214
- max_dist, grid, coord_array[loc_]
215
- )
216
-
217
- return proj_matrix_run[0], proj_matrix_run[1] # cortex, subcortex
218
-
219
- def _initialize_channels(self) -> None:
220
- """Initialize channel names via nm_channel new_name column"""
221
-
222
- if self.project_cortex:
223
- self.ecog_channels = self.nm_channels.query(
224
- '(type=="ecog") and (used == 1) and (status=="good")'
225
- ).name.to_list()
226
-
227
- chs_ecog = self.ecog_channels.copy()
228
- for ecog_channel in chs_ecog:
229
- if ecog_channel not in self.ecog_strip_names:
230
- self.ecog_channels.remove(ecog_channel)
231
- # write ecog_channels to be new_name
232
- self.ecog_channels = list(
233
- self.nm_channels.query("name == @self.ecog_channels").new_name
234
- )
235
-
236
- if self.project_subcortex:
237
- self.lfp_channels = self.nm_channels.query(
238
- '(type=="lfp" or type=="seeg" or type=="dbs") \
239
- and (used == 1) and (status=="good")'
240
- ).name.to_list()
241
- # project only channels that are in the coords
242
- # this also deletes channels of the other hemisphere
243
- chs_lfp = self.lfp_channels.copy()
244
- for lfp_channel in chs_lfp:
245
- if lfp_channel not in self.lfp_elec_names:
246
- self.lfp_channels.remove(lfp_channel)
247
- # write lfp_channels to be new_name
248
- self.lfp_channels = list(
249
- self.nm_channels.query("name == @self.lfp_channels").new_name
250
- )
251
-
252
- def init_projection_run(self, feature_series: pd.Series) -> None:
253
- """Initialize indexes for respective channels in feature series computed by nm_features.py"""
254
- # here it is assumed that only one hemisphere is recorded at a time!
255
- if self.project_cortex:
256
- for ecog_channel in self.ecog_channels:
257
- self.idx_chs_ecog.append(
258
- [
259
- ch_idx
260
- for ch_idx, ch in enumerate(feature_series.keys())
261
- if ch.startswith(ecog_channel)
262
- ]
263
- )
264
- self.names_chs_ecog.append(
265
- [
266
- ch
267
- for _, ch in enumerate(feature_series.keys())
268
- if ch.startswith(ecog_channel)
269
- ]
270
- )
271
- if self.names_chs_ecog:
272
- # get feature_names; given by ECoG sequency of features
273
- self.feature_names = [
274
- feature_name[len(self.ecog_channels[0]) + 1 :]
275
- for feature_name in self.names_chs_ecog[0]
276
- ]
277
-
278
- if self.project_subcortex:
279
- # for lfp_channels select here only the ones from the correct hemisphere!
280
- for lfp_channel in self.lfp_channels:
281
- self.idx_chs_lfp.append(
282
- [
283
- ch_idx
284
- for ch_idx, ch in enumerate(feature_series.keys())
285
- if ch.startswith(lfp_channel)
286
- ]
287
- )
288
- self.names_chs_lfp.append(
289
- [
290
- ch
291
- for _, ch in enumerate(feature_series.keys())
292
- if ch.startswith(lfp_channel)
293
- ]
294
- )
295
- if not self.feature_names and self.names_chs_lfp:
296
- # get feature_names; given by LFP sequency of features
297
- self.feature_names = [
298
- feature_name[len(self.lfp_channels[0]) + 1 :]
299
- for feature_name in self.names_chs_lfp[0]
300
- ]
301
-
302
- self.initialized = True
303
-
304
- def project_features(self, feature_series: pd.Series) -> pd.Series:
305
- """Project data, given idx_chs_ecog/stn"""
306
-
307
- if not self.initialized:
308
- self.init_projection_run(feature_series=feature_series)
309
-
310
- dat_cortex = (
311
- np.vstack(
312
- [
313
- feature_series.iloc[idx_ch].values
314
- for idx_ch in self.idx_chs_ecog
315
- ]
316
- )
317
- if self.project_cortex
318
- else None
319
- )
320
-
321
- dat_subcortex = (
322
- np.vstack(
323
- [
324
- feature_series.iloc[idx_ch].values
325
- for idx_ch in self.idx_chs_lfp
326
- ]
327
- )
328
- if self.project_subcortex
329
- else None
330
- )
331
-
332
- # project data
333
- (
334
- proj_cortex_array,
335
- proj_subcortex_array,
336
- ) = self.get_projected_cortex_subcortex_data(dat_cortex, dat_subcortex)
337
-
338
- features_new = {}
339
- # proj_cortex_array has shape grid_points x feature_number
340
- if self.project_cortex:
341
-
342
- features_new = features_new | {
343
- "gridcortex_"
344
- + str(act_grid_point)
345
- + "_"
346
- + feature_name: proj_cortex_array[act_grid_point, feature_idx]
347
- for feature_idx, feature_name in enumerate(self.feature_names)
348
- for act_grid_point in self.active_cortex_gridpoints
349
- }
350
- if self.project_subcortex:
351
- features_new = features_new | {
352
- "gridsubcortex_"
353
- + str(act_grid_point)
354
- + "_"
355
- + feature_name: proj_subcortex_array[
356
- act_grid_point, feature_idx
357
- ]
358
- for feature_idx, feature_name in enumerate(self.feature_names)
359
- for act_grid_point in self.active_subcortex_gridpoints
360
- }
361
- feature_series = pd.concat([feature_series, pd.Series(features_new)])
362
-
363
- return feature_series
364
-
365
- def get_projected_cortex_subcortex_data(
366
- self,
367
- dat_cortex: Optional[np.ndarray] = None,
368
- dat_subcortex: Optional[np.ndarray] = None,
369
- ) -> tuple[Optional[np.ndarray], Optional[np.ndarray]]:
370
- """Project cortical and subcortical data to predefined projection matrices
371
-
372
- Parameters
373
- ----------
374
- dat_cortex : np.ndarray, optional
375
- cortical features, by default None
376
- dat_subcortex : np.ndarray, optional
377
- subcortical features, by default None
378
-
379
- Returns
380
- -------
381
- proj_cortex : np.ndarray
382
- projected cortical features, by detault None
383
- proj_subcortex : np.ndarray
384
- projected subcortical features, by detault None
385
- """
386
- proj_cortex = None
387
- proj_subcortex = None
388
-
389
- if dat_cortex is not None:
390
- proj_cortex = self.proj_matrix_cortex @ dat_cortex
391
- if dat_subcortex is not None:
392
- proj_subcortex = self.proj_matrix_subcortex @ dat_subcortex
393
-
394
- return proj_cortex, proj_subcortex
1
+ import numpy as np
2
+ from pydantic import Field
3
+ from py_neuromodulation.utils.types import NMBaseModel
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from py_neuromodulation import NMSettings
8
+ import pandas as pd
9
+
10
+
11
+ class ProjectionSettings(NMBaseModel):
12
+ max_dist_mm: float = Field(default=20.0, gt=0.0)
13
+
14
+
15
+ class Projection:
16
+ def __init__(
17
+ self,
18
+ settings: "NMSettings",
19
+ grid_cortex: "pd.DataFrame",
20
+ grid_subcortex: "pd.DataFrame",
21
+ coords: dict,
22
+ channels: "pd.DataFrame",
23
+ plot_projection: bool = False,
24
+ ) -> None:
25
+ self.grid_cortex = grid_cortex
26
+ self.grid_subcortex = grid_subcortex
27
+ self.coords = coords
28
+ self.channels = channels
29
+ self.project_cortex = settings.postprocessing.project_cortex
30
+ self.project_subcortex = settings.postprocessing.project_subcortex
31
+ self.max_dist_cortex = settings.project_cortex_settings.max_dist_mm
32
+ self.max_dist_subcortex = settings.project_subcortex_settings.max_dist_mm
33
+ self.ecog_channels: list # None case never handled, no need for default value
34
+ self.lfp_channels: list # None case never handled, no need for default value
35
+
36
+ self.idx_chs_ecog: list = [] # feature series indexes for ecog channels
37
+ self.names_chs_ecog: list = [] # feature series name of ecog features
38
+ self.idx_chs_lfp: list = [] # feature series indexes for lfp channels
39
+ self.names_chs_lfp: list = [] # feature series name of lfp features
40
+ self.feature_names: list | None = None
41
+ self.initialized: bool = False
42
+
43
+ self.remove_not_used_ch_from_coords() # remove beforehand non used channels from coords
44
+
45
+ if len(self.coords["cortex_left"]["positions"]) == 0:
46
+ self.sess_right = True
47
+ self.ecog_strip = self.coords["cortex_right"]["positions"]
48
+ self.ecog_strip_names = self.coords["cortex_right"]["ch_names"]
49
+ elif len(self.coords["cortex_right"]["positions"]) == 0:
50
+ self.sess_right = False
51
+ self.ecog_strip = self.coords["cortex_left"]["positions"]
52
+ self.ecog_strip_names = self.coords["cortex_left"]["ch_names"]
53
+
54
+ if self.sess_right and len(self.coords["subcortex_right"]["positions"]) > 0:
55
+ self.lfp_elec = self.coords["subcortex_right"]["positions"]
56
+ self.lfp_elec_names = self.coords["subcortex_right"]["ch_names"]
57
+ elif (
58
+ self.sess_right is False
59
+ and len(self.coords["subcortex_left"]["positions"]) > 0
60
+ ):
61
+ self.lfp_elec = self.coords["subcortex_left"]["positions"]
62
+ self.lfp_elec_names = self.coords["subcortex_left"]["ch_names"]
63
+
64
+ self._initialize_channels()
65
+
66
+ (
67
+ self.proj_matrix_cortex,
68
+ self.proj_matrix_subcortex,
69
+ ) = self.calc_projection_matrix()
70
+
71
+ if self.project_cortex:
72
+ self.active_cortex_gridpoints = np.nonzero(
73
+ self.proj_matrix_cortex.sum(axis=1)
74
+ )[0]
75
+ if self.project_subcortex:
76
+ self.active_subcortex_gridpoints = np.nonzero(
77
+ self.proj_matrix_subcortex.sum(axis=1)
78
+ )[0]
79
+
80
+ if plot_projection:
81
+ from py_neuromodulation.analysis.plots import NM_Plot
82
+
83
+ nmplotter = NM_Plot(
84
+ ecog_strip=self.ecog_strip,
85
+ grid_cortex=self.grid_cortex.to_numpy(),
86
+ grid_subcortex=self.grid_subcortex.to_numpy(),
87
+ sess_right=self.sess_right,
88
+ proj_matrix_cortex=self.proj_matrix_cortex,
89
+ )
90
+ nmplotter.plot_cortex()
91
+
92
+ def remove_not_used_ch_from_coords(self):
93
+ ch_not_used = self.channels.query('(used==0) or (status=="bad")').name
94
+ if len(ch_not_used) > 0:
95
+ for ch in ch_not_used:
96
+ for key_ in self.coords:
97
+ for idx, ch_coords in enumerate(self.coords[key_]["ch_names"]):
98
+ if ch.startswith(ch_coords):
99
+ # delete index
100
+ self.coords[key_]["positions"] = np.delete(
101
+ self.coords[key_]["positions"], idx, axis=0
102
+ )
103
+ self.coords[key_]["ch_names"].remove(ch)
104
+
105
+ def calc_proj_matrix(
106
+ self,
107
+ max_dist: float,
108
+ grid: np.ndarray,
109
+ coord_array: np.ndarray,
110
+ ) -> np.ndarray:
111
+ """Calculate projection matrix."""
112
+
113
+ channels = coord_array.shape[0]
114
+ distance_matrix = np.zeros([grid.shape[1], channels])
115
+
116
+ for project_point in range(grid.shape[1]):
117
+ for channel in range(coord_array.shape[0]):
118
+ distance_matrix[project_point, channel] = np.linalg.norm(
119
+ grid[:, project_point] - coord_array[channel, :]
120
+ )
121
+
122
+ proj_matrix = np.zeros(distance_matrix.shape)
123
+ for grid_point in range(distance_matrix.shape[0]):
124
+ used_channels = np.where(distance_matrix[grid_point, :] < max_dist)[0]
125
+
126
+ rec_distances = distance_matrix[grid_point, used_channels]
127
+ sum_distances: float = np.sum(1 / rec_distances)
128
+
129
+ for _, used_channel in enumerate(used_channels):
130
+ proj_matrix[grid_point, used_channel] = (
131
+ 1 / distance_matrix[grid_point, used_channel]
132
+ ) / sum_distances
133
+ return proj_matrix
134
+
135
+ def calc_projection_matrix(self):
136
+ """Calculates a projection matrix based on the used coordinate arrays
137
+
138
+ Returns
139
+ -------
140
+ proj_matrix_cortex (np.array)
141
+ cortical projection_matrix in shape [grid contacts, channel contact] defaults to None
142
+ proj_matrix_subcortex (np.array)
143
+ subcortical projection_matrix in shape [grid contacts, channel contact] defaults to None
144
+ """
145
+
146
+ proj_matrix_run = np.empty(2, dtype=object)
147
+
148
+ if self.sess_right:
149
+ if self.project_cortex:
150
+ cortex_grid_right = np.copy(self.grid_cortex)
151
+ cortex_grid_right[:, 0] = cortex_grid_right[:, 0] * -1
152
+ self.cortex_grid_right = np.array(cortex_grid_right.T)
153
+ else:
154
+ self.cortex_grid_right = None
155
+
156
+ if self.project_subcortex:
157
+ subcortex_grid_right = np.copy(self.grid_subcortex)
158
+ subcortex_grid_right[:, 0] = subcortex_grid_right[:, 0] * -1
159
+ self.subcortex_grid_right = np.array(subcortex_grid_right).T
160
+ else:
161
+ self.subcortex_grid_right = None
162
+
163
+ grid_session = [self.cortex_grid_right, self.subcortex_grid_right]
164
+
165
+ else:
166
+ if self.project_cortex:
167
+ self.cortex_grid_left = np.array(self.grid_cortex.T)
168
+ else:
169
+ self.cortex_grid_left = None
170
+ if self.project_subcortex:
171
+ self.subcortex_grid_left = np.array(self.grid_subcortex.T)
172
+ else:
173
+ self.subcortex_grid_left = None
174
+
175
+ grid_session = [self.cortex_grid_left, self.subcortex_grid_left]
176
+
177
+ coord_array = [
178
+ self.ecog_strip if grid_session[0] is not None else None,
179
+ self.lfp_elec if grid_session[1] is not None else None,
180
+ ]
181
+
182
+ for loc_, grid in enumerate(grid_session):
183
+ if loc_ == 0: # cortex
184
+ max_dist = self.max_dist_cortex
185
+ elif loc_ == 1: # subcortex
186
+ max_dist = self.max_dist_subcortex
187
+
188
+ if grid_session[loc_] is not None:
189
+ proj_matrix_run[loc_] = self.calc_proj_matrix(
190
+ max_dist, grid, coord_array[loc_]
191
+ )
192
+
193
+ return proj_matrix_run[0], proj_matrix_run[1] # cortex, subcortex
194
+
195
+ def _initialize_channels(self) -> None:
196
+ """Initialize channel names via nm_channel new_name column"""
197
+
198
+ if self.project_cortex:
199
+ self.ecog_channels = self.channels.query(
200
+ '(type=="ecog") and (used == 1) and (status=="good")'
201
+ ).name.to_list()
202
+
203
+ chs_ecog = self.ecog_channels.copy()
204
+ for ecog_channel in chs_ecog:
205
+ if ecog_channel not in self.ecog_strip_names:
206
+ self.ecog_channels.remove(ecog_channel)
207
+ # write ecog_channels to be new_name
208
+ self.ecog_channels = list(
209
+ self.channels.query("name == @self.ecog_channels").new_name
210
+ )
211
+
212
+ if self.project_subcortex:
213
+ self.lfp_channels = self.channels.query(
214
+ '(type=="lfp" or type=="seeg" or type=="dbs") \
215
+ and (used == 1) and (status=="good")'
216
+ ).name.to_list()
217
+ # project only channels that are in the coords
218
+ # this also deletes channels of the other hemisphere
219
+ chs_lfp = self.lfp_channels.copy()
220
+ for lfp_channel in chs_lfp:
221
+ if lfp_channel not in self.lfp_elec_names:
222
+ self.lfp_channels.remove(lfp_channel)
223
+ # write lfp_channels to be new_name
224
+ self.lfp_channels = list(
225
+ self.channels.query("name == @self.lfp_channels").new_name
226
+ )
227
+
228
+ def init_projection_run(self, feature_dict: dict) -> None:
229
+ """Initialize indexes for respective channels in feature series computed by features.py"""
230
+ # here it is assumed that only one hemisphere is recorded at a time!
231
+ if self.project_cortex:
232
+ for ecog_channel in self.ecog_channels:
233
+ self.idx_chs_ecog.append(
234
+ [
235
+ ch_idx
236
+ for ch_idx, ch in enumerate(feature_dict.keys())
237
+ if ch.startswith(ecog_channel)
238
+ ]
239
+ )
240
+ self.names_chs_ecog.append(
241
+ [
242
+ ch
243
+ for _, ch in enumerate(feature_dict.keys())
244
+ if ch.startswith(ecog_channel)
245
+ ]
246
+ )
247
+ if self.names_chs_ecog:
248
+ # get feature_names; given by ECoG sequency of features
249
+ self.feature_names = [
250
+ feature_name[len(self.ecog_channels[0]) + 1 :]
251
+ for feature_name in self.names_chs_ecog[0]
252
+ ]
253
+
254
+ if self.project_subcortex:
255
+ # for lfp_channels select here only the ones from the correct hemisphere!
256
+ for lfp_channel in self.lfp_channels:
257
+ self.idx_chs_lfp.append(
258
+ [
259
+ ch_idx
260
+ for ch_idx, ch in enumerate(feature_dict.keys())
261
+ if ch.startswith(lfp_channel)
262
+ ]
263
+ )
264
+ self.names_chs_lfp.append(
265
+ [
266
+ ch
267
+ for _, ch in enumerate(feature_dict.keys())
268
+ if ch.startswith(lfp_channel)
269
+ ]
270
+ )
271
+ if not self.feature_names and self.names_chs_lfp:
272
+ # get feature_names; given by LFP sequency of features
273
+ self.feature_names = [
274
+ feature_name[len(self.lfp_channels[0]) + 1 :]
275
+ for feature_name in self.names_chs_lfp[0]
276
+ ]
277
+
278
+ self.initialized = True
279
+
280
+ def project_features(self, feature_dict: dict) -> None:
281
+ """Project data, given idx_chs_ecog/stn"""
282
+
283
+ if not self.initialized:
284
+ self.init_projection_run(feature_dict)
285
+
286
+ dat_cortex = (
287
+ np.array(
288
+ [
289
+ np.array([feature_dict[ch] for ch in ch_names])
290
+ for ch_names in self.names_chs_ecog
291
+ ]
292
+ )
293
+ if self.project_cortex
294
+ else None
295
+ )
296
+
297
+ dat_subcortex = (
298
+ np.array(
299
+ [
300
+ np.array([feature_dict[ch] for ch in ch_names])
301
+ for ch_names in self.names_chs_lfp
302
+ ]
303
+ )
304
+ if self.project_subcortex
305
+ else None
306
+ )
307
+
308
+ # project data
309
+ # get_projected_cortex_subcortex_data can return None
310
+ # but None is not handled and will throw error in the code below
311
+
312
+ proj_cortex_array: np.ndarray
313
+ proj_subcortex_array: np.ndarray
314
+ (proj_cortex_array, proj_subcortex_array) = (
315
+ self.get_projected_cortex_subcortex_data(dat_cortex, dat_subcortex)
316
+ ) # type: ignore # Ignore None return
317
+
318
+ features_new: dict = {}
319
+ # proj_cortex_array has shape grid_points x feature_number
320
+ if self.project_cortex:
321
+ features_new = features_new | {
322
+ "gridcortex_"
323
+ + str(act_grid_point)
324
+ + "_"
325
+ + feature_name: proj_cortex_array[act_grid_point, feature_idx]
326
+ for feature_idx, feature_name in enumerate(self.feature_names) # type: ignore # Empty list handled above
327
+ for act_grid_point in self.active_cortex_gridpoints
328
+ }
329
+ if self.project_subcortex:
330
+ features_new = features_new | {
331
+ "gridsubcortex_"
332
+ + str(act_grid_point)
333
+ + "_"
334
+ + feature_name: proj_subcortex_array[act_grid_point, feature_idx]
335
+ for feature_idx, feature_name in enumerate(self.feature_names) # type: ignore # Empty list handled above
336
+ for act_grid_point in self.active_subcortex_gridpoints
337
+ }
338
+
339
+ feature_dict.update(features_new)
340
+
341
+ def get_projected_cortex_subcortex_data(
342
+ self,
343
+ dat_cortex: np.ndarray | None = None,
344
+ dat_subcortex: np.ndarray | None = None,
345
+ ) -> tuple[np.ndarray | None, np.ndarray | None]:
346
+ """Project cortical and subcortical data to predefined projection matrices
347
+
348
+ Parameters
349
+ ----------
350
+ dat_cortex : np.ndarray, optional
351
+ cortical features, by default None
352
+ dat_subcortex : np.ndarray, optional
353
+ subcortical features, by default None
354
+
355
+ Returns
356
+ -------
357
+ proj_cortex : np.ndarray
358
+ projected cortical features, by detault None
359
+ proj_subcortex : np.ndarray
360
+ projected subcortical features, by detault None
361
+ """
362
+ proj_cortex = None
363
+ proj_subcortex = None
364
+
365
+ if dat_cortex is not None:
366
+ proj_cortex = self.proj_matrix_cortex @ dat_cortex
367
+ if dat_subcortex is not None:
368
+ proj_subcortex = self.proj_matrix_subcortex @ dat_subcortex
369
+
370
+ return proj_cortex, proj_subcortex