py-neuromodulation 0.0.7__py3-none-any.whl → 0.1.1__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 (55) hide show
  1. py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +0 -1
  2. py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +0 -2
  3. py_neuromodulation/__init__.py +12 -4
  4. py_neuromodulation/analysis/RMAP.py +3 -3
  5. py_neuromodulation/analysis/decode.py +55 -2
  6. py_neuromodulation/analysis/feature_reader.py +1 -0
  7. py_neuromodulation/analysis/stats.py +3 -3
  8. py_neuromodulation/default_settings.yaml +25 -20
  9. py_neuromodulation/features/bandpower.py +65 -23
  10. py_neuromodulation/features/bursts.py +9 -8
  11. py_neuromodulation/features/coherence.py +7 -4
  12. py_neuromodulation/features/feature_processor.py +4 -4
  13. py_neuromodulation/features/fooof.py +7 -6
  14. py_neuromodulation/features/mne_connectivity.py +60 -87
  15. py_neuromodulation/features/oscillatory.py +5 -4
  16. py_neuromodulation/features/sharpwaves.py +21 -0
  17. py_neuromodulation/filter/kalman_filter.py +17 -6
  18. py_neuromodulation/gui/__init__.py +3 -0
  19. py_neuromodulation/gui/backend/app_backend.py +419 -0
  20. py_neuromodulation/gui/backend/app_manager.py +345 -0
  21. py_neuromodulation/gui/backend/app_pynm.py +253 -0
  22. py_neuromodulation/gui/backend/app_socket.py +97 -0
  23. py_neuromodulation/gui/backend/app_utils.py +306 -0
  24. py_neuromodulation/gui/backend/app_window.py +202 -0
  25. py_neuromodulation/gui/frontend/assets/Figtree-VariableFont_wght-CkXbWBDP.ttf +0 -0
  26. py_neuromodulation/gui/frontend/assets/index-_6V8ZfAS.js +300137 -0
  27. py_neuromodulation/gui/frontend/assets/plotly-DTCwMlpS.js +23594 -0
  28. py_neuromodulation/gui/frontend/charite.svg +16 -0
  29. py_neuromodulation/gui/frontend/index.html +14 -0
  30. py_neuromodulation/gui/window_api.py +115 -0
  31. py_neuromodulation/lsl_api.cfg +3 -0
  32. py_neuromodulation/processing/data_preprocessor.py +9 -2
  33. py_neuromodulation/processing/filter_preprocessing.py +43 -27
  34. py_neuromodulation/processing/normalization.py +32 -17
  35. py_neuromodulation/processing/projection.py +2 -2
  36. py_neuromodulation/processing/resample.py +6 -2
  37. py_neuromodulation/run_gui.py +36 -0
  38. py_neuromodulation/stream/__init__.py +7 -1
  39. py_neuromodulation/stream/backend_interface.py +47 -0
  40. py_neuromodulation/stream/data_processor.py +24 -3
  41. py_neuromodulation/stream/mnelsl_player.py +121 -21
  42. py_neuromodulation/stream/mnelsl_stream.py +9 -17
  43. py_neuromodulation/stream/settings.py +80 -34
  44. py_neuromodulation/stream/stream.py +83 -62
  45. py_neuromodulation/utils/channels.py +1 -1
  46. py_neuromodulation/utils/file_writer.py +110 -0
  47. py_neuromodulation/utils/io.py +46 -5
  48. py_neuromodulation/utils/perf.py +156 -0
  49. py_neuromodulation/utils/pydantic_extensions.py +322 -0
  50. py_neuromodulation/utils/types.py +33 -107
  51. {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/METADATA +23 -4
  52. {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/RECORD +55 -35
  53. {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/WHEEL +1 -1
  54. py_neuromodulation-0.1.1.dist-info/entry_points.txt +2 -0
  55. {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 26.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" id="svg3265" xmlns:svg="http://www.w3.org/2000/svg"
4
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 295 295.5"
5
+ style="enable-background:new 0 0 295 295.5;" xml:space="preserve">
6
+ <style type="text/css">
7
+ .st0{fill-rule:evenodd;clip-rule:evenodd;}
8
+ .st1{fill-rule:evenodd;clip-rule:evenodd;fill:#878787;}
9
+ </style>
10
+ <path id="polygon10" class="st0" d="M158,94.4c-33.3,0-54.1,24.9-54.1,53.7c0,28.8,21,52.9,54.1,52.9c10.8,0,21.5-3.6,30.2-9.9
11
+ v-13.7c-8.1,8.1-19.1,13.3-30.6,13.3c-24.9,0-41.3-20.5-41.3-43c0-22.4,16.1-43,41.1-43c11.8,0,22.8,5,30.9,13.3v-13.7
12
+ C179.2,97.7,169.3,94.4,158,94.4z"/>
13
+ <path id="path26" class="st1" d="M0,147.7C0,66.1,65.7,0,149,0c36.5,0,69.8,13.2,95.6,35.1v22.7c-23.8-25.2-57.5-41-94.9-41
14
+ c-73,0-130,58.4-130,130.4c0,72,57.1,130.4,130,130.4c37.4,0,71.1-15.7,94.9-41v23.7c-25.8,21.9-59.1,35.1-95.6,35.1
15
+ C65.8,295.5,0,229.3,0,147.7L0,147.7L0,147.7z"/>
16
+ </svg>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/charite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PyNeuromodulation</title>
8
+ <script type="module" crossorigin src="/assets/index-_6V8ZfAS.js"></script>
9
+ <link rel="modulepreload" crossorigin href="/assets/plotly-DTCwMlpS.js">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,115 @@
1
+ import webview
2
+ import threading
3
+ import time
4
+
5
+
6
+ # API class implementing all the methods available in the PyWebView Window object
7
+ # API Reference: https://pywebview.flowrl.com/guide/api.html#webview-window
8
+ class WindowAPI:
9
+ def __init__(self):
10
+ self.window = None
11
+ self.is_resizing = False
12
+ self.start_x = 0
13
+ self.start_y = 0
14
+ self.start_width = 0
15
+ self.start_height = 0
16
+
17
+ # Function to store the reference to the PyWevView window
18
+ def register_window(self, window: webview.Window):
19
+ self._window = window
20
+
21
+ # Functions to handle window resizing
22
+ def start_resize(self, start_x, start_y):
23
+ self.is_resizing = True
24
+ self.start_x = start_x
25
+ self.start_y = start_y
26
+ self.start_width, self.start_height = self.get_size()
27
+ threading.Thread(target=self._resize_loop).start()
28
+
29
+ def stop_resize(self):
30
+ self.is_resizing = False
31
+
32
+ def update_resize(self, current_x, current_y):
33
+ if self.is_resizing:
34
+ dx = current_x - self.start_x
35
+ dy = current_y - self.start_y
36
+ new_width = max(self.start_width + dx, 200) # Minimum width
37
+ new_height = max(self.start_height + dy, 200) # Minimum height
38
+ self.set_size(int(new_width), int(new_height))
39
+
40
+ def _resize_loop(self):
41
+ while self.is_resizing:
42
+ time.sleep(0.01) # Small delay to prevent excessive CPU usage
43
+
44
+ # All API methods from the PyWebView docs
45
+ def close_window(self):
46
+ self._window.destroy()
47
+
48
+ def maximize_window(self):
49
+ self._window.maximize()
50
+
51
+ def minimize_window(self):
52
+ self._window.minimize()
53
+
54
+ def restore_window(self):
55
+ self._window.restore()
56
+
57
+ def toggle_fullscreen(self):
58
+ self._window.toggle_fullscreen()
59
+
60
+ def set_title(self, title: str):
61
+ self._window.title = title
62
+
63
+ def get_position(self):
64
+ return (self._window.x, self._window.y)
65
+
66
+ def set_position(self, x: int, y: int):
67
+ self._window.move(x, y)
68
+
69
+ def get_size(self):
70
+ return (self._window.width, self._window.height)
71
+
72
+ def set_size(self, width: int, height: int):
73
+ self._window.resize(width, height)
74
+
75
+ def set_on_top(self, on_top: bool):
76
+ self._window.on_top = on_top
77
+
78
+ def show(self):
79
+ self._window.show()
80
+
81
+ def hide(self):
82
+ self._window.hide()
83
+
84
+ def create_file_dialog(
85
+ self,
86
+ dialog_type=webview.OPEN_DIALOG,
87
+ directory="",
88
+ allow_multiple=False,
89
+ save_filename="",
90
+ file_types=(),
91
+ ):
92
+ return self._window.create_file_dialog(
93
+ dialog_type, directory, allow_multiple, save_filename, file_types
94
+ )
95
+
96
+ def create_confirmation_dialog(self, title, message):
97
+ return self._window.create_confirmation_dialog(title, message)
98
+
99
+ def load_url(self, url):
100
+ self._window.load_url(url)
101
+
102
+ def load_html(self, content, base_uri: str):
103
+ self._window.load_html(content, base_uri)
104
+
105
+ def load_css(self, css):
106
+ self._window.load_css(css)
107
+
108
+ def evaluate_js(self, script, callback=None):
109
+ return self._window.evaluate_js(script, callback)
110
+
111
+ def get_current_url(self):
112
+ return self._window.get_current_url()
113
+
114
+ def get_elements(self, selector):
115
+ return self._window.get_elements(selector)
@@ -0,0 +1,3 @@
1
+ [log]
2
+ level = 0
3
+ file = ./lsllog.txt
@@ -1,12 +1,12 @@
1
1
  from typing import TYPE_CHECKING, Type
2
- from py_neuromodulation.utils.types import PreprocessorName, NMPreprocessor
2
+ from py_neuromodulation.utils.types import PREPROCESSOR_NAME, NMPreprocessor
3
3
 
4
4
  if TYPE_CHECKING:
5
5
  import numpy as np
6
6
  import pandas as pd
7
7
  from py_neuromodulation.stream.settings import NMSettings
8
8
 
9
- PREPROCESSOR_DICT: dict[PreprocessorName, str] = {
9
+ PREPROCESSOR_DICT: dict[PREPROCESSOR_NAME, str] = {
10
10
  "preprocessing_filter": "PreprocessingFilter",
11
11
  "notch_filter": "NotchFilter",
12
12
  "raw_resampling": "Resampler",
@@ -72,6 +72,13 @@ class DataPreprocessor:
72
72
  ]
73
73
 
74
74
  def process_data(self, data: "np.ndarray") -> "np.ndarray":
75
+ """
76
+ Args:
77
+ data (np.ndarray): shape: (n_channels, n_samples)
78
+
79
+ Returns:
80
+ np.ndarray: shape: (n_channels, n_samples)
81
+ """
75
82
  for preprocessor in self.preprocessors:
76
83
  data = preprocessor.process(data)
77
84
  return data
@@ -1,22 +1,14 @@
1
1
  import numpy as np
2
2
 
3
- from pydantic import Field
4
3
  from typing import TYPE_CHECKING
5
4
 
6
5
  from py_neuromodulation.utils.types import BoolSelector, FrequencyRange, NMPreprocessor
6
+ from py_neuromodulation.utils.pydantic_extensions import NMField
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from py_neuromodulation import NMSettings
10
10
 
11
11
 
12
- FILTER_SETTINGS_MAP = {
13
- "bandstop_filter": "bandstop_filter_settings",
14
- "bandpass_filter": "bandpass_filter_settings",
15
- "lowpass_filter": "lowpass_filter_cutoff_hz",
16
- "highpass_filter": "highpass_filter_cutoff_hz",
17
- }
18
-
19
-
20
12
  class FilterSettings(BoolSelector):
21
13
  bandstop_filter: bool = True
22
14
  bandpass_filter: bool = True
@@ -25,21 +17,23 @@ class FilterSettings(BoolSelector):
25
17
 
26
18
  bandstop_filter_settings: FrequencyRange = FrequencyRange(100, 160)
27
19
  bandpass_filter_settings: FrequencyRange = FrequencyRange(2, 200)
28
- lowpass_filter_cutoff_hz: float = Field(default=200)
29
- highpass_filter_cutoff_hz: float = Field(default=3)
30
-
31
- def get_filter_tuple(self, filter_name) -> tuple[float | None, float | None]:
32
- filter_value = self[FILTER_SETTINGS_MAP[filter_name]]
33
-
20
+ lowpass_filter_cutoff_hz: float = NMField(
21
+ default=200, gt=0, custom_metadata={"unit": "Hz"}
22
+ )
23
+ highpass_filter_cutoff_hz: float = NMField(
24
+ default=3, gt=0, custom_metadata={"unit": "Hz"}
25
+ )
26
+
27
+ def get_filter_tuple(self, filter_name) -> FrequencyRange:
34
28
  match filter_name:
35
29
  case "bandstop_filter":
36
- return (filter_value.frequency_high_hz, filter_value.frequency_low_hz)
30
+ return self.bandstop_filter_settings
37
31
  case "bandpass_filter":
38
- return (filter_value.frequency_low_hz, filter_value.frequency_high_hz)
32
+ return self.bandpass_filter_settings
39
33
  case "lowpass_filter":
40
- return (None, filter_value)
34
+ return FrequencyRange(None, self.lowpass_filter_cutoff_hz)
41
35
  case "highpass_filter":
42
- return (filter_value, None)
36
+ return FrequencyRange(self.highpass_filter_cutoff_hz, None)
43
37
  case _:
44
38
  raise ValueError(
45
39
  "Filter name must be one of 'bandstop_filter', 'lowpass_filter', "
@@ -51,15 +45,37 @@ class PreprocessingFilter(NMPreprocessor):
51
45
  def __init__(self, settings: "NMSettings", sfreq: float) -> None:
52
46
  from py_neuromodulation.filter import MNEFilter
53
47
 
54
- self.filters: list[MNEFilter] = [
55
- MNEFilter(
56
- f_ranges=[settings.preprocessing_filter.get_filter_tuple(filter_name)], # type: ignore
57
- sfreq=sfreq,
58
- filter_length=sfreq - 1,
59
- verbose=False,
48
+
49
+ self.filters: list[MNEFilter] = []
50
+ for filter_name in settings.preprocessing_filter.get_enabled():
51
+ if filter_name != "lowpass_filter" and filter_name != "highpass_filter":
52
+ self.filters += [
53
+ MNEFilter(
54
+ f_ranges=[settings.preprocessing_filter.get_filter_tuple(filter_name)], # type: ignore
55
+ sfreq=sfreq,
56
+ filter_length=sfreq - 1,
57
+ verbose=False,
58
+ )
59
+ ]
60
+
61
+ if "lowpass_filter" in settings.preprocessing_filter.get_enabled():
62
+ self.filters.append(
63
+ MNEFilter(
64
+ f_ranges=[[None, settings.preprocessing_filter.lowpass_filter_cutoff_hz]], # type: ignore
65
+ sfreq=sfreq,
66
+ filter_length=sfreq - 1,
67
+ verbose=False,
68
+ )
69
+ )
70
+ if "highpass_filter" in settings.preprocessing_filter.get_enabled():
71
+ self.filters.append(
72
+ MNEFilter(
73
+ f_ranges=[[settings.preprocessing_filter.highpass_filter_cutoff_hz, None]], # type: ignore
74
+ sfreq=sfreq,
75
+ filter_length=sfreq - 1,
76
+ verbose=False,
77
+ )
60
78
  )
61
- for filter_name in settings.preprocessing_filter.get_enabled()
62
- ]
63
79
 
64
80
  def process(self, data: np.ndarray) -> np.ndarray:
65
81
  """Preprocess data according to the initialized list of PreprocessingFilter objects
@@ -3,10 +3,10 @@
3
3
  import numpy as np
4
4
  from typing import TYPE_CHECKING, Callable, Literal, get_args
5
5
 
6
+ from py_neuromodulation.utils.pydantic_extensions import NMField
6
7
  from py_neuromodulation.utils.types import (
7
8
  NMBaseModel,
8
- Field,
9
- NormMethod,
9
+ NORM_METHOD,
10
10
  NMPreprocessor,
11
11
  )
12
12
 
@@ -17,14 +17,15 @@ NormalizerType = Literal["raw", "feature"]
17
17
 
18
18
 
19
19
  class NormalizationSettings(NMBaseModel):
20
- normalization_time_s: float = 30
21
- normalization_method: NormMethod = "zscore"
22
- clip: float = Field(default=3, ge=0)
20
+ normalization_time_s: float = NMField(30, gt=0, custom_metadata={"unit": "s"})
21
+ normalization_method: NORM_METHOD = NMField(default="zscore")
22
+ clip: float = NMField(default=3, ge=0, custom_metadata={"unit": "a.u."})
23
23
 
24
24
  @staticmethod
25
- def list_normalization_methods() -> list[NormMethod]:
26
- return list(get_args(NormMethod))
25
+ def list_normalization_methods() -> list[NORM_METHOD]:
26
+ return list(get_args(NORM_METHOD))
27
27
 
28
+ class FeatureNormalizationSettings(NormalizationSettings): normalize_psd: bool = False
28
29
 
29
30
  class Normalizer(NMPreprocessor):
30
31
  def __init__(
@@ -32,9 +33,13 @@ class Normalizer(NMPreprocessor):
32
33
  sfreq: float,
33
34
  settings: "NMSettings",
34
35
  type: NormalizerType,
36
+ **kwargs,
35
37
  ) -> None:
36
38
  self.type = type
37
- self.settings: NormalizationSettings
39
+ if self.type == "raw":
40
+ self.settings: NormalizationSettings
41
+ else:
42
+ self.settings: FeatureNormalizationSettings
38
43
 
39
44
  match self.type:
40
45
  case "raw":
@@ -55,7 +60,7 @@ class Normalizer(NMPreprocessor):
55
60
  if self.using_sklearn:
56
61
  import sklearn.preprocessing as skpp
57
62
 
58
- NORM_METHODS_SKLEARN: dict[NormMethod, Callable] = {
63
+ NORM_METHODS_SKLEARN: dict[NORM_METHOD, Callable] = {
59
64
  "quantile": lambda: skpp.QuantileTransformer(n_quantiles=300),
60
65
  "robust": skpp.RobustScaler,
61
66
  "minmax": skpp.MinMaxScaler,
@@ -74,14 +79,24 @@ class Normalizer(NMPreprocessor):
74
79
  self.normalizer = NORM_FUNCTIONS[self.method]
75
80
 
76
81
  def process(self, data: np.ndarray) -> np.ndarray:
77
- # TODO: does feature normalization need to be transposed too?
78
- if self.type == "raw":
79
- data = data.T
82
+ """Process normalization.
83
+ Note: raw data has to be internally transposed, s.t. raw and features
84
+ are normalized in the same way.
80
85
 
86
+ Args:
87
+ data (np.ndarray): shape (channels, n_samples)
88
+
89
+ Returns:
90
+ np.ndarray: (channels, n_samples)
91
+ """
92
+
81
93
  if self.previous.size == 0: # Check if empty
82
94
  self.previous = data
83
- return data if self.type == "raw" else data.T
84
-
95
+ if self.type == "raw":
96
+ self.previous = self.previous.T
97
+ return data
98
+ if self.type == "raw":
99
+ data = data.T
85
100
  self.previous = np.vstack((self.previous, data[-self.add_samples :]))
86
101
 
87
102
  data = self.normalizer(data, self.previous)
@@ -93,12 +108,12 @@ class Normalizer(NMPreprocessor):
93
108
 
94
109
  data = np.nan_to_num(data)
95
110
 
96
- return data if self.type == "raw" else data.T
111
+ return data if self.type != "raw" else data.T
97
112
 
98
113
 
99
114
  class RawNormalizer(Normalizer):
100
- def __init__(self, sfreq: float, settings: "NMSettings") -> None:
101
- super().__init__(sfreq, settings, "raw")
115
+ def __init__(self, sfreq: float, settings: "NMSettings", **kwargs,) -> None:
116
+ super().__init__(sfreq, settings, "raw", **kwargs)
102
117
 
103
118
 
104
119
  class FeatureNormalizer(Normalizer):
@@ -1,6 +1,6 @@
1
1
  import numpy as np
2
- from pydantic import Field
3
2
  from py_neuromodulation.utils.types import NMBaseModel
3
+ from py_neuromodulation.utils.pydantic_extensions import NMField
4
4
  from typing import TYPE_CHECKING
5
5
 
6
6
  if TYPE_CHECKING:
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
 
10
10
 
11
11
  class ProjectionSettings(NMBaseModel):
12
- max_dist_mm: float = Field(default=20.0, gt=0.0)
12
+ max_dist_mm: float = NMField(default=20.0, gt=0.0, custom_metadata={"unit": "mm"})
13
13
 
14
14
 
15
15
  class Projection:
@@ -1,11 +1,14 @@
1
1
  """Module for resampling."""
2
2
 
3
3
  import numpy as np
4
- from py_neuromodulation.utils.types import NMBaseModel, Field, NMPreprocessor
4
+ from py_neuromodulation.utils.types import NMBaseModel, NMPreprocessor
5
+ from py_neuromodulation.utils.pydantic_extensions import NMField
5
6
 
6
7
 
7
8
  class ResamplerSettings(NMBaseModel):
8
- resample_freq_hz: float = Field(default=1000, gt=0)
9
+ resample_freq_hz: float = NMField(
10
+ default=1000, gt=0, custom_metadata={"unit": "Hz"}
11
+ )
9
12
 
10
13
 
11
14
  class Resampler(NMPreprocessor):
@@ -26,6 +29,7 @@ class Resampler(NMPreprocessor):
26
29
  self,
27
30
  sfreq: float,
28
31
  resample_freq_hz: float,
32
+ **kwargs,
29
33
  ) -> None:
30
34
  self.settings = ResamplerSettings(resample_freq_hz=resample_freq_hz)
31
35
 
@@ -0,0 +1,36 @@
1
+ import webbrowser
2
+
3
+ from py_neuromodulation.stream import (
4
+ LSLStream,
5
+ LSLOfflinePlayer,
6
+ )
7
+ from py_neuromodulation import io
8
+ from py_neuromodulation import App
9
+
10
+
11
+ def main():
12
+ (
13
+ RUN_NAME,
14
+ PATH_RUN,
15
+ PATH_BIDS,
16
+ PATH_OUT,
17
+ datatype,
18
+ ) = io.get_paths_example_data()
19
+
20
+ (
21
+ raw,
22
+ data,
23
+ sfreq,
24
+ line_noise,
25
+ coord_list,
26
+ coord_names,
27
+ ) = io.read_BIDS_data(PATH_RUN=PATH_RUN)
28
+
29
+ player = LSLOfflinePlayer(raw=raw, stream_name="example_stream")
30
+
31
+ player.start_player(chunk_size=30, n_repeat=5999999)
32
+
33
+ App(run_in_webview=False, dev=False, debug=False).launch()
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -1,3 +1,9 @@
1
1
  from .generator import RawDataGenerator
2
2
  from .mnelsl_player import LSLOfflinePlayer
3
- from .mnelsl_stream import LSLStream
3
+ from .stream import Stream
4
+ from .settings import NMSettings
5
+
6
+ try:
7
+ from .mnelsl_stream import LSLStream
8
+ except Exception as e:
9
+ print(f"A RuntimeError occurred: {e}")
@@ -0,0 +1,47 @@
1
+ from typing import Any
2
+ import py_neuromodulation as nm
3
+ import multiprocessing as mp
4
+
5
+
6
+ class StreamBackendInterface:
7
+ """Handles stream data output via queues"""
8
+
9
+ def __init__(
10
+ self, feature_queue: mp.Queue, raw_data_queue: mp.Queue, control_queue: mp.Queue
11
+ ):
12
+ self.feature_queue = feature_queue
13
+ self.rawdata_queue = raw_data_queue
14
+ self.control_queue = control_queue
15
+
16
+ def send_command(self, command: str) -> None:
17
+ """Send a command through the control queue"""
18
+ try:
19
+ self.control_queue.put(command)
20
+ except Exception as e:
21
+ nm.logger.error(f"Error sending command: {e}")
22
+
23
+ def send_features(self, features: dict[str, Any]) -> None:
24
+ """Send feature data through the feature queue"""
25
+ try:
26
+ nm.logger.debug("backend_interface.send_features before feature put in queue")
27
+ self.feature_queue.put(features)
28
+ nm.logger.debug("backend_interface.send_features after feature put in queue")
29
+ except Exception as e:
30
+ nm.logger.error(f"Error sending features: {e}")
31
+
32
+ def send_raw_data(self, data: dict[str, Any]) -> None:
33
+ """Send raw data through the rawdata queue"""
34
+ try:
35
+ self.rawdata_queue.put(data)
36
+ except Exception as e:
37
+ nm.logger.error(f"Error sending raw data: {e}")
38
+
39
+ def check_control_signals(self) -> str | None:
40
+ """Check for control signals (non-blocking)"""
41
+ try:
42
+ if not self.control_queue.empty():
43
+ return self.control_queue.get_nowait()
44
+ return None
45
+ except Exception as e:
46
+ nm.logger.error(f"Error checking control signals: {e}")
47
+ return None
@@ -55,6 +55,7 @@ class DataProcessor:
55
55
  self.sfreq_raw: float = sfreq // 1
56
56
  self.line_noise: float | None = line_noise
57
57
  self.path_grids: _PathLike | None = path_grids
58
+ self.non_psd_indices: np.ndarray | None = None
58
59
  self.verbose: bool = verbose
59
60
 
60
61
  self.features_previous = None
@@ -255,9 +256,29 @@ class DataProcessor:
255
256
 
256
257
  # normalize features
257
258
  if self.settings.postprocessing.feature_normalization:
258
- normed_features = self.feature_normalizer.process(
259
- np.fromiter(features_dict.values(), dtype=np.float64)
260
- )
259
+ if not self.settings.feature_normalization_settings.normalize_psd:
260
+ if self.non_psd_indices is None:
261
+ self.non_psd_indices = [
262
+ idx
263
+ for idx, key in enumerate(features_dict.keys())
264
+ if "psd" not in key
265
+ ]
266
+ self.psd_indices = list(set(range(len(features_dict))) - set(
267
+ self.non_psd_indices
268
+ ))
269
+ feature_values = np.fromiter(features_dict.values(), dtype=np.float64)
270
+ normed_features_non_psd = self.feature_normalizer.process(
271
+ feature_values[self.non_psd_indices]
272
+ )
273
+
274
+ # combine values in new array
275
+ normed_features = np.empty((feature_values.shape[0]))
276
+ normed_features[self.non_psd_indices] = normed_features_non_psd
277
+ normed_features[self.psd_indices] = feature_values[self.psd_indices]
278
+ else:
279
+ normed_features = self.feature_normalizer.process(
280
+ np.fromiter(features_dict.values(), dtype=np.float64)
281
+ )
261
282
  features_dict = {
262
283
  key: normed_features[idx]
263
284
  for idx, key in enumerate(features_dict.keys())