py-neuromodulation 0.0.7__py3-none-any.whl → 0.1.0__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.
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +0 -1
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +0 -2
- py_neuromodulation/__init__.py +12 -4
- py_neuromodulation/analysis/RMAP.py +3 -3
- py_neuromodulation/analysis/decode.py +55 -2
- py_neuromodulation/analysis/feature_reader.py +1 -0
- py_neuromodulation/analysis/stats.py +3 -3
- py_neuromodulation/default_settings.yaml +24 -17
- py_neuromodulation/features/bandpower.py +65 -23
- py_neuromodulation/features/bursts.py +9 -8
- py_neuromodulation/features/coherence.py +7 -4
- py_neuromodulation/features/feature_processor.py +4 -4
- py_neuromodulation/features/fooof.py +7 -6
- py_neuromodulation/features/mne_connectivity.py +25 -3
- py_neuromodulation/features/oscillatory.py +5 -4
- py_neuromodulation/features/sharpwaves.py +21 -0
- py_neuromodulation/filter/kalman_filter.py +17 -6
- py_neuromodulation/gui/__init__.py +3 -0
- py_neuromodulation/gui/backend/app_backend.py +419 -0
- py_neuromodulation/gui/backend/app_manager.py +345 -0
- py_neuromodulation/gui/backend/app_pynm.py +244 -0
- py_neuromodulation/gui/backend/app_socket.py +95 -0
- py_neuromodulation/gui/backend/app_utils.py +306 -0
- py_neuromodulation/gui/backend/app_window.py +202 -0
- py_neuromodulation/gui/frontend/assets/Figtree-VariableFont_wght-CkXbWBDP.ttf +0 -0
- py_neuromodulation/gui/frontend/assets/index-NbJiOU5a.js +300133 -0
- py_neuromodulation/gui/frontend/assets/plotly-DTCwMlpS.js +23594 -0
- py_neuromodulation/gui/frontend/charite.svg +16 -0
- py_neuromodulation/gui/frontend/index.html +14 -0
- py_neuromodulation/gui/window_api.py +115 -0
- py_neuromodulation/lsl_api.cfg +3 -0
- py_neuromodulation/processing/data_preprocessor.py +9 -2
- py_neuromodulation/processing/filter_preprocessing.py +43 -27
- py_neuromodulation/processing/normalization.py +32 -17
- py_neuromodulation/processing/projection.py +2 -2
- py_neuromodulation/processing/resample.py +6 -2
- py_neuromodulation/run_gui.py +36 -0
- py_neuromodulation/stream/__init__.py +7 -1
- py_neuromodulation/stream/backend_interface.py +47 -0
- py_neuromodulation/stream/data_processor.py +24 -3
- py_neuromodulation/stream/mnelsl_player.py +121 -21
- py_neuromodulation/stream/mnelsl_stream.py +9 -17
- py_neuromodulation/stream/settings.py +80 -34
- py_neuromodulation/stream/stream.py +82 -62
- py_neuromodulation/utils/channels.py +1 -1
- py_neuromodulation/utils/file_writer.py +110 -0
- py_neuromodulation/utils/io.py +46 -5
- py_neuromodulation/utils/perf.py +156 -0
- py_neuromodulation/utils/pydantic_extensions.py +322 -0
- py_neuromodulation/utils/types.py +33 -107
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.0.dist-info}/METADATA +18 -4
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.0.dist-info}/RECORD +55 -35
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.0.dist-info}/WHEEL +1 -1
- py_neuromodulation-0.1.0.dist-info/entry_points.txt +2 -0
- {py_neuromodulation-0.0.7.dist-info → py_neuromodulation-0.1.0.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-NbJiOU5a.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)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, Type
|
|
2
|
-
from py_neuromodulation.utils.types import
|
|
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[
|
|
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 =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
30
|
+
return self.bandstop_filter_settings
|
|
37
31
|
case "bandpass_filter":
|
|
38
|
-
return
|
|
32
|
+
return self.bandpass_filter_settings
|
|
39
33
|
case "lowpass_filter":
|
|
40
|
-
return (None,
|
|
34
|
+
return FrequencyRange(None, self.lowpass_filter_cutoff_hz)
|
|
41
35
|
case "highpass_filter":
|
|
42
|
-
return (
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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:
|
|
22
|
-
clip: float =
|
|
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[
|
|
26
|
-
return list(get_args(
|
|
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.
|
|
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[
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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,
|
|
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 =
|
|
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).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 .
|
|
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 logging
|
|
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
|
+
self.logger = logging.getLogger("PyNM")
|
|
17
|
+
|
|
18
|
+
def send_command(self, command: str) -> None:
|
|
19
|
+
"""Send a command through the control queue"""
|
|
20
|
+
try:
|
|
21
|
+
self.control_queue.put(command)
|
|
22
|
+
except Exception as e:
|
|
23
|
+
self.logger.error(f"Error sending command: {e}")
|
|
24
|
+
|
|
25
|
+
def send_features(self, features: dict[str, Any]) -> None:
|
|
26
|
+
"""Send feature data through the feature queue"""
|
|
27
|
+
try:
|
|
28
|
+
self.feature_queue.put(features)
|
|
29
|
+
except Exception as e:
|
|
30
|
+
self.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
|
+
self.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
|
+
self.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
|
-
|
|
259
|
-
|
|
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())
|