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