nucleuskit-toolkit 2.2.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.
- nucleuskit_toolkit/__init__.py +3 -0
- nucleuskit_toolkit/__main__.py +60 -0
- nucleuskit_toolkit/app.py +34 -0
- nucleuskit_toolkit/camera/__init__.py +9 -0
- nucleuskit_toolkit/camera/processing.py +374 -0
- nucleuskit_toolkit/config.py +86 -0
- nucleuskit_toolkit/hermes/__init__.py +0 -0
- nucleuskit_toolkit/hermes/constants.py +77 -0
- nucleuskit_toolkit/hermes/realtime/__init__.py +8 -0
- nucleuskit_toolkit/hermes/realtime/proxy.py +652 -0
- nucleuskit_toolkit/logging_utils.py +68 -0
- nucleuskit_toolkit/mqtt/__init__.py +5 -0
- nucleuskit_toolkit/mqtt/mqtt_controller.py +256 -0
- nucleuskit_toolkit/pipeline.py +194 -0
- nucleuskit_toolkit/position/__init__.py +5 -0
- nucleuskit_toolkit/position/gps_processor.py +91 -0
- nucleuskit_toolkit/session_job.py +28 -0
- nucleuskit_toolkit/shimmer/__init__.py +5 -0
- nucleuskit_toolkit/shimmer/constants.py +32 -0
- nucleuskit_toolkit/shimmer/realtime/__init__.py +5 -0
- nucleuskit_toolkit/shimmer/realtime/proxy.py +279 -0
- nucleuskit_toolkit/tools/__init__.py +1 -0
- nucleuskit_toolkit/tools/eeg_regression.py +91 -0
- nucleuskit_toolkit/tools/recording_merger.py +437 -0
- nucleuskit_toolkit/ui/__init__.py +1 -0
- nucleuskit_toolkit/ui/icons.py +165 -0
- nucleuskit_toolkit/ui/main_window.py +465 -0
- nucleuskit_toolkit/ui/offline_job.py +144 -0
- nucleuskit_toolkit/ui/pages/__init__.py +1 -0
- nucleuskit_toolkit/ui/pages/about_page.py +88 -0
- nucleuskit_toolkit/ui/pages/channel_fixer_page.py +205 -0
- nucleuskit_toolkit/ui/pages/channel_gain_page.py +362 -0
- nucleuskit_toolkit/ui/pages/eeg_regression_page.py +385 -0
- nucleuskit_toolkit/ui/pages/mqtt_controller_page.py +394 -0
- nucleuskit_toolkit/ui/pages/offline_page.py +754 -0
- nucleuskit_toolkit/ui/pages/playback_page.py +1524 -0
- nucleuskit_toolkit/ui/pages/ppg_fixer_page.py +199 -0
- nucleuskit_toolkit/ui/pages/realtime_viewer_page.py +954 -0
- nucleuskit_toolkit/ui/pages/recording_merge_page.py +395 -0
- nucleuskit_toolkit/ui/pages/revert_original_page.py +128 -0
- nucleuskit_toolkit/ui/pages/settings_page.py +69 -0
- nucleuskit_toolkit/ui/pages/tools_menu_page.py +42 -0
- nucleuskit_toolkit/ui/plot_style.py +155 -0
- nucleuskit_toolkit/ui/realtime_eeg_plot.py +244 -0
- nucleuskit_toolkit/ui/realtime_hermes_motion_plot.py +162 -0
- nucleuskit_toolkit/ui/realtime_nlms_config_dialog.py +101 -0
- nucleuskit_toolkit/ui/realtime_shimmer_plot.py +135 -0
- nucleuskit_toolkit/ui/resources/__init__.py +1 -0
- nucleuskit_toolkit/ui/resources/branding/__init__.py +1 -0
- nucleuskit_toolkit/ui/resources/branding/logo.png +0 -0
- nucleuskit_toolkit/ui/session_inspect.py +226 -0
- nucleuskit_toolkit/ui/theme.py +330 -0
- nucleuskit_toolkit/ui/widgets.py +190 -0
- nucleuskit_toolkit-2.2.0.dist-info/METADATA +351 -0
- nucleuskit_toolkit-2.2.0.dist-info/RECORD +59 -0
- nucleuskit_toolkit-2.2.0.dist-info/WHEEL +5 -0
- nucleuskit_toolkit-2.2.0.dist-info/entry_points.txt +2 -0
- nucleuskit_toolkit-2.2.0.dist-info/licenses/LICENSE +675 -0
- nucleuskit_toolkit-2.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""PPG Fixer: artifact rejection via squared z-score outlier detection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import queue
|
|
6
|
+
import sys
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot
|
|
10
|
+
from PySide6.QtGui import QTextCursor
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QFileDialog,
|
|
13
|
+
QGroupBox,
|
|
14
|
+
QHBoxLayout,
|
|
15
|
+
QLineEdit,
|
|
16
|
+
QMessageBox,
|
|
17
|
+
QPlainTextEdit,
|
|
18
|
+
QProgressBar,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QVBoxLayout,
|
|
21
|
+
QWidget,
|
|
22
|
+
)
|
|
23
|
+
from reak_core.tools import fix_ppg_session
|
|
24
|
+
|
|
25
|
+
from nucleuskit_toolkit.ui.offline_job import QueueTextWriter, ppg_fixer_preflight
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _PpgFixerWorker(QObject):
|
|
29
|
+
finished_ok = Signal()
|
|
30
|
+
finished_err = Signal(str)
|
|
31
|
+
|
|
32
|
+
def __init__(self, folder: str, log_queue: queue.SimpleQueue[str]) -> None:
|
|
33
|
+
super().__init__()
|
|
34
|
+
self._folder = folder
|
|
35
|
+
self._log_queue = log_queue
|
|
36
|
+
|
|
37
|
+
@Slot()
|
|
38
|
+
def run(self) -> None:
|
|
39
|
+
err: str | None = None
|
|
40
|
+
writer = QueueTextWriter(self._log_queue)
|
|
41
|
+
old_out, old_err = sys.stdout, sys.stderr
|
|
42
|
+
try:
|
|
43
|
+
sys.stdout = writer
|
|
44
|
+
sys.stderr = writer
|
|
45
|
+
fix_ppg_session(self._folder)
|
|
46
|
+
except BaseException:
|
|
47
|
+
err = traceback.format_exc()
|
|
48
|
+
self._log_queue.put(err)
|
|
49
|
+
finally:
|
|
50
|
+
sys.stdout = old_out
|
|
51
|
+
sys.stderr = old_err
|
|
52
|
+
|
|
53
|
+
if err:
|
|
54
|
+
self.finished_err.emit("Run finished with errors (see log).")
|
|
55
|
+
else:
|
|
56
|
+
self.finished_ok.emit()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PpgFixerPage(QWidget):
|
|
60
|
+
go_tools_menu = Signal()
|
|
61
|
+
processing_changed = Signal(bool)
|
|
62
|
+
|
|
63
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
64
|
+
super().__init__(parent)
|
|
65
|
+
self._log_queue: queue.SimpleQueue[str] = queue.SimpleQueue()
|
|
66
|
+
self._thread: QThread | None = None
|
|
67
|
+
self._worker: _PpgFixerWorker | None = None
|
|
68
|
+
|
|
69
|
+
# ── top bar ──────────────────────────────────────────────────
|
|
70
|
+
self._back = QPushButton("Back")
|
|
71
|
+
self._back.setProperty("secondary", True)
|
|
72
|
+
self._back.clicked.connect(self.go_tools_menu.emit)
|
|
73
|
+
|
|
74
|
+
top = QHBoxLayout()
|
|
75
|
+
top.addStretch(1)
|
|
76
|
+
top.addWidget(self._back)
|
|
77
|
+
|
|
78
|
+
# ── session folder selector ───────────────────────────────────
|
|
79
|
+
self._session = QLineEdit()
|
|
80
|
+
self._session.setPlaceholderText("Session folder path…")
|
|
81
|
+
browse = QPushButton("Browse…")
|
|
82
|
+
browse.clicked.connect(self._browse_session)
|
|
83
|
+
|
|
84
|
+
sess_row = QHBoxLayout()
|
|
85
|
+
sess_row.addWidget(self._session, 1)
|
|
86
|
+
sess_row.addWidget(browse)
|
|
87
|
+
|
|
88
|
+
session_box = QGroupBox(
|
|
89
|
+
"Session (must contain features/ppg/ppg_resampled.csv)"
|
|
90
|
+
)
|
|
91
|
+
sg = QVBoxLayout(session_box)
|
|
92
|
+
sg.addLayout(sess_row)
|
|
93
|
+
|
|
94
|
+
# ── actions ───────────────────────────────────────────────────
|
|
95
|
+
self._run = QPushButton("Run PPG Fixer")
|
|
96
|
+
self._run.clicked.connect(self._run_clicked)
|
|
97
|
+
|
|
98
|
+
self._progress = QProgressBar()
|
|
99
|
+
self._progress.setRange(0, 0)
|
|
100
|
+
self._progress.setVisible(False)
|
|
101
|
+
|
|
102
|
+
actions = QHBoxLayout()
|
|
103
|
+
actions.addWidget(self._run)
|
|
104
|
+
actions.addWidget(self._progress, 1)
|
|
105
|
+
|
|
106
|
+
# ── log ───────────────────────────────────────────────────────
|
|
107
|
+
self._log = QPlainTextEdit()
|
|
108
|
+
self._log.setReadOnly(True)
|
|
109
|
+
|
|
110
|
+
log_box = QGroupBox("Log")
|
|
111
|
+
lg = QVBoxLayout(log_box)
|
|
112
|
+
lg.addWidget(self._log)
|
|
113
|
+
|
|
114
|
+
# ── assemble ─────────────────────────────────────────────────
|
|
115
|
+
layout = QVBoxLayout(self)
|
|
116
|
+
layout.addLayout(top)
|
|
117
|
+
layout.addWidget(session_box)
|
|
118
|
+
layout.addLayout(actions)
|
|
119
|
+
layout.addWidget(log_box, 1)
|
|
120
|
+
|
|
121
|
+
self._poll_timer = QTimer(self)
|
|
122
|
+
self._poll_timer.timeout.connect(self._drain_log_queue)
|
|
123
|
+
self._poll_timer.start(120)
|
|
124
|
+
|
|
125
|
+
# ── private helpers ───────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def _browse_session(self) -> None:
|
|
128
|
+
path = QFileDialog.getExistingDirectory(self, "Select session folder")
|
|
129
|
+
if path:
|
|
130
|
+
self._session.setText(path)
|
|
131
|
+
|
|
132
|
+
def _set_processing(self, running: bool) -> None:
|
|
133
|
+
self._back.setEnabled(not running)
|
|
134
|
+
self._run.setEnabled(not running)
|
|
135
|
+
self._session.setEnabled(not running)
|
|
136
|
+
self._progress.setVisible(running)
|
|
137
|
+
self.processing_changed.emit(running)
|
|
138
|
+
|
|
139
|
+
def _run_clicked(self) -> None:
|
|
140
|
+
folder = self._session.text().strip()
|
|
141
|
+
err = ppg_fixer_preflight(folder)
|
|
142
|
+
if err:
|
|
143
|
+
QMessageBox.warning(self, "Session", err)
|
|
144
|
+
return
|
|
145
|
+
if self._thread is not None and self._thread.isRunning():
|
|
146
|
+
QMessageBox.information(self, "Busy", "A run is already in progress.")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
self._set_processing(True)
|
|
150
|
+
self._insert_log(f"\n--- Starting PPG Fixer: {folder!r} ---\n")
|
|
151
|
+
|
|
152
|
+
thread = QThread()
|
|
153
|
+
worker = _PpgFixerWorker(folder, self._log_queue)
|
|
154
|
+
worker.moveToThread(thread)
|
|
155
|
+
|
|
156
|
+
thread.started.connect(worker.run)
|
|
157
|
+
worker.finished_ok.connect(self._on_finished_ok)
|
|
158
|
+
worker.finished_err.connect(self._on_finished_err)
|
|
159
|
+
worker.finished_ok.connect(thread.quit)
|
|
160
|
+
worker.finished_err.connect(thread.quit)
|
|
161
|
+
thread.finished.connect(worker.deleteLater)
|
|
162
|
+
thread.finished.connect(thread.deleteLater)
|
|
163
|
+
thread.finished.connect(self._clear_thread_ref)
|
|
164
|
+
|
|
165
|
+
self._thread = thread
|
|
166
|
+
self._worker = worker
|
|
167
|
+
thread.start()
|
|
168
|
+
|
|
169
|
+
def _clear_thread_ref(self) -> None:
|
|
170
|
+
self._thread = None
|
|
171
|
+
self._worker = None
|
|
172
|
+
|
|
173
|
+
def _on_finished_ok(self) -> None:
|
|
174
|
+
self._set_processing(False)
|
|
175
|
+
QMessageBox.information(
|
|
176
|
+
self,
|
|
177
|
+
"PPG Fixer",
|
|
178
|
+
"Done.\n\n"
|
|
179
|
+
"Artifacts removed, features/ppg/ppg_rejected.png saved, and "
|
|
180
|
+
"results/HeartDynamics.csv recomputed.",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _on_finished_err(self, msg: str) -> None:
|
|
184
|
+
self._set_processing(False)
|
|
185
|
+
QMessageBox.critical(self, "PPG Fixer", msg)
|
|
186
|
+
|
|
187
|
+
def _insert_log(self, text: str) -> None:
|
|
188
|
+
self._log.moveCursor(QTextCursor.End)
|
|
189
|
+
self._log.insertPlainText(text)
|
|
190
|
+
sb = self._log.verticalScrollBar()
|
|
191
|
+
sb.setValue(sb.maximum())
|
|
192
|
+
|
|
193
|
+
def _drain_log_queue(self) -> None:
|
|
194
|
+
try:
|
|
195
|
+
while True:
|
|
196
|
+
chunk = self._log_queue.get_nowait()
|
|
197
|
+
self._insert_log(chunk)
|
|
198
|
+
except queue.Empty:
|
|
199
|
+
pass
|