py-neuromodulation 0.0.5__py3-none-any.whl → 0.0.7__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 (57) hide show
  1. py_neuromodulation/__init__.py +16 -10
  2. py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +2 -2
  3. py_neuromodulation/analysis/__init__.py +4 -0
  4. py_neuromodulation/{nm_decode.py → analysis/decode.py} +4 -4
  5. py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +21 -20
  6. py_neuromodulation/{nm_plots.py → analysis/plots.py} +54 -12
  7. py_neuromodulation/{nm_stats.py → analysis/stats.py} +2 -8
  8. py_neuromodulation/{nm_settings.yaml → default_settings.yaml} +7 -9
  9. py_neuromodulation/features/__init__.py +31 -0
  10. py_neuromodulation/features/bandpower.py +165 -0
  11. py_neuromodulation/{nm_bispectra.py → features/bispectra.py} +11 -12
  12. py_neuromodulation/{nm_bursts.py → features/bursts.py} +14 -9
  13. py_neuromodulation/{nm_coherence.py → features/coherence.py} +28 -19
  14. py_neuromodulation/{nm_features.py → features/feature_processor.py} +30 -53
  15. py_neuromodulation/{nm_fooof.py → features/fooof.py} +11 -8
  16. py_neuromodulation/{nm_hjorth_raw.py → features/hjorth_raw.py} +10 -5
  17. py_neuromodulation/{nm_linelength.py → features/linelength.py} +1 -1
  18. py_neuromodulation/{nm_mne_connectivity.py → features/mne_connectivity.py} +5 -6
  19. py_neuromodulation/{nm_nolds.py → features/nolds.py} +5 -7
  20. py_neuromodulation/{nm_oscillatory.py → features/oscillatory.py} +7 -181
  21. py_neuromodulation/{nm_sharpwaves.py → features/sharpwaves.py} +13 -4
  22. py_neuromodulation/filter/__init__.py +3 -0
  23. py_neuromodulation/{nm_kalmanfilter.py → filter/kalman_filter.py} +67 -71
  24. py_neuromodulation/filter/kalman_filter_external.py +1890 -0
  25. py_neuromodulation/{nm_filter.py → filter/mne_filter.py} +128 -219
  26. py_neuromodulation/filter/notch_filter.py +93 -0
  27. py_neuromodulation/processing/__init__.py +10 -0
  28. py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +2 -3
  29. py_neuromodulation/{nm_preprocessing.py → processing/data_preprocessor.py} +19 -25
  30. py_neuromodulation/{nm_filter_preprocessing.py → processing/filter_preprocessing.py} +3 -4
  31. py_neuromodulation/{nm_normalization.py → processing/normalization.py} +9 -7
  32. py_neuromodulation/{nm_projection.py → processing/projection.py} +14 -14
  33. py_neuromodulation/{nm_rereference.py → processing/rereference.py} +13 -13
  34. py_neuromodulation/{nm_resample.py → processing/resample.py} +1 -4
  35. py_neuromodulation/stream/__init__.py +3 -0
  36. py_neuromodulation/{nm_run_analysis.py → stream/data_processor.py} +42 -42
  37. py_neuromodulation/stream/generator.py +53 -0
  38. py_neuromodulation/{nm_mnelsl_generator.py → stream/mnelsl_player.py} +10 -6
  39. py_neuromodulation/{nm_mnelsl_stream.py → stream/mnelsl_stream.py} +13 -9
  40. py_neuromodulation/{nm_settings.py → stream/settings.py} +27 -24
  41. py_neuromodulation/{nm_stream.py → stream/stream.py} +217 -188
  42. py_neuromodulation/utils/__init__.py +2 -0
  43. py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +14 -9
  44. py_neuromodulation/{nm_database.py → utils/database.py} +2 -2
  45. py_neuromodulation/{nm_IO.py → utils/io.py} +42 -77
  46. py_neuromodulation/utils/keyboard.py +52 -0
  47. py_neuromodulation/{nm_logger.py → utils/logging.py} +3 -3
  48. py_neuromodulation/{nm_types.py → utils/types.py} +72 -14
  49. {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/METADATA +12 -29
  50. py_neuromodulation-0.0.7.dist-info/RECORD +89 -0
  51. py_neuromodulation/FieldTrip.py +0 -589
  52. py_neuromodulation/_write_example_dataset_helper.py +0 -83
  53. py_neuromodulation/nm_generator.py +0 -45
  54. py_neuromodulation/nm_stream_abc.py +0 -166
  55. py_neuromodulation-0.0.5.dist-info/RECORD +0 -83
  56. {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/WHEEL +0 -0
  57. {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/licenses/LICENSE +0 -0
@@ -1,17 +1,12 @@
1
- from collections.abc import Iterable
1
+ from collections.abc import Sequence
2
2
  import numpy as np
3
3
  from itertools import product
4
4
 
5
- from py_neuromodulation.nm_types import NMBaseModel
6
- from pydantic import field_validator
5
+ from py_neuromodulation.utils.types import NMBaseModel, BoolSelector, NMFeature
7
6
  from typing import TYPE_CHECKING
8
7
 
9
- from py_neuromodulation.nm_features import NMFeature
10
- from py_neuromodulation.nm_types import BoolSelector
11
-
12
8
  if TYPE_CHECKING:
13
- from py_neuromodulation.nm_settings import NMSettings
14
- from py_neuromodulation.nm_kalmanfilter import KalmanSettings
9
+ from py_neuromodulation.stream.settings import NMSettings
15
10
 
16
11
 
17
12
  class OscillatoryFeatures(BoolSelector):
@@ -40,7 +35,7 @@ ESTIMATOR_DICT = {
40
35
 
41
36
  class OscillatoryFeature(NMFeature):
42
37
  def __init__(
43
- self, settings: "NMSettings", ch_names: Iterable[str], sfreq: int
38
+ self, settings: "NMSettings", ch_names: Sequence[str], sfreq: int
44
39
  ) -> None:
45
40
  settings.validate()
46
41
  self.settings: OscillatorySettings # Assignment in subclass __init__
@@ -63,7 +58,7 @@ class FFT(OscillatoryFeature):
63
58
  def __init__(
64
59
  self,
65
60
  settings: "NMSettings",
66
- ch_names: Iterable[str],
61
+ ch_names: Sequence[str],
67
62
  sfreq: int,
68
63
  ) -> None:
69
64
  from scipy.fft import rfftfreq
@@ -127,7 +122,7 @@ class Welch(OscillatoryFeature):
127
122
  def __init__(
128
123
  self,
129
124
  settings: "NMSettings",
130
- ch_names: Iterable[str],
125
+ ch_names: Sequence[str],
131
126
  sfreq: int,
132
127
  ) -> None:
133
128
  from scipy.fft import rfftfreq
@@ -190,7 +185,7 @@ class STFT(OscillatoryFeature):
190
185
  def __init__(
191
186
  self,
192
187
  settings: "NMSettings",
193
- ch_names: Iterable[str],
188
+ ch_names: Sequence[str],
194
189
  sfreq: int,
195
190
  ) -> None:
196
191
  from scipy.fft import rfftfreq
@@ -252,172 +247,3 @@ class STFT(OscillatoryFeature):
252
247
  )[idx]
253
248
 
254
249
  return feature_results
255
-
256
-
257
- class BandpowerFeatures(BoolSelector):
258
- activity: bool = True
259
- mobility: bool = False
260
- complexity: bool = False
261
-
262
-
263
- ###################################
264
- ######## BANDPOWER FEATURE ########
265
- ###################################
266
-
267
-
268
- class BandpassSettings(NMBaseModel):
269
- segment_lengths_ms: dict[str, int] = {
270
- "theta": 1000,
271
- "alpha": 500,
272
- "low_beta": 333,
273
- "high_beta": 333,
274
- "low_gamma": 100,
275
- "high_gamma": 100,
276
- "HFA": 100,
277
- }
278
- bandpower_features: BandpowerFeatures = BandpowerFeatures()
279
- log_transform: bool = True
280
- kalman_filter: bool = False
281
-
282
- @field_validator("segment_lengths_ms")
283
- @classmethod
284
- # Replace spaces with underscores in frequency band names
285
- def fbands_spaces_to_underscores(cls, segment_lengths_ms: dict[str, int]):
286
- return {k.replace(" ", "_"): v for k, v in segment_lengths_ms.items()}
287
-
288
- @field_validator("bandpower_features")
289
- @classmethod
290
- def bandpower_features_validator(cls, bandpower_features: BandpowerFeatures):
291
- assert (
292
- len(bandpower_features.get_enabled()) > 0
293
- ), "Set at least one bandpower_feature to True."
294
-
295
- return bandpower_features
296
-
297
- def validate_fbands(self, settings: "NMSettings") -> None:
298
- # Ensure that each freq-band is defined in the global settings
299
- for fband_name in settings.frequency_ranges_hz.keys():
300
- assert fband_name in self.segment_lengths_ms, (
301
- f"frequency range {fband_name} "
302
- "needs to be defined in settings.bandpass_filter_settings.segment_lengths_ms]"
303
- )
304
-
305
- # Ensure that segment length for each freq-band is smaller than the feature segment length setting
306
- for fband_name, seg_length_fband in self.segment_lengths_ms.items():
307
- assert seg_length_fband <= settings.segment_length_features_ms, (
308
- f"segment length {seg_length_fband} needs to be smaller than "
309
- f" settings['segment_length_features_ms'] = {settings.segment_length_features_ms}"
310
- )
311
-
312
-
313
- class BandPower(NMFeature):
314
- def __init__(
315
- self,
316
- settings: "NMSettings",
317
- ch_names: Iterable[str],
318
- sfreq: float,
319
- use_kf: bool | None = None,
320
- ) -> None:
321
- settings.validate()
322
-
323
- self.bp_settings: BandpassSettings = settings.bandpass_filter_settings
324
- self.kalman_filter_settings: KalmanSettings = settings.kalman_filter_settings
325
- self.sfreq = sfreq
326
- self.ch_names = ch_names
327
- self.KF_dict: dict = {}
328
-
329
- from py_neuromodulation.nm_filter import MNEFilter
330
-
331
- self.bandpass_filter = MNEFilter(
332
- f_ranges=[
333
- tuple(frange) for frange in settings.frequency_ranges_hz.values()
334
- ],
335
- sfreq=self.sfreq,
336
- filter_length=self.sfreq - 1,
337
- verbose=False,
338
- )
339
-
340
- if use_kf or (use_kf is None and self.bp_settings.kalman_filter):
341
- self.init_KF("bandpass_activity")
342
-
343
- seglengths = self.bp_settings.segment_lengths_ms
344
-
345
- self.feature_params = []
346
- for ch_idx, ch_name in enumerate(self.ch_names):
347
- for f_band_idx, f_band in enumerate(settings.frequency_ranges_hz.keys()):
348
- seglength_ms = seglengths[f_band]
349
- seglen = int(np.floor(self.sfreq / 1000 * seglength_ms))
350
- for bp_feature in self.bp_settings.bandpower_features.get_enabled():
351
- feature_name = "_".join([ch_name, "bandpass", bp_feature, f_band])
352
- self.feature_params.append(
353
- (
354
- ch_idx,
355
- f_band_idx,
356
- seglen,
357
- bp_feature,
358
- feature_name,
359
- )
360
- )
361
-
362
- def init_KF(self, feature: str) -> None:
363
- from py_neuromodulation.nm_kalmanfilter import define_KF
364
-
365
- for f_band in self.kalman_filter_settings.frequency_bands:
366
- for channel in self.ch_names:
367
- self.KF_dict["_".join([channel, feature, f_band])] = define_KF(
368
- self.kalman_filter_settings.Tp,
369
- self.kalman_filter_settings.sigma_w,
370
- self.kalman_filter_settings.sigma_v,
371
- )
372
-
373
- def update_KF(self, feature_calc: np.floating, KF_name: str) -> np.floating:
374
- if KF_name in self.KF_dict:
375
- self.KF_dict[KF_name].predict()
376
- self.KF_dict[KF_name].update(feature_calc)
377
- feature_calc = self.KF_dict[KF_name].x[0]
378
- return feature_calc
379
-
380
- def calc_feature(self, data: np.ndarray) -> dict:
381
- data = self.bandpass_filter.filter_data(data)
382
-
383
- feature_results = {}
384
-
385
- for (
386
- ch_idx,
387
- f_band_idx,
388
- seglen,
389
- bp_feature,
390
- feature_name,
391
- ) in self.feature_params:
392
- feature_results[feature_name] = self.calc_bp_feature(
393
- bp_feature, feature_name, data[ch_idx, f_band_idx, -seglen:]
394
- )
395
-
396
- return feature_results
397
-
398
- def calc_bp_feature(self, bp_feature, feature_name, data):
399
- match bp_feature:
400
- case "activity":
401
- feature_calc = np.var(data)
402
- if self.bp_settings.log_transform:
403
- feature_calc = np.log10(feature_calc)
404
- if self.KF_dict:
405
- feature_calc = self.update_KF(feature_calc, feature_name)
406
- case "mobility":
407
- feature_calc = np.sqrt(np.var(np.diff(data)) / np.var(data))
408
- case "complexity":
409
- feature_calc = self.calc_complexity(data)
410
- case _:
411
- raise ValueError(f"Unknown bandpower feature: {bp_feature}")
412
-
413
- return np.nan_to_num(feature_calc)
414
-
415
- @staticmethod
416
- def calc_complexity(data: np.ndarray) -> float:
417
- dat_deriv = np.diff(data)
418
- deriv_variance = np.var(dat_deriv)
419
- mobility = np.sqrt(deriv_variance / np.var(data))
420
- dat_deriv_2_var = np.var(np.diff(dat_deriv))
421
- deriv_mobility = np.sqrt(dat_deriv_2_var / deriv_variance)
422
-
423
- return deriv_mobility / mobility
@@ -2,7 +2,6 @@ from collections.abc import Sequence
2
2
  from collections import defaultdict
3
3
  from itertools import product
4
4
 
5
- from py_neuromodulation.nm_types import NMBaseModel
6
5
  from pydantic import model_validator
7
6
  from typing import TYPE_CHECKING, Any, Callable
8
7
 
@@ -13,11 +12,15 @@ if np.__version__ >= "2.0.0":
13
12
  else:
14
13
  from numpy.core._methods import _mean as np_mean
15
14
 
16
- from py_neuromodulation.nm_features import NMFeature
17
- from py_neuromodulation.nm_types import BoolSelector, FrequencyRange
15
+ from py_neuromodulation.utils.types import (
16
+ NMFeature,
17
+ NMBaseModel,
18
+ BoolSelector,
19
+ FrequencyRange,
20
+ )
18
21
 
19
22
  if TYPE_CHECKING:
20
- from py_neuromodulation.nm_settings import NMSettings
23
+ from py_neuromodulation import NMSettings
21
24
 
22
25
  # Using low-level numpy mean function for performance, could do the same for the other estimators
23
26
  ESTIMATOR_DICT = {
@@ -38,6 +41,7 @@ class PeakDetectionSettings(NMBaseModel):
38
41
  class SharpwaveFeatures(BoolSelector):
39
42
  peak_left: bool = False
40
43
  peak_right: bool = False
44
+ num_peaks: bool = False
41
45
  trough: bool = False
42
46
  width: bool = False
43
47
  prominence: bool = True
@@ -368,6 +372,11 @@ class SharpwaveAnalyzer(NMFeature):
368
372
  # results["sharpness"] = ((trough_height - left_height) + (trough_height - right_height)) / 2
369
373
  results["sharpness"] = trough_height - 0.5 * (left_height + right_height)
370
374
 
375
+ if self.sw_settings.sharpwave_features.num_peaks:
376
+ results["num_peaks"] = [
377
+ trough_idx.shape[0]
378
+ ] # keep list to the estimator can be applied
379
+
371
380
  if self.need_steepness:
372
381
  # steepness is calculated as the first derivative
373
382
  steepness: np.ndarray = np.concatenate((np.zeros(1), np.diff(data)))
@@ -0,0 +1,3 @@
1
+ from .kalman_filter import define_KF, KalmanSettings
2
+ from .notch_filter import NotchFilter
3
+ from .mne_filter import MNEFilter
@@ -1,71 +1,67 @@
1
- from numpy import array, cov
2
- from py_neuromodulation.nm_types import NMBaseModel
3
- from typing import TYPE_CHECKING
4
-
5
- from pydantic import field_validator
6
-
7
- if TYPE_CHECKING:
8
- from py_neuromodulation.nm_settings import NMSettings
9
-
10
-
11
- class KalmanSettings(NMBaseModel):
12
- Tp: float = 0.1
13
- sigma_w: float = 0.7
14
- sigma_v: float = 1.0
15
- frequency_bands: list[str] = [
16
- "theta",
17
- "alpha",
18
- "low_beta",
19
- "high_beta",
20
- "low_gamma",
21
- "high_gamma",
22
- "HFA",
23
- ]
24
-
25
- @field_validator("frequency_bands")
26
- def fbands_spaces_to_underscores(cls, frequency_bands):
27
- return [f.replace(" ", "_") for f in frequency_bands]
28
-
29
- def validate_fbands(self, settings: "NMSettings") -> None:
30
- assert all(
31
- (item in settings.frequency_ranges_hz for item in self.frequency_bands)
32
- ), (
33
- "Frequency bands for Kalman filter must also be specified in "
34
- "bandpass_filter_settings."
35
- )
36
-
37
-
38
- def define_KF(Tp, sigma_w, sigma_v):
39
- """Define Kalman filter according to white noise acceleration model.
40
- See DOI: 10.1109/TBME.2009.2038990 for explanation
41
- See https://filterpy.readthedocs.io/en/latest/kalman/KalmanFilter.html#r64ca38088676-2 for implementation details
42
-
43
- Parameters
44
- ----------
45
- Tp : float
46
- prediction interval
47
- sigma_w : float
48
- process noise
49
- sigma_v : float
50
- measurement noise
51
-
52
- Returns
53
- -------
54
- filterpy.KalmanFilter
55
- initialized KalmanFilter object
56
- """
57
- from filterpy.kalman import KalmanFilter
58
-
59
- f = KalmanFilter(dim_x=2, dim_z=1)
60
- f.x = array([0, 1]) # x here sensor signal and it's first derivative
61
- f.F = array([[1, Tp], [0, 1]])
62
- f.H = array([[1, 0]])
63
- f.R = sigma_v
64
- f.Q = array(
65
- [
66
- [(sigma_w**2) * (Tp**3) / 3, (sigma_w**2) * (Tp**2) / 2],
67
- [(sigma_w**2) * (Tp**2) / 2, (sigma_w**2) * Tp],
68
- ]
69
- )
70
- f.P = cov([[1, 0], [0, 1]])
71
- return f
1
+ import numpy as np
2
+ from typing import TYPE_CHECKING
3
+
4
+ from py_neuromodulation.utils.types import NMBaseModel
5
+
6
+
7
+ if TYPE_CHECKING:
8
+ from py_neuromodulation.stream.settings import NMSettings
9
+
10
+
11
+ class KalmanSettings(NMBaseModel):
12
+ Tp: float = 0.1
13
+ sigma_w: float = 0.7
14
+ sigma_v: float = 1.0
15
+ frequency_bands: list[str] = [
16
+ "theta",
17
+ "alpha",
18
+ "low_beta",
19
+ "high_beta",
20
+ "low_gamma",
21
+ "high_gamma",
22
+ "HFA",
23
+ ]
24
+
25
+ def validate_fbands(self, settings: "NMSettings") -> None:
26
+ assert all(
27
+ (item in settings.frequency_ranges_hz for item in self.frequency_bands)
28
+ ), (
29
+ "Frequency bands for Kalman filter must also be specified in "
30
+ "bandpass_filter_settings."
31
+ )
32
+
33
+
34
+ def define_KF(Tp, sigma_w, sigma_v):
35
+ """Define Kalman filter according to white noise acceleration model.
36
+ See DOI: 10.1109/TBME.2009.2038990 for explanation
37
+ See https://filterpy.readthedocs.io/en/latest/kalman/KalmanFilter.html#r64ca38088676-2 for implementation details
38
+
39
+ Parameters
40
+ ----------
41
+ Tp : float
42
+ prediction interval
43
+ sigma_w : float
44
+ process noise
45
+ sigma_v : float
46
+ measurement noise
47
+
48
+ Returns
49
+ -------
50
+ filterpy.KalmanFilter
51
+ initialized KalmanFilter object
52
+ """
53
+ from .kalman_filter_external import KalmanFilter
54
+
55
+ f = KalmanFilter(dim_x=2, dim_z=1)
56
+ f.x = np.array([0, 1]) # x here sensor signal and it's first derivative
57
+ f.F = np.array([[1, Tp], [0, 1]])
58
+ f.H = np.array([[1, 0]])
59
+ f.R = sigma_v
60
+ f.Q = np.array(
61
+ [
62
+ [(sigma_w**2) * (Tp**3) / 3, (sigma_w**2) * (Tp**2) / 2],
63
+ [(sigma_w**2) * (Tp**2) / 2, (sigma_w**2) * Tp],
64
+ ]
65
+ )
66
+ f.P = np.cov([[1, 0], [0, 1]])
67
+ return f