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,385 @@
|
|
|
1
|
+
"""EEG Regression Denoising tool page.
|
|
2
|
+
|
|
3
|
+
Applies EMG reference-channel regression to remove shared muscle noise from EEG
|
|
4
|
+
before recomputing all cognition outputs for a session. The standard pipeline is
|
|
5
|
+
not modified; this tool clears the existing cognition cache and regenerates it.
|
|
6
|
+
|
|
7
|
+
Reference channels used as regressors (CHEEK_R, CHEEK_L, BROW_L, NOSE) are
|
|
8
|
+
recorded by the Hermes hardware but are not used in the normal EEG derivation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import queue
|
|
15
|
+
import shutil
|
|
16
|
+
import sys
|
|
17
|
+
import traceback
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import pandas as pd
|
|
21
|
+
from PySide6.QtCore import QObject, QThread, QTimer, Signal, Slot
|
|
22
|
+
from PySide6.QtGui import QTextCursor
|
|
23
|
+
from PySide6.QtWidgets import (
|
|
24
|
+
QFileDialog,
|
|
25
|
+
QGroupBox,
|
|
26
|
+
QHBoxLayout,
|
|
27
|
+
QLineEdit,
|
|
28
|
+
QMessageBox,
|
|
29
|
+
QPlainTextEdit,
|
|
30
|
+
QProgressBar,
|
|
31
|
+
QPushButton,
|
|
32
|
+
QVBoxLayout,
|
|
33
|
+
QWidget,
|
|
34
|
+
)
|
|
35
|
+
from reak_core.constants import EXG_SAMPLING_RATE
|
|
36
|
+
from reak_core.eeg.cognition import (
|
|
37
|
+
GAP_THRESHOLD_S,
|
|
38
|
+
bandpass_filter,
|
|
39
|
+
compute_cognitive_indexes,
|
|
40
|
+
compute_eeg_power_bands,
|
|
41
|
+
reject_temporal_artefacts,
|
|
42
|
+
save_eeg_artefact_plot,
|
|
43
|
+
save_filtered_eeg_csv,
|
|
44
|
+
)
|
|
45
|
+
from reak_core.io.exg import derive_eeg_channels, load_exg, mark_invalid
|
|
46
|
+
from reak_core.sync.resample import resample_to_timebase
|
|
47
|
+
|
|
48
|
+
from nucleuskit_toolkit.logging_utils import printError, printInfo, printWarning
|
|
49
|
+
from nucleuskit_toolkit.tools.eeg_regression import reference_channels, regress_noise_channels
|
|
50
|
+
from nucleuskit_toolkit.ui.offline_job import QueueTextWriter, eeg_regression_preflight
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Background worker
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
class _EegRegressionWorker(QObject):
|
|
57
|
+
finished_ok = Signal()
|
|
58
|
+
finished_err = Signal(str)
|
|
59
|
+
|
|
60
|
+
def __init__(self, folder: str, log_queue: queue.SimpleQueue[str]) -> None:
|
|
61
|
+
super().__init__()
|
|
62
|
+
self._folder = folder
|
|
63
|
+
self._log_queue = log_queue
|
|
64
|
+
|
|
65
|
+
@Slot()
|
|
66
|
+
def run(self) -> None:
|
|
67
|
+
err: str | None = None
|
|
68
|
+
writer = QueueTextWriter(self._log_queue)
|
|
69
|
+
old_out, old_err = sys.stdout, sys.stderr
|
|
70
|
+
try:
|
|
71
|
+
sys.stdout = writer
|
|
72
|
+
sys.stderr = writer
|
|
73
|
+
self._process()
|
|
74
|
+
except BaseException:
|
|
75
|
+
err = traceback.format_exc()
|
|
76
|
+
self._log_queue.put(err)
|
|
77
|
+
finally:
|
|
78
|
+
sys.stdout = old_out
|
|
79
|
+
sys.stderr = old_err
|
|
80
|
+
|
|
81
|
+
if err:
|
|
82
|
+
self.finished_err.emit("Run finished with errors (see log).")
|
|
83
|
+
else:
|
|
84
|
+
self.finished_ok.emit()
|
|
85
|
+
|
|
86
|
+
def _process(self) -> None:
|
|
87
|
+
recpath = self._folder
|
|
88
|
+
sf = EXG_SAMPLING_RATE
|
|
89
|
+
features_dir = os.path.join(recpath, "features", "cognition")
|
|
90
|
+
cognitive_path = os.path.join(recpath, "results", "Cognition.csv")
|
|
91
|
+
powerbands_path = os.path.join(features_dir, "powerBands.csv")
|
|
92
|
+
artefact_path = os.path.join(features_dir, "artefactStats.csv")
|
|
93
|
+
temporal_bp_path = os.path.join(features_dir, "temporalBandPowers.csv")
|
|
94
|
+
filtered_eeg_path = os.path.join(features_dir, "filteredEEG.csv")
|
|
95
|
+
artefact_plot_path = os.path.join(features_dir, "eegArtefactPlot.png")
|
|
96
|
+
|
|
97
|
+
# -- Clear existing cognition cache so outputs are fully regenerated --
|
|
98
|
+
printInfo("[eegRegressionTool] Clearing existing cognition outputs")
|
|
99
|
+
if os.path.isfile(cognitive_path):
|
|
100
|
+
os.remove(cognitive_path)
|
|
101
|
+
if os.path.isdir(features_dir):
|
|
102
|
+
shutil.rmtree(features_dir)
|
|
103
|
+
|
|
104
|
+
# -- Load EEG ------------------------------------------------------------
|
|
105
|
+
printInfo("[eegRegressionTool] Loading EEG data")
|
|
106
|
+
recording = load_exg(recpath)
|
|
107
|
+
if recording is None or recording.data.shape[1] < 5:
|
|
108
|
+
printError("[eegRegressionTool] No usable EXG data — aborting")
|
|
109
|
+
raise RuntimeError("No EEG data available.")
|
|
110
|
+
# Invalid raw samples (non-finite or full scale) become NaN, as in re-ak-core's cognition.
|
|
111
|
+
raw = mark_invalid(recording.data)
|
|
112
|
+
original_timestamps = recording.timestamps
|
|
113
|
+
eeg_data = derive_eeg_channels(raw)
|
|
114
|
+
|
|
115
|
+
printInfo(f"[eegRegressionTool] EEG loaded: {len(eeg_data)} samples, "
|
|
116
|
+
f"columns: {list(eeg_data.columns)}")
|
|
117
|
+
|
|
118
|
+
# -- Timestamp gap detection + hardware invalid mask ---------------------
|
|
119
|
+
if original_timestamps is None:
|
|
120
|
+
printWarning(
|
|
121
|
+
"[eegRegressionTool] Hardware timestamps unavailable — "
|
|
122
|
+
"falling back to synthetic timestamps. Gap detection disabled."
|
|
123
|
+
)
|
|
124
|
+
original_timestamps = np.arange(len(eeg_data), dtype=float) / sf
|
|
125
|
+
gap_sample_mask = np.zeros(len(eeg_data), dtype=bool)
|
|
126
|
+
else:
|
|
127
|
+
dt = np.diff(original_timestamps)
|
|
128
|
+
gap_indices = np.where(dt >= GAP_THRESHOLD_S)[0]
|
|
129
|
+
gap_sample_mask = np.zeros(len(original_timestamps), dtype=bool)
|
|
130
|
+
if len(gap_indices):
|
|
131
|
+
printWarning(
|
|
132
|
+
f"[eegRegressionTool] {len(gap_indices)} hardware timestamp gap(s) "
|
|
133
|
+
f">= {GAP_THRESHOLD_S:.0f} s detected — affected windows will be NaN."
|
|
134
|
+
)
|
|
135
|
+
gap_sample_mask[gap_indices + 1] = True
|
|
136
|
+
|
|
137
|
+
hardware_invalid = eeg_data.isna().any(axis=1).to_numpy() | gap_sample_mask
|
|
138
|
+
|
|
139
|
+
# -- Bandpass filter EEG -------------------------------------------------
|
|
140
|
+
printInfo("[eegRegressionTool] Bandpass filtering EEG")
|
|
141
|
+
filtered_eeg = bandpass_filter(eeg_data, sf)
|
|
142
|
+
|
|
143
|
+
# -- Load + filter reference channels ------------------------------------
|
|
144
|
+
printInfo("[eegRegressionTool] Loading EMG reference channels")
|
|
145
|
+
ref_data = reference_channels(raw)
|
|
146
|
+
|
|
147
|
+
if ref_data is not None:
|
|
148
|
+
printInfo("[eegRegressionTool] Bandpass filtering reference channels")
|
|
149
|
+
filtered_refs = bandpass_filter(ref_data, sf)
|
|
150
|
+
|
|
151
|
+
# First pass: denoise all EEG channels with the EMG reference set
|
|
152
|
+
# (CHEEK_R, CHEEK_L, BROW_L, NOSE).
|
|
153
|
+
printInfo("[eegRegressionTool] Applying EMG regression denoising (all channels)")
|
|
154
|
+
filtered_eeg = regress_noise_channels(filtered_eeg, filtered_refs)
|
|
155
|
+
|
|
156
|
+
# Second pass: denoise T9/T10 with an extended reference set that
|
|
157
|
+
# additionally includes AF7 and AF8. These frontal channels capture
|
|
158
|
+
# EOG (eye blinks, lateral movements) and frontal movement artefacts
|
|
159
|
+
# that contaminate the temporal electrodes via the shared EAR_R
|
|
160
|
+
# midpoint reference. Only T9/T10 are targeted to avoid regressing
|
|
161
|
+
# frontal brain activity out of the frontal channels themselves.
|
|
162
|
+
if {'AF7', 'AF8', 'T9', 'T10'}.issubset(filtered_eeg.columns):
|
|
163
|
+
printInfo(
|
|
164
|
+
"[eegRegressionTool] Applying extended regression on T9/T10 "
|
|
165
|
+
"using AF7+AF8 as additional EOG/movement references"
|
|
166
|
+
)
|
|
167
|
+
extended_refs = pd.concat(
|
|
168
|
+
[filtered_refs, filtered_eeg[['AF7', 'AF8']]],
|
|
169
|
+
axis=1,
|
|
170
|
+
)
|
|
171
|
+
cleaned_temporal = regress_noise_channels(
|
|
172
|
+
filtered_eeg[['T9', 'T10']],
|
|
173
|
+
extended_refs,
|
|
174
|
+
)
|
|
175
|
+
filtered_eeg = filtered_eeg.copy()
|
|
176
|
+
filtered_eeg[['T9', 'T10']] = cleaned_temporal[['T9', 'T10']]
|
|
177
|
+
else:
|
|
178
|
+
printWarning(
|
|
179
|
+
"[eegRegressionTool] AF7/AF8 or T9/T10 missing from filtered EEG "
|
|
180
|
+
"— skipping extended T9/T10 regression"
|
|
181
|
+
)
|
|
182
|
+
else:
|
|
183
|
+
printWarning(
|
|
184
|
+
"[eegRegressionTool] Reference channels unavailable — "
|
|
185
|
+
"proceeding without regression denoising"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# -- Artefact rejection --------------------------------------------------
|
|
189
|
+
printInfo("[eegRegressionTool] Running temporal artefact rejection")
|
|
190
|
+
artefact_mask, artefact_stats, epoch_metrics = reject_temporal_artefacts(
|
|
191
|
+
filtered_eeg, hardware_invalid, sf
|
|
192
|
+
)
|
|
193
|
+
combined_invalid = hardware_invalid | artefact_mask
|
|
194
|
+
|
|
195
|
+
# -- Save filtered EEG + artefact plot -----------------------------------
|
|
196
|
+
os.makedirs(features_dir, exist_ok=True)
|
|
197
|
+
save_filtered_eeg_csv(filtered_eeg, original_timestamps, filtered_eeg_path)
|
|
198
|
+
save_eeg_artefact_plot(filtered_eeg, original_timestamps, combined_invalid, artefact_plot_path)
|
|
199
|
+
|
|
200
|
+
if artefact_stats is not None:
|
|
201
|
+
artefact_stats.to_csv(artefact_path, index=False)
|
|
202
|
+
printInfo(f"[eegRegressionTool] Artefact stats saved to {artefact_path}")
|
|
203
|
+
|
|
204
|
+
epoch_metrics_path = os.path.join(features_dir, "epochMetrics.csv")
|
|
205
|
+
if epoch_metrics is not None:
|
|
206
|
+
epoch_metrics.to_csv(epoch_metrics_path, index=False)
|
|
207
|
+
printInfo(f"[eegRegressionTool] Epoch metrics saved to {epoch_metrics_path}")
|
|
208
|
+
|
|
209
|
+
# -- Power bands ---------------------------------------------------------
|
|
210
|
+
printInfo("[eegRegressionTool] Computing power bands")
|
|
211
|
+
powerbands = compute_eeg_power_bands(
|
|
212
|
+
filtered_eeg[['T9', 'T10']],
|
|
213
|
+
timestamps=original_timestamps,
|
|
214
|
+
hardware_invalid=combined_invalid,
|
|
215
|
+
)
|
|
216
|
+
if powerbands is None:
|
|
217
|
+
raise RuntimeError("compute_eeg_power_bands returned None — recording may be too short.")
|
|
218
|
+
|
|
219
|
+
powerbands.to_csv(powerbands_path, index=False)
|
|
220
|
+
printInfo(f"[eegRegressionTool] Power bands saved to {powerbands_path}")
|
|
221
|
+
|
|
222
|
+
# -- Cognitive indexes ---------------------------------------------------
|
|
223
|
+
printInfo("[eegRegressionTool] Computing cognitive indexes")
|
|
224
|
+
result, temporal_bands = compute_cognitive_indexes(powerbands)
|
|
225
|
+
if result is None:
|
|
226
|
+
raise RuntimeError("compute_cognitive_indexes returned None.")
|
|
227
|
+
|
|
228
|
+
if temporal_bands is not None:
|
|
229
|
+
temporal_bands.to_csv(temporal_bp_path, index=False)
|
|
230
|
+
printInfo(f"[eegRegressionTool] Temporal band powers saved to {temporal_bp_path}")
|
|
231
|
+
|
|
232
|
+
result = resample_to_timebase(result, target_interval=0.5)
|
|
233
|
+
|
|
234
|
+
os.makedirs(os.path.join(recpath, "results"), exist_ok=True)
|
|
235
|
+
result.to_csv(cognitive_path, index=False)
|
|
236
|
+
printInfo(f"[eegRegressionTool] Cognition results saved to {cognitive_path}")
|
|
237
|
+
|
|
238
|
+
printInfo("[eegRegressionTool] Done.")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ---------------------------------------------------------------------------
|
|
242
|
+
# UI page
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
class EegRegressionPage(QWidget):
|
|
246
|
+
go_tools_menu = Signal()
|
|
247
|
+
processing_changed = Signal(bool)
|
|
248
|
+
|
|
249
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
250
|
+
super().__init__(parent)
|
|
251
|
+
self._log_queue: queue.SimpleQueue[str] = queue.SimpleQueue()
|
|
252
|
+
self._thread: QThread | None = None
|
|
253
|
+
self._worker: _EegRegressionWorker | None = None
|
|
254
|
+
|
|
255
|
+
# ── top bar ──────────────────────────────────────────────────
|
|
256
|
+
self._back = QPushButton("Back")
|
|
257
|
+
self._back.setProperty("secondary", True)
|
|
258
|
+
self._back.clicked.connect(self.go_tools_menu.emit)
|
|
259
|
+
|
|
260
|
+
top = QHBoxLayout()
|
|
261
|
+
top.addStretch(1)
|
|
262
|
+
top.addWidget(self._back)
|
|
263
|
+
|
|
264
|
+
# ── session folder selector ───────────────────────────────────
|
|
265
|
+
self._session = QLineEdit()
|
|
266
|
+
self._session.setPlaceholderText("Session folder path…")
|
|
267
|
+
browse = QPushButton("Browse…")
|
|
268
|
+
browse.clicked.connect(self._browse_session)
|
|
269
|
+
|
|
270
|
+
sess_row = QHBoxLayout()
|
|
271
|
+
sess_row.addWidget(self._session, 1)
|
|
272
|
+
sess_row.addWidget(browse)
|
|
273
|
+
|
|
274
|
+
session_box = QGroupBox(
|
|
275
|
+
"Session (must contain rawData/ with a raw EEG file)"
|
|
276
|
+
)
|
|
277
|
+
sg = QVBoxLayout(session_box)
|
|
278
|
+
sg.addLayout(sess_row)
|
|
279
|
+
|
|
280
|
+
# ── actions ───────────────────────────────────────────────────
|
|
281
|
+
self._run = QPushButton("Run EEG Regression Denoising")
|
|
282
|
+
self._run.clicked.connect(self._run_clicked)
|
|
283
|
+
|
|
284
|
+
self._progress = QProgressBar()
|
|
285
|
+
self._progress.setRange(0, 0)
|
|
286
|
+
self._progress.setVisible(False)
|
|
287
|
+
|
|
288
|
+
actions = QHBoxLayout()
|
|
289
|
+
actions.addWidget(self._run)
|
|
290
|
+
actions.addWidget(self._progress, 1)
|
|
291
|
+
|
|
292
|
+
# ── log ───────────────────────────────────────────────────────
|
|
293
|
+
self._log = QPlainTextEdit()
|
|
294
|
+
self._log.setReadOnly(True)
|
|
295
|
+
|
|
296
|
+
log_box = QGroupBox("Log")
|
|
297
|
+
lg = QVBoxLayout(log_box)
|
|
298
|
+
lg.addWidget(self._log)
|
|
299
|
+
|
|
300
|
+
# ── assemble ─────────────────────────────────────────────────
|
|
301
|
+
layout = QVBoxLayout(self)
|
|
302
|
+
layout.addLayout(top)
|
|
303
|
+
layout.addWidget(session_box)
|
|
304
|
+
layout.addLayout(actions)
|
|
305
|
+
layout.addWidget(log_box, 1)
|
|
306
|
+
|
|
307
|
+
self._poll_timer = QTimer(self)
|
|
308
|
+
self._poll_timer.timeout.connect(self._drain_log_queue)
|
|
309
|
+
self._poll_timer.start(120)
|
|
310
|
+
|
|
311
|
+
# ── private helpers ───────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
def _browse_session(self) -> None:
|
|
314
|
+
path = QFileDialog.getExistingDirectory(self, "Select session folder")
|
|
315
|
+
if path:
|
|
316
|
+
self._session.setText(path)
|
|
317
|
+
|
|
318
|
+
def _set_processing(self, running: bool) -> None:
|
|
319
|
+
self._back.setEnabled(not running)
|
|
320
|
+
self._run.setEnabled(not running)
|
|
321
|
+
self._session.setEnabled(not running)
|
|
322
|
+
self._progress.setVisible(running)
|
|
323
|
+
self.processing_changed.emit(running)
|
|
324
|
+
|
|
325
|
+
def _run_clicked(self) -> None:
|
|
326
|
+
folder = self._session.text().strip()
|
|
327
|
+
err = eeg_regression_preflight(folder)
|
|
328
|
+
if err:
|
|
329
|
+
QMessageBox.warning(self, "Session", err)
|
|
330
|
+
return
|
|
331
|
+
if self._thread is not None and self._thread.isRunning():
|
|
332
|
+
QMessageBox.information(self, "Busy", "A run is already in progress.")
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
self._set_processing(True)
|
|
336
|
+
self._insert_log(f"\n--- Starting EEG Regression Denoising: {folder!r} ---\n")
|
|
337
|
+
|
|
338
|
+
thread = QThread()
|
|
339
|
+
worker = _EegRegressionWorker(folder, self._log_queue)
|
|
340
|
+
worker.moveToThread(thread)
|
|
341
|
+
|
|
342
|
+
thread.started.connect(worker.run)
|
|
343
|
+
worker.finished_ok.connect(self._on_finished_ok)
|
|
344
|
+
worker.finished_err.connect(self._on_finished_err)
|
|
345
|
+
worker.finished_ok.connect(thread.quit)
|
|
346
|
+
worker.finished_err.connect(thread.quit)
|
|
347
|
+
thread.finished.connect(worker.deleteLater)
|
|
348
|
+
thread.finished.connect(thread.deleteLater)
|
|
349
|
+
thread.finished.connect(self._clear_thread_ref)
|
|
350
|
+
|
|
351
|
+
self._thread = thread
|
|
352
|
+
self._worker = worker
|
|
353
|
+
thread.start()
|
|
354
|
+
|
|
355
|
+
def _clear_thread_ref(self) -> None:
|
|
356
|
+
self._thread = None
|
|
357
|
+
self._worker = None
|
|
358
|
+
|
|
359
|
+
def _on_finished_ok(self) -> None:
|
|
360
|
+
self._set_processing(False)
|
|
361
|
+
QMessageBox.information(
|
|
362
|
+
self,
|
|
363
|
+
"EEG Regression Denoising",
|
|
364
|
+
"Done.\n\n"
|
|
365
|
+
"Cognition outputs regenerated with EMG regression applied.\n"
|
|
366
|
+
"results/Cognition.csv and features/cognition/ have been updated.",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def _on_finished_err(self, msg: str) -> None:
|
|
370
|
+
self._set_processing(False)
|
|
371
|
+
QMessageBox.critical(self, "EEG Regression Denoising", msg)
|
|
372
|
+
|
|
373
|
+
def _insert_log(self, text: str) -> None:
|
|
374
|
+
self._log.moveCursor(QTextCursor.End)
|
|
375
|
+
self._log.insertPlainText(text)
|
|
376
|
+
sb = self._log.verticalScrollBar()
|
|
377
|
+
sb.setValue(sb.maximum())
|
|
378
|
+
|
|
379
|
+
def _drain_log_queue(self) -> None:
|
|
380
|
+
try:
|
|
381
|
+
while True:
|
|
382
|
+
chunk = self._log_queue.get_nowait()
|
|
383
|
+
self._insert_log(chunk)
|
|
384
|
+
except queue.Empty:
|
|
385
|
+
pass
|