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,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shimmer3 LogAndStream client over Bluetooth Classic.
|
|
3
|
+
|
|
4
|
+
Supports two connection modes:
|
|
5
|
+
- COM port (e.g. "COM7") — uses pyserial, classic approach.
|
|
6
|
+
- MAC address (e.g. "00:06:66:B7:00:00") — opens a direct Bluetooth RFCOMM
|
|
7
|
+
socket, bypassing COM-port selection entirely. This is more reliable on
|
|
8
|
+
Windows where two "Standard Serial over Bluetooth link" ports are created
|
|
9
|
+
per device (incoming / outgoing) and only one can be opened by applications.
|
|
10
|
+
|
|
11
|
+
Configures streaming at ~51.2 Hz and invokes a callback with each decoded row:
|
|
12
|
+
[epoch_ms, imu_x, imu_y, imu_z, eda_kohm, ppg_mv] (host epoch milliseconds).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
import re
|
|
19
|
+
import struct
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from time import sleep, time
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from nucleuskit_toolkit.shimmer.constants import ShimmerConstants
|
|
27
|
+
|
|
28
|
+
ShimmerSampleCallback = Callable[[list[float]], None]
|
|
29
|
+
|
|
30
|
+
ACK_BYTE = b"\xff"
|
|
31
|
+
FRAME_SIZE = 14 # 1 type + 3 ts + 5x uint16 payload (accel xyz, PPG, GSR packed)
|
|
32
|
+
|
|
33
|
+
_MAC_RE = re.compile(r"^([0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}$")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _BtSocketAdapter:
|
|
37
|
+
"""Wraps a Bluetooth RFCOMM socket with a serial-like read/write interface.
|
|
38
|
+
|
|
39
|
+
``is_open`` starts True and is set to False on ``close()``. ``read(n)``
|
|
40
|
+
accumulates bytes until *n* bytes arrive or the per-call socket timeout
|
|
41
|
+
elapses, matching the behaviour of ``serial.Serial.read(n, timeout=…)``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, sock: Any) -> None:
|
|
45
|
+
self._sock = sock
|
|
46
|
+
self.is_open = True
|
|
47
|
+
|
|
48
|
+
def reset_input_buffer(self) -> None:
|
|
49
|
+
"""Discard any data already in the receive buffer."""
|
|
50
|
+
self._sock.settimeout(0.05)
|
|
51
|
+
try:
|
|
52
|
+
while True:
|
|
53
|
+
data = self._sock.recv(4096)
|
|
54
|
+
if not data:
|
|
55
|
+
break
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
self._sock.settimeout(0.2)
|
|
59
|
+
|
|
60
|
+
def read(self, n: int) -> bytes:
|
|
61
|
+
"""Return up to *n* bytes; returns fewer (or b'') on timeout / error."""
|
|
62
|
+
result = b""
|
|
63
|
+
while len(result) < n:
|
|
64
|
+
try:
|
|
65
|
+
chunk = self._sock.recv(n - len(result))
|
|
66
|
+
if not chunk:
|
|
67
|
+
break
|
|
68
|
+
result += chunk
|
|
69
|
+
except OSError:
|
|
70
|
+
break
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
def write(self, data: bytes) -> None:
|
|
74
|
+
self._sock.sendall(data)
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
self.is_open = False
|
|
78
|
+
try:
|
|
79
|
+
self._sock.close()
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ShimmerSerialProxy:
|
|
85
|
+
"""Background thread: connect to Shimmer3, send setup, stream packets until disconnect.
|
|
86
|
+
|
|
87
|
+
``port_or_mac`` may be:
|
|
88
|
+
|
|
89
|
+
* A serial port name — ``"COM7"``, ``"/dev/rfcomm0"`` etc.
|
|
90
|
+
* A Bluetooth MAC address — ``"00:06:66:B7:00:00"`` (colons or hyphens).
|
|
91
|
+
A direct RFCOMM socket is opened; no virtual COM port is needed.
|
|
92
|
+
|
|
93
|
+
Pass the result of :meth:`is_mac_address` to determine which mode will be
|
|
94
|
+
used before constructing the proxy.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
def __init__(self, port_or_mac: str, sample_callback: ShimmerSampleCallback) -> None:
|
|
98
|
+
self._target = port_or_mac
|
|
99
|
+
self._sample_callback = sample_callback
|
|
100
|
+
self.is_connected = False
|
|
101
|
+
self._conn: Any = None # serial.Serial or _BtSocketAdapter
|
|
102
|
+
self._stop = threading.Event()
|
|
103
|
+
self._connection_error: str | None = None
|
|
104
|
+
self._worker = threading.Thread(target=self._run, name="ShimmerSerial", daemon=True)
|
|
105
|
+
self._worker.start()
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------
|
|
108
|
+
# Public helpers
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def is_mac_address(s: str) -> bool:
|
|
113
|
+
"""Return True if *s* looks like a Bluetooth MAC address."""
|
|
114
|
+
return bool(_MAC_RE.match(s.strip()))
|
|
115
|
+
|
|
116
|
+
def wait_until_connected(self, timeout: float = 90.0) -> None:
|
|
117
|
+
deadline = time() + timeout
|
|
118
|
+
while time() < deadline:
|
|
119
|
+
if self.is_connected:
|
|
120
|
+
return
|
|
121
|
+
if self._connection_error is not None:
|
|
122
|
+
raise RuntimeError(self._connection_error)
|
|
123
|
+
sleep(0.05)
|
|
124
|
+
raise TimeoutError("Shimmer connection timed out")
|
|
125
|
+
|
|
126
|
+
def disconnect(self) -> None:
|
|
127
|
+
self._stop.set()
|
|
128
|
+
conn = self._conn
|
|
129
|
+
if conn is not None and conn.is_open:
|
|
130
|
+
try:
|
|
131
|
+
conn.write(struct.pack("B", 0x20))
|
|
132
|
+
except OSError:
|
|
133
|
+
pass
|
|
134
|
+
try:
|
|
135
|
+
conn.close()
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
138
|
+
self._worker.join(timeout=5.0)
|
|
139
|
+
self.is_connected = False
|
|
140
|
+
self._conn = None
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
# Internal: connection
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _open_bt_socket(mac: str) -> _BtSocketAdapter:
|
|
148
|
+
"""Open a direct Bluetooth RFCOMM socket to *mac* (channel 1)."""
|
|
149
|
+
import socket as _socket
|
|
150
|
+
|
|
151
|
+
mac = mac.upper().replace("-", ":")
|
|
152
|
+
|
|
153
|
+
if sys.platform == "win32":
|
|
154
|
+
# AF_BTH = 32, BTPROTO_RFCOMM = 3 (Winsock2 constants)
|
|
155
|
+
AF_BTH = getattr(_socket, "AF_BTH", 32)
|
|
156
|
+
BTPROTO_RFCOMM = getattr(_socket, "BTPROTO_RFCOMM", 3)
|
|
157
|
+
sock = _socket.socket(AF_BTH, _socket.SOCK_STREAM, BTPROTO_RFCOMM)
|
|
158
|
+
else:
|
|
159
|
+
sock = _socket.socket(
|
|
160
|
+
_socket.AF_BLUETOOTH, _socket.SOCK_STREAM, _socket.BTPROTO_RFCOMM
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
sock.settimeout(12.0)
|
|
164
|
+
sock.connect((mac, 1)) # RFCOMM channel 1
|
|
165
|
+
sock.settimeout(0.2)
|
|
166
|
+
return _BtSocketAdapter(sock)
|
|
167
|
+
|
|
168
|
+
# ------------------------------------------------------------------
|
|
169
|
+
# Internal: Shimmer protocol
|
|
170
|
+
# ------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
def _wait_for_ack(self, conn: Any, timeout_s: float = 8.0) -> None:
|
|
173
|
+
deadline = time() + timeout_s
|
|
174
|
+
while time() < deadline:
|
|
175
|
+
if self._stop.is_set():
|
|
176
|
+
raise InterruptedError("Shimmer disconnect requested")
|
|
177
|
+
b = conn.read(1)
|
|
178
|
+
if b == ACK_BYTE:
|
|
179
|
+
return
|
|
180
|
+
raise TimeoutError("Timed out waiting for Shimmer ACK (0xFF)")
|
|
181
|
+
|
|
182
|
+
def _configure_streaming(self, conn: Any) -> None:
|
|
183
|
+
# SET_SENSORS: accel + GSR + PPG (same bitmask as legacy stack)
|
|
184
|
+
conn.write(struct.pack("BBBB", 0x08, 0x84, 0x01, 0x00))
|
|
185
|
+
self._wait_for_ack(conn)
|
|
186
|
+
|
|
187
|
+
conn.write(struct.pack("BB", 0x5E, 0x01))
|
|
188
|
+
self._wait_for_ack(conn)
|
|
189
|
+
|
|
190
|
+
conn.write(struct.pack("B", 0x20))
|
|
191
|
+
self._wait_for_ack(conn)
|
|
192
|
+
|
|
193
|
+
clock_wait = int(math.ceil((2 << 14) / ShimmerConstants.SAMPLING_RATE))
|
|
194
|
+
conn.write(struct.pack("<BH", 0x05, clock_wait))
|
|
195
|
+
self._wait_for_ack(conn)
|
|
196
|
+
|
|
197
|
+
conn.write(struct.pack("B", 0x01))
|
|
198
|
+
self._wait_for_ack(conn)
|
|
199
|
+
|
|
200
|
+
response_size = 9
|
|
201
|
+
inquiry = conn.read(response_size)
|
|
202
|
+
if len(inquiry) < response_size:
|
|
203
|
+
raise TimeoutError("Shimmer inquiry response too short")
|
|
204
|
+
|
|
205
|
+
num_channels = inquiry[7]
|
|
206
|
+
channels = conn.read(num_channels)
|
|
207
|
+
if len(channels) < num_channels:
|
|
208
|
+
raise TimeoutError("Shimmer channel map too short")
|
|
209
|
+
|
|
210
|
+
conn.write(struct.pack("B", 0x07))
|
|
211
|
+
self._wait_for_ack(conn)
|
|
212
|
+
|
|
213
|
+
def _decode_frame(self, data: bytes) -> list[float] | None:
|
|
214
|
+
if len(data) != FRAME_SIZE:
|
|
215
|
+
return None
|
|
216
|
+
# Native uint16 order matches legacy Shimmer streaming clients on Windows/Linux.
|
|
217
|
+
x, y, z, ppg_raw, gsr_raw = struct.unpack("HHHHH", data[4:FRAME_SIZE])
|
|
218
|
+
|
|
219
|
+
data_range = (gsr_raw >> 14) & 0x03
|
|
220
|
+
rf = (40.2, 287.0, 1000.0, 3300.0)[data_range]
|
|
221
|
+
gsr_to_volts = (gsr_raw & 0x3FFF) * (3.0 / 4095.0)
|
|
222
|
+
den = (gsr_to_volts / 0.5) - 1.0
|
|
223
|
+
if abs(den) < 1e-9:
|
|
224
|
+
gsr_kohm = float("nan")
|
|
225
|
+
else:
|
|
226
|
+
gsr_kohm = rf / den
|
|
227
|
+
|
|
228
|
+
ppg_mv = ppg_raw * (3000.0 / 4095.0)
|
|
229
|
+
epoch_ms = round(time() * 1000.0)
|
|
230
|
+
return [float(epoch_ms), float(x), float(y), float(z), float(gsr_kohm), float(ppg_mv)]
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
# Worker thread
|
|
234
|
+
# ------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
def _run(self) -> None:
|
|
237
|
+
try:
|
|
238
|
+
target = self._target.strip()
|
|
239
|
+
if self.is_mac_address(target):
|
|
240
|
+
conn: Any = self._open_bt_socket(target)
|
|
241
|
+
else:
|
|
242
|
+
import serial
|
|
243
|
+
conn = serial.Serial(target, baudrate=115200, timeout=0.2)
|
|
244
|
+
|
|
245
|
+
self._conn = conn
|
|
246
|
+
conn.reset_input_buffer()
|
|
247
|
+
self._configure_streaming(conn)
|
|
248
|
+
self.is_connected = True
|
|
249
|
+
|
|
250
|
+
buf = b""
|
|
251
|
+
while not self._stop.is_set():
|
|
252
|
+
chunk = conn.read(FRAME_SIZE)
|
|
253
|
+
if not chunk:
|
|
254
|
+
continue
|
|
255
|
+
buf += chunk
|
|
256
|
+
while len(buf) >= FRAME_SIZE and not self._stop.is_set():
|
|
257
|
+
frame = buf[:FRAME_SIZE]
|
|
258
|
+
buf = buf[FRAME_SIZE:]
|
|
259
|
+
row = self._decode_frame(frame)
|
|
260
|
+
if row is not None:
|
|
261
|
+
self._sample_callback(row)
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
self._connection_error = str(e)
|
|
265
|
+
finally:
|
|
266
|
+
self.is_connected = False
|
|
267
|
+
conn = self._conn
|
|
268
|
+
if conn is not None:
|
|
269
|
+
if conn.is_open:
|
|
270
|
+
try:
|
|
271
|
+
conn.write(struct.pack("B", 0x20))
|
|
272
|
+
self._wait_for_ack(conn, timeout_s=2.0)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
try:
|
|
276
|
+
conn.close()
|
|
277
|
+
except OSError:
|
|
278
|
+
pass
|
|
279
|
+
self._conn = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Standalone processing tools (non-pipeline utilities)."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EMG-regression variant of the cognition pipeline (Tools > EEG regression page).
|
|
3
|
+
|
|
4
|
+
Exploratory Nucleus-Kit tool, not part of re-ak-core: it removes the EMG noise shared with the
|
|
5
|
+
reference channels (CHEEK_R, CHEEK_L, BROW_L, NOSE) from the EEG by ridge regression before
|
|
6
|
+
re-ak-core's cognition steps (filtering, artefact rejection, power bands, Engagement).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from reak_core.io.exg import CHANNEL_NAMES
|
|
14
|
+
from sklearn.linear_model import Ridge
|
|
15
|
+
|
|
16
|
+
from nucleuskit_toolkit.logging_utils import printInfo, printWarning
|
|
17
|
+
|
|
18
|
+
REFERENCE_CHANNELS = ("CHEEK_R", "CHEEK_L", "BROW_L", "NOSE")
|
|
19
|
+
"""EMG reference channels. AFz is left out: it is an EEG channel and would remove brain signal."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def reference_channels(exg: np.ndarray) -> pd.DataFrame | None:
|
|
23
|
+
"""The reference channels of a marked EXG matrix (hardware order, invalid samples already NaN)."""
|
|
24
|
+
if exg is None or exg.ndim != 2 or exg.shape[1] < len(CHANNEL_NAMES):
|
|
25
|
+
printWarning(f"[eegRegressionTool] Expected {len(CHANNEL_NAMES)} EXG channels to extract the references")
|
|
26
|
+
return None
|
|
27
|
+
return pd.DataFrame({name: exg[:, CHANNEL_NAMES.index(name)] for name in REFERENCE_CHANNELS})
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def regress_noise_channels(eeg_df, ref_df):
|
|
31
|
+
"""
|
|
32
|
+
Remove shared EMG noise from EEG channels via ridge regression.
|
|
33
|
+
|
|
34
|
+
For each EEG channel, a Ridge regressor is fitted against all reference
|
|
35
|
+
channels using only the samples where both the target and every reference
|
|
36
|
+
are finite (non-NaN). The fitted model's prediction is then subtracted
|
|
37
|
+
from the full signal; samples that are NaN in the reference matrix are
|
|
38
|
+
replaced with zeros before prediction (those samples are already flagged
|
|
39
|
+
by ``hardware_invalid`` and will be excluded downstream).
|
|
40
|
+
|
|
41
|
+
Reference channels are expected to be filtered with the same bandpass as
|
|
42
|
+
``eeg_df`` so that the regression operates on the same frequency content.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
eeg_df: Bandpass-filtered EEG DataFrame (samples × EEG channels).
|
|
46
|
+
ref_df: Bandpass-filtered reference DataFrame (samples × reference
|
|
47
|
+
channels, e.g. CHEEK_R, CHEEK_L, BROW_L, NOSE).
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Cleaned EEG DataFrame of the same shape as ``eeg_df``.
|
|
51
|
+
"""
|
|
52
|
+
printInfo("[cognitionProcessor] Applying regression-based noise removal")
|
|
53
|
+
|
|
54
|
+
cleaned = eeg_df.copy()
|
|
55
|
+
X_raw = ref_df.to_numpy(dtype=float)
|
|
56
|
+
X_safe = np.nan_to_num(X_raw, nan=0.0) # for prediction on gap samples
|
|
57
|
+
|
|
58
|
+
for ch in eeg_df.columns:
|
|
59
|
+
y = eeg_df[ch].to_numpy(dtype=float)
|
|
60
|
+
|
|
61
|
+
# Use only rows where target AND all references are finite
|
|
62
|
+
valid = np.isfinite(y) & np.all(np.isfinite(X_raw), axis=1)
|
|
63
|
+
n_valid = int(valid.sum())
|
|
64
|
+
|
|
65
|
+
if n_valid < X_raw.shape[1] + 10:
|
|
66
|
+
printWarning(
|
|
67
|
+
f"[cognitionProcessor] regress_noise_channels: channel '{ch}' "
|
|
68
|
+
f"has only {n_valid} valid samples (need > {X_raw.shape[1] + 9}) "
|
|
69
|
+
f"— skipping regression for this channel"
|
|
70
|
+
)
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
reg = Ridge(alpha=1.0, fit_intercept=True)
|
|
74
|
+
reg.fit(X_raw[valid], y[valid])
|
|
75
|
+
|
|
76
|
+
predicted = reg.predict(X_safe)
|
|
77
|
+
residual = y - predicted
|
|
78
|
+
|
|
79
|
+
# Variance reduction as a quality indicator
|
|
80
|
+
var_orig = float(np.nanvar(y[valid]))
|
|
81
|
+
var_res = float(np.nanvar(residual[valid]))
|
|
82
|
+
pct_reduction = round(100.0 * (1.0 - var_res / (var_orig + 1e-12)), 1)
|
|
83
|
+
|
|
84
|
+
printInfo(
|
|
85
|
+
f"[cognitionProcessor] regress_noise_channels: '{ch}' fitted on "
|
|
86
|
+
f"{n_valid} samples — variance reduction {pct_reduction}%"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
cleaned[ch] = residual
|
|
90
|
+
|
|
91
|
+
return cleaned
|