Spaces:
Sleeping
Sleeping
Yann Bouteiller
commited on
Commit
Β·
a64c5cc
1
Parent(s):
39c06fc
A lot of reorg and development of phase detection
Browse files- portiloop/{nn β demo}/demo_net.py +0 -0
- portiloop/notebooks/tests.ipynb +45 -5
- portiloop/{capture.py β src/capture.py} +126 -496
- portiloop/src/config.py +136 -0
- portiloop/{detection.py β src/detection.py} +1 -1
- portiloop/{hardware β src/hardware}/__init__.py +0 -0
- portiloop/{hardware β src/hardware}/frontend.py +0 -0
- portiloop/{hardware β src/hardware}/leds.py +0 -0
- portiloop/src/processing.py +141 -0
- portiloop/{stimulation.py β src/stimulation.py} +114 -32
- portiloop/src/utils.py +108 -0
portiloop/{nn β demo}/demo_net.py
RENAMED
File without changes
|
portiloop/notebooks/tests.ipynb
CHANGED
@@ -2,16 +2,56 @@
|
|
2 |
"cells": [
|
3 |
{
|
4 |
"cell_type": "code",
|
5 |
-
"execution_count":
|
6 |
"id": "16651843",
|
7 |
"metadata": {
|
8 |
"scrolled": false
|
9 |
},
|
10 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11 |
"source": [
|
12 |
-
"from portiloop.capture import Capture\n",
|
13 |
-
"from portiloop.detection import SleepSpindleRealTimeDetector\n",
|
14 |
-
"from portiloop.stimulation import SleepSpindleRealTimeStimulator\n",
|
15 |
"\n",
|
16 |
"my_detector_class = SleepSpindleRealTimeDetector # you may want to implement yours\n",
|
17 |
"my_stimulator_class = SleepSpindleRealTimeStimulator # you may also want to implement yours\n",
|
|
|
2 |
"cells": [
|
3 |
{
|
4 |
"cell_type": "code",
|
5 |
+
"execution_count": 1,
|
6 |
"id": "16651843",
|
7 |
"metadata": {
|
8 |
"scrolled": false
|
9 |
},
|
10 |
+
"outputs": [
|
11 |
+
{
|
12 |
+
"data": {
|
13 |
+
"application/vnd.jupyter.widget-view+json": {
|
14 |
+
"model_id": "573c77a0af1944859cce1eeb2c6eadea",
|
15 |
+
"version_major": 2,
|
16 |
+
"version_minor": 0
|
17 |
+
},
|
18 |
+
"text/plain": [
|
19 |
+
"VBox(children=(Accordion(children=(GridBox(children=(Label(value='CH2'), Label(value='CH3'), Label(value='CH4'β¦"
|
20 |
+
]
|
21 |
+
},
|
22 |
+
"metadata": {},
|
23 |
+
"output_type": "display_data"
|
24 |
+
},
|
25 |
+
{
|
26 |
+
"name": "stdout",
|
27 |
+
"output_type": "stream",
|
28 |
+
"text": [
|
29 |
+
"DEBUG:/home/mendel/portiloop-software/portiloop/sounds/stimulus.wav\n"
|
30 |
+
]
|
31 |
+
},
|
32 |
+
{
|
33 |
+
"name": "stderr",
|
34 |
+
"output_type": "stream",
|
35 |
+
"text": [
|
36 |
+
"Exception in thread Thread-5:\n",
|
37 |
+
"Traceback (most recent call last):\n",
|
38 |
+
" File \"/usr/lib/python3.7/threading.py\", line 917, in _bootstrap_inner\n",
|
39 |
+
" self.run()\n",
|
40 |
+
" File \"/usr/lib/python3.7/threading.py\", line 865, in run\n",
|
41 |
+
" self._target(*self._args, **self._kwargs)\n",
|
42 |
+
" File \"/home/mendel/portiloop-software/portiloop/src/capture.py\", line 920, in start_capture\n",
|
43 |
+
" stimulator = stimulator_cls() if stimulator_cls is not None else None\n",
|
44 |
+
" File \"/home/mendel/portiloop-software/portiloop/src/stimulation.py\", line 81, in __init__\n",
|
45 |
+
" self.pcm = alsaaudio.PCM(channels=f.getnchannels(), rate=f.getframerate(), format=format, periodsize=self.periodsize, device=device)\n",
|
46 |
+
"alsaaudio.ALSAAudioError: Device or resource busy [default]\n",
|
47 |
+
"\n"
|
48 |
+
]
|
49 |
+
}
|
50 |
+
],
|
51 |
"source": [
|
52 |
+
"from portiloop.src.capture import Capture\n",
|
53 |
+
"from portiloop.src.detection import SleepSpindleRealTimeDetector\n",
|
54 |
+
"from portiloop.src.stimulation import SleepSpindleRealTimeStimulator\n",
|
55 |
"\n",
|
56 |
"my_detector_class = SleepSpindleRealTimeDetector # you may want to implement yours\n",
|
57 |
"my_stimulator_class = SleepSpindleRealTimeStimulator # you may also want to implement yours\n",
|
portiloop/{capture.py β src/capture.py}
RENAMED
@@ -1,345 +1,27 @@
|
|
1 |
-
|
2 |
-
import sys
|
3 |
|
4 |
from time import sleep
|
5 |
import time
|
6 |
import numpy as np
|
7 |
-
import os
|
8 |
from copy import deepcopy
|
9 |
-
from
|
10 |
-
from datetime import datetime, timedelta
|
11 |
import multiprocessing as mp
|
12 |
import warnings
|
13 |
-
import shutil
|
14 |
from threading import Thread, Lock
|
15 |
import alsaaudio
|
16 |
|
17 |
-
from
|
18 |
-
from scipy.signal import firwin
|
19 |
-
|
20 |
-
from portilooplot.jupyter_plot import ProgressPlot
|
21 |
-
from portiloop.hardware.frontend import Frontend
|
22 |
-
from portiloop.hardware.leds import LEDs, Color
|
23 |
|
|
|
|
|
|
|
|
|
|
|
24 |
from IPython.display import clear_output, display
|
25 |
import ipywidgets as widgets
|
26 |
|
27 |
|
28 |
-
|
29 |
-
# nomenclature: name [default setting] [bits 7-0] : description
|
30 |
-
# Read only ID:
|
31 |
-
0x3E, # ID [xx] [REV_ID[2:0], 1, DEV_ID[1:0], NU_CH[1:0]] : (RO)
|
32 |
-
# Global Settings Across Channels:
|
33 |
-
0x96, # CONFIG1 [96] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 250 SPS
|
34 |
-
0xC0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]] : No tests
|
35 |
-
0x60, # CONFIG3 [60] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
36 |
-
0x00, # LOFF [00] [COMP_TH[2:0], 0, ILEAD_OFF[1:0], FLEAD_OFF[1:0]] : No lead-off
|
37 |
-
# Channel-Specific Settings:
|
38 |
-
0x61, # CH1SET [61] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] : Channel 1 active, 24 gain, no SRB2 & input shorted
|
39 |
-
0x61, # CH2SET [61] [PD2, GAIN2[2:0], SRB2, MUX2[2:0]] : Channel 2 active, 24 gain, no SRB2 & input shorted
|
40 |
-
0x61, # CH3SET [61] [PD3, GAIN3[2:0], SRB2, MUX3[2:0]] : Channel 3 active, 24 gain, no SRB2 & input shorted
|
41 |
-
0x61, # CH4SET [61] [PD4, GAIN4[2:0], SRB2, MUX4[2:0]] : Channel 4 active, 24 gain, no SRB2 & input shorted
|
42 |
-
0x61, # CH5SET [61] [PD5, GAIN5[2:0], SRB2, MUX5[2:0]] : Channel 5 active, 24 gain, no SRB2 & input shorted
|
43 |
-
0x61, # CH6SET [61] [PD6, GAIN6[2:0], SRB2, MUX6[2:0]] : Channel 6 active, 24 gain, no SRB2 & input shorted
|
44 |
-
0x61, # CH7SET [61] [PD7, GAIN7[2:0], SRB2, MUX7[2:0]] : Channel 7 active, 24 gain, no SRB2 & input shorted
|
45 |
-
0x61, # CH8SET [61] [PD8, GAIN8[2:0], SRB2, MUX8[2:0]] : Channel 8 active, 24 gain, no SRB2 & input shorted
|
46 |
-
0x00, # BIAS_SENSP [00] [BIASP8, BIASP7, BIASP6, BIASP5, BIASP4, BIASP3, BIASP2, BIASP1] : No bias
|
47 |
-
0x00, # BIAS_SENSN [00] [BIASN8, BIASN7, BIASN6, BIASN5, BIASN4, BIASN3, BIASN2, BIASN1] No bias
|
48 |
-
0x00, # LOFF_SENSP [00] [LOFFP8, LOFFP7, LOFFP6, LOFFP5, LOFFP4, LOFFP3, LOFFP2, LOFFP1] : No lead-off
|
49 |
-
0x00, # LOFF_SENSN [00] [LOFFM8, LOFFM7, LOFFM6, LOFFM5, LOFFM4, LOFFM3, LOFFM2, LOFFM1] : No lead-off
|
50 |
-
0x00, # LOFF_FLIP [00] [LOFF_FLIP8, LOFF_FLIP7, LOFF_FLIP6, LOFF_FLIP5, LOFF_FLIP4, LOFF_FLIP3, LOFF_FLIP2, LOFF_FLIP1] : No lead-off flip
|
51 |
-
# Lead-Off Status Registers (Read-Only Registers):
|
52 |
-
0x00, # LOFF_STATP [00] [IN8P_OFF, IN7P_OFF, IN6P_OFF, IN5P_OFF, IN4P_OFF, IN3P_OFF, IN2P_OFF, IN1P_OFF] : Lead-off positive status (RO)
|
53 |
-
0x00, # LOFF_STATN [00] [IN8M_OFF, IN7M_OFF, IN6M_OFF, IN5M_OFF, IN4M_OFF, IN3M_OFF, IN2M_OFF, IN1M_OFF] : Laed-off negative status (RO)
|
54 |
-
# GPIO and OTHER Registers:
|
55 |
-
0x0F, # GPIO [0F] [GPIOD[4:1], GPIOC[4:1]] : All GPIOs as inputs
|
56 |
-
0x00, # MISC1 [00] [0, 0, SRB1, 0, 0, 0, 0, 0] : Disable SRBM
|
57 |
-
0x00, # MISC2 [00] [00] : Unused
|
58 |
-
0x00, # CONFIG4 [00] [0, 0, 0, 0, SINGLE_SHOT, 0, PD_LOFF_COMP(bar), 0] : Single-shot, lead-off comparator disabled
|
59 |
-
]
|
60 |
-
|
61 |
-
FRONTEND_CONFIG = [
|
62 |
-
0x3E, # ID (RO)
|
63 |
-
0x95, # CONFIG1 [95] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 500 SPS
|
64 |
-
0xD0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]]
|
65 |
-
0xFC, # CONFIG3 [E0] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
66 |
-
0x00, # No lead-off
|
67 |
-
0x62, # CH1SET [60] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] set to measure BIAS signal
|
68 |
-
0x60, # CH2SET
|
69 |
-
0x60, # CH3SET
|
70 |
-
0x60, # CH4SET
|
71 |
-
0x60, # CH5SET
|
72 |
-
0x60, # CH6SET
|
73 |
-
0x60, # CH7SET
|
74 |
-
0x60, # CH8SET
|
75 |
-
0x00, # BIAS_SENSP 00
|
76 |
-
0x00, # BIAS_SENSN 00
|
77 |
-
0x00, # LOFF_SENSP Lead-off on all positive pins?
|
78 |
-
0x00, # LOFF_SENSN Lead-off on all negative pins?
|
79 |
-
0x00, # Normal lead-off
|
80 |
-
0x00, # Lead-off positive status (RO)
|
81 |
-
0x00, # Lead-off negative status (RO)
|
82 |
-
0x00, # All GPIOs as output ?
|
83 |
-
0x20, # Enable SRB1
|
84 |
-
]
|
85 |
-
|
86 |
-
|
87 |
-
LEADOFF_CONFIG = [
|
88 |
-
0x3E, # ID (RO)
|
89 |
-
0x95, # CONFIG1 [95] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 500 SPS
|
90 |
-
0xC0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]]
|
91 |
-
0xFC, # CONFIG3 [E0] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
92 |
-
0x00, # No lead-off
|
93 |
-
0x60, # CH1SET [60] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] set to measure BIAS signal
|
94 |
-
0x60, # CH2SET
|
95 |
-
0x60, # CH3SET
|
96 |
-
0x60, # CH4SET
|
97 |
-
0x60, # CH5SET
|
98 |
-
0x60, # CH6SET
|
99 |
-
0x60, # CH7SET
|
100 |
-
0x60, # CH8SET
|
101 |
-
0x00, # BIAS_SENSP 00
|
102 |
-
0x00, # BIAS_SENSN 00
|
103 |
-
0xFF, # LOFF_SENSP Lead-off on all positive pins?
|
104 |
-
0xFF, # LOFF_SENSN Lead-off on all negative pins?
|
105 |
-
0x00, # Normal lead-off
|
106 |
-
0x00, # Lead-off positive status (RO)
|
107 |
-
0x00, # Lead-off negative status (RO)
|
108 |
-
0x00, # All GPIOs as output ?
|
109 |
-
0x20, # Enable SRB1
|
110 |
-
0x00,
|
111 |
-
0x02,
|
112 |
-
]
|
113 |
-
|
114 |
-
EDF_PATH = Path.home() / 'workspace' / 'edf_recording'
|
115 |
-
|
116 |
-
|
117 |
-
def to_ads_frequency(frequency):
|
118 |
-
possible_datarates = [250, 500, 1000, 2000, 4000, 8000, 16000]
|
119 |
-
dr = 16000
|
120 |
-
for i in possible_datarates:
|
121 |
-
if i >= frequency:
|
122 |
-
dr = i
|
123 |
-
break
|
124 |
-
return dr
|
125 |
-
|
126 |
-
def mod_config(config, datarate, channel_modes):
|
127 |
-
|
128 |
-
# datarate:
|
129 |
-
|
130 |
-
possible_datarates = [(250, 0x06),
|
131 |
-
(500, 0x05),
|
132 |
-
(1000, 0x04),
|
133 |
-
(2000, 0x03),
|
134 |
-
(4000, 0x02),
|
135 |
-
(8000, 0x01),
|
136 |
-
(16000, 0x00)]
|
137 |
-
mod_dr = 0x00
|
138 |
-
for i, j in possible_datarates:
|
139 |
-
if i >= datarate:
|
140 |
-
mod_dr = j
|
141 |
-
break
|
142 |
-
|
143 |
-
new_cf1 = config[1] & 0xF8
|
144 |
-
new_cf1 = new_cf1 | mod_dr
|
145 |
-
config[1] = new_cf1
|
146 |
-
|
147 |
-
# bias:
|
148 |
-
assert len(channel_modes) == 7
|
149 |
-
config[13] = 0x00 # clear BIAS_SENSP
|
150 |
-
config[14] = 0x00 # clear BIAS_SENSN
|
151 |
-
for chan_i, chan_mode in enumerate(channel_modes):
|
152 |
-
n = 6 + chan_i
|
153 |
-
mod = config[n] & 0x78 # clear PDn and MUX[2:0]
|
154 |
-
if chan_mode == 'simple':
|
155 |
-
# If channel is activated, we send the channel's output to the BIAS mechanism
|
156 |
-
bit_i = 1 << chan_i + 1
|
157 |
-
config[13] = config[13] | bit_i
|
158 |
-
config[14] = config[14] | bit_i
|
159 |
-
elif chan_mode == 'disabled':
|
160 |
-
mod = mod | 0x81 # PDn = 1 and input shorted (001)
|
161 |
-
else:
|
162 |
-
assert False, f"Wrong key: {chan_mode}."
|
163 |
-
config[n] = mod
|
164 |
-
for n, c in enumerate(config): # print ADS1299 configuration registers
|
165 |
-
print(f"config[{n}]:\t{c:08b}\t({hex(c)})")
|
166 |
-
return config
|
167 |
-
|
168 |
-
|
169 |
-
def filter_24(value):
|
170 |
-
return (value * 4.5) / (2**23 - 1) / 24.0 * 1e6 # 23 because 1 bit is lost for sign
|
171 |
-
|
172 |
-
|
173 |
-
def filter_2scomplement_np(value):
|
174 |
-
return np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
|
175 |
-
|
176 |
-
|
177 |
-
def filter_np(value):
|
178 |
-
return filter_24(filter_2scomplement_np(value))
|
179 |
-
|
180 |
-
|
181 |
-
def shift_numpy(arr, num, fill_value=np.nan):
|
182 |
-
result = np.empty_like(arr)
|
183 |
-
if num > 0:
|
184 |
-
result[:num] = fill_value
|
185 |
-
result[num:] = arr[:-num]
|
186 |
-
elif num < 0:
|
187 |
-
result[num:] = fill_value
|
188 |
-
result[:num] = arr[-num:]
|
189 |
-
else:
|
190 |
-
result[:] = arr
|
191 |
-
return result
|
192 |
-
|
193 |
-
|
194 |
-
class FIR:
|
195 |
-
def __init__(self, nb_channels, coefficients, buffer=None):
|
196 |
-
|
197 |
-
self.coefficients = np.expand_dims(np.array(coefficients), axis=1)
|
198 |
-
self.taps = len(self.coefficients)
|
199 |
-
self.nb_channels = nb_channels
|
200 |
-
self.buffer = np.array(z) if buffer is not None else np.zeros((self.taps, self.nb_channels))
|
201 |
-
|
202 |
-
def filter(self, x):
|
203 |
-
self.buffer = shift_numpy(self.buffer, 1, x)
|
204 |
-
filtered = np.sum(self.buffer * self.coefficients, axis=0)
|
205 |
-
return filtered
|
206 |
-
|
207 |
-
|
208 |
-
class FilterPipeline:
|
209 |
-
def __init__(self,
|
210 |
-
nb_channels,
|
211 |
-
sampling_rate,
|
212 |
-
power_line_fq=60,
|
213 |
-
use_custom_fir=False,
|
214 |
-
custom_fir_order=20,
|
215 |
-
custom_fir_cutoff=30,
|
216 |
-
alpha_avg=0.1,
|
217 |
-
alpha_std=0.001,
|
218 |
-
epsilon=0.000001,
|
219 |
-
filter_args=[]):
|
220 |
-
if len(filter_args) > 0:
|
221 |
-
use_fir, use_notch, use_std = filter_args
|
222 |
-
else:
|
223 |
-
use_fir=True,
|
224 |
-
use_notch=True,
|
225 |
-
use_std=True
|
226 |
-
self.use_fir = use_fir
|
227 |
-
self.use_notch = use_notch
|
228 |
-
self.use_std = use_std
|
229 |
-
self.nb_channels = nb_channels
|
230 |
-
assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50 Hz and 60 Hz"
|
231 |
-
if power_line_fq == 60:
|
232 |
-
self.notch_coeff1 = -0.12478308884588535
|
233 |
-
self.notch_coeff2 = 0.98729186796473023
|
234 |
-
self.notch_coeff3 = 0.99364593398236511
|
235 |
-
self.notch_coeff4 = -0.12478308884588535
|
236 |
-
self.notch_coeff5 = 0.99364593398236511
|
237 |
-
else:
|
238 |
-
self.notch_coeff1 = -0.61410695998423581
|
239 |
-
self.notch_coeff2 = 0.98729186796473023
|
240 |
-
self.notch_coeff3 = 0.99364593398236511
|
241 |
-
self.notch_coeff4 = -0.61410695998423581
|
242 |
-
self.notch_coeff5 = 0.99364593398236511
|
243 |
-
self.dfs = [np.zeros(self.nb_channels), np.zeros(self.nb_channels)]
|
244 |
-
|
245 |
-
self.moving_average = None
|
246 |
-
self.moving_variance = np.zeros(self.nb_channels)
|
247 |
-
self.ALPHA_AVG = alpha_avg
|
248 |
-
self.ALPHA_STD = alpha_std
|
249 |
-
self.EPSILON = epsilon
|
250 |
-
|
251 |
-
if use_custom_fir:
|
252 |
-
self.fir_coef = firwin(numtaps=custom_fir_order+1, cutoff=custom_fir_cutoff, fs=sampling_rate)
|
253 |
-
else:
|
254 |
-
self.fir_coef = [
|
255 |
-
0.001623780150148094927192721215192250384,
|
256 |
-
0.014988684599373741992978104065059596905,
|
257 |
-
0.021287595318265635502275046064823982306,
|
258 |
-
0.007349500393709578957568417933998716762,
|
259 |
-
-0.025127515717112181709014251396183681209,
|
260 |
-
-0.052210507359822452833064687638398027048,
|
261 |
-
-0.039273839505489904766477593511808663607,
|
262 |
-
0.033021568427940004020193498490698402748,
|
263 |
-
0.147606943281569008563636202779889572412,
|
264 |
-
0.254000252034505602516389899392379447818,
|
265 |
-
0.297330876398883392486283128164359368384,
|
266 |
-
0.254000252034505602516389899392379447818,
|
267 |
-
0.147606943281569008563636202779889572412,
|
268 |
-
0.033021568427940004020193498490698402748,
|
269 |
-
-0.039273839505489904766477593511808663607,
|
270 |
-
-0.052210507359822452833064687638398027048,
|
271 |
-
-0.025127515717112181709014251396183681209,
|
272 |
-
0.007349500393709578957568417933998716762,
|
273 |
-
0.021287595318265635502275046064823982306,
|
274 |
-
0.014988684599373741992978104065059596905,
|
275 |
-
0.001623780150148094927192721215192250384]
|
276 |
-
self.fir = FIR(self.nb_channels, self.fir_coef)
|
277 |
-
|
278 |
-
def filter(self, value):
|
279 |
-
"""
|
280 |
-
value: a numpy array of shape (data series, channels)
|
281 |
-
"""
|
282 |
-
for i, x in enumerate(value): # loop over the data series
|
283 |
-
# FIR:
|
284 |
-
if self.use_fir:
|
285 |
-
x = self.fir.filter(x)
|
286 |
-
# notch:
|
287 |
-
if self.use_notch:
|
288 |
-
denAccum = (x - self.notch_coeff1 * self.dfs[0]) - self.notch_coeff2 * self.dfs[1]
|
289 |
-
x = (self.notch_coeff3 * denAccum + self.notch_coeff4 * self.dfs[0]) + self.notch_coeff5 * self.dfs[1]
|
290 |
-
self.dfs[1] = self.dfs[0]
|
291 |
-
self.dfs[0] = denAccum
|
292 |
-
# standardization:
|
293 |
-
if self.use_std:
|
294 |
-
if self.moving_average is not None:
|
295 |
-
delta = x - self.moving_average
|
296 |
-
self.moving_average = self.moving_average + self.ALPHA_AVG * delta
|
297 |
-
self.moving_variance = (1 - self.ALPHA_STD) * (self.moving_variance + self.ALPHA_STD * delta**2)
|
298 |
-
moving_std = np.sqrt(self.moving_variance)
|
299 |
-
x = (x - self.moving_average) / (moving_std + self.EPSILON)
|
300 |
-
else:
|
301 |
-
self.moving_average = x
|
302 |
-
value[i] = x
|
303 |
-
return value
|
304 |
-
|
305 |
-
|
306 |
-
class LiveDisplay():
|
307 |
-
def __init__(self, channel_names, window_len=100):
|
308 |
-
self.datapoint_dim = len(channel_names)
|
309 |
-
self.history = []
|
310 |
-
self.pp = ProgressPlot(plot_names=channel_names, max_window_len=window_len)
|
311 |
-
self.matplotlib = False
|
312 |
-
|
313 |
-
def add_datapoints(self, datapoints):
|
314 |
-
"""
|
315 |
-
Adds 8 lists of datapoints to the plot
|
316 |
-
|
317 |
-
Args:
|
318 |
-
datapoints: list of 8 lists of floats (or list of 8 floats)
|
319 |
-
"""
|
320 |
-
if self.matplotlib:
|
321 |
-
import matplotlib.pyplot as plt
|
322 |
-
disp_list = []
|
323 |
-
for datapoint in datapoints:
|
324 |
-
d = [[elt] for elt in datapoint]
|
325 |
-
disp_list.append(d)
|
326 |
-
|
327 |
-
if self.matplotlib:
|
328 |
-
self.history += d[1]
|
329 |
-
|
330 |
-
if not self.matplotlib:
|
331 |
-
self.pp.update_with_datapoints(disp_list)
|
332 |
-
elif len(self.history) == 1000:
|
333 |
-
plt.plot(self.history)
|
334 |
-
plt.show()
|
335 |
-
self.history = []
|
336 |
-
|
337 |
-
def add_datapoint(self, datapoint):
|
338 |
-
disp_list = [[elt] for elt in datapoint]
|
339 |
-
self.pp.update(disp_list)
|
340 |
-
|
341 |
-
|
342 |
-
def _capture_process(p_data_o, p_msg_io, duration, frequency, python_clock, time_msg_in, channel_states):
|
343 |
"""
|
344 |
Args:
|
345 |
p_data_o: multiprocessing.Pipe: captured datapoints are put here
|
@@ -434,69 +116,8 @@ def _capture_process(p_data_o, p_msg_io, duration, frequency, python_clock, time
|
|
434 |
p_msg_io.send('STOP')
|
435 |
p_msg_io.close()
|
436 |
p_data_o.close()
|
437 |
-
|
438 |
-
|
439 |
-
class DummyAlsaMixer:
|
440 |
-
def __init__(self):
|
441 |
-
self.volume = 50
|
442 |
|
443 |
-
def getvolume(self):
|
444 |
-
return [self.volume]
|
445 |
-
|
446 |
-
def setvolume(self, volume):
|
447 |
-
self.volume = volume
|
448 |
-
|
449 |
-
|
450 |
-
class UpStateDelayer:
|
451 |
-
def __init__(self, sample_freq, spindle_freq, peak):
|
452 |
-
'''
|
453 |
-
args:
|
454 |
-
buffer_size: int -> Size of desired buffer in length
|
455 |
-
sample_freq: int -> Sampling frequency of signal in Hz
|
456 |
-
'''
|
457 |
-
# Get number of timesteps for a whole spindle
|
458 |
-
self.spindle_timesteps = (1/spindle_freq) * sample_freq # s *
|
459 |
-
self.sample_freq = sample_freq
|
460 |
-
self.buffer_size = 1.5 * self.spindle_timesteps
|
461 |
-
self.peak = peak
|
462 |
-
self.buffer = []
|
463 |
|
464 |
-
def add_point(self, point):
|
465 |
-
'''
|
466 |
-
Adds a point to the buffer to be able to keep track of peaks
|
467 |
-
'''
|
468 |
-
self.buffer.append(point)
|
469 |
-
if len(self.buffer) > self.buffer_size:
|
470 |
-
self.buffer.pop(0)
|
471 |
-
|
472 |
-
def stimulate(self):
|
473 |
-
# Calculate how far away is last peak
|
474 |
-
last_peak = -1
|
475 |
-
count = 0
|
476 |
-
for idx, point in reversed(list(enumerate(self.buffer))):
|
477 |
-
if self.peak:
|
478 |
-
try:
|
479 |
-
sup = point >= self.buffer[idx+1]
|
480 |
-
except IndexError:
|
481 |
-
sup = False
|
482 |
-
try:
|
483 |
-
inf = point >= self.buffer[idx-1]
|
484 |
-
except IndexError:
|
485 |
-
inf = False
|
486 |
-
else:
|
487 |
-
try:
|
488 |
-
sup = point <= self.buffer[idx+1]
|
489 |
-
except IndexError:
|
490 |
-
sup = False
|
491 |
-
try:
|
492 |
-
inf = point <= self.buffer[idx-1]
|
493 |
-
except IndexError:
|
494 |
-
inf = False
|
495 |
-
if sup and inf:
|
496 |
-
last_peak = count
|
497 |
-
return self.spindle_timesteps - last_peak
|
498 |
-
count += 1
|
499 |
-
return -1
|
500 |
|
501 |
class Capture:
|
502 |
def __init__(self, detector_cls=None, stimulator_cls=None):
|
@@ -521,13 +142,10 @@ class Capture:
|
|
521 |
self.threshold = 0.5
|
522 |
self.lsl = False
|
523 |
self.display = False
|
|
|
524 |
self.python_clock = True
|
525 |
self.edf_writer = None
|
526 |
self.edf_buffer = []
|
527 |
-
self.nb_signals = 8
|
528 |
-
self.samples_per_datarecord_array = self.frequency
|
529 |
-
self.physical_max = 5
|
530 |
-
self.physical_min = -5
|
531 |
self.signal_labels = ['Common Mode', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6', 'ch7', 'ch8']
|
532 |
self._lock_msg_out = Lock()
|
533 |
self._msg_out = None
|
@@ -678,7 +296,7 @@ class Capture:
|
|
678 |
)
|
679 |
|
680 |
self.b_clock = widgets.ToggleButtons(
|
681 |
-
options=['
|
682 |
description='Clock:',
|
683 |
disabled=False,
|
684 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
@@ -694,6 +312,15 @@ class Capture:
|
|
694 |
tooltips=['North America 60 Hz',
|
695 |
'Europe 50 Hz'],
|
696 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
697 |
|
698 |
self.b_custom_fir = widgets.ToggleButtons(
|
699 |
options=['Default', 'Custom'],
|
@@ -890,6 +517,7 @@ class Capture:
|
|
890 |
self.b_spindle_mode.observe(self.on_b_spindle_mode, 'value')
|
891 |
self.b_spindle_freq.observe(self.on_b_spindle_freq, 'value')
|
892 |
self.b_power_line.observe(self.on_b_power_line, 'value')
|
|
|
893 |
self.b_custom_fir.observe(self.on_b_custom_fir, 'value')
|
894 |
self.b_custom_fir_order.observe(self.on_b_custom_fir_order, 'value')
|
895 |
self.b_custom_fir_cutoff.observe(self.on_b_custom_fir_cutoff, 'value')
|
@@ -912,6 +540,7 @@ class Capture:
|
|
912 |
self.b_frequency,
|
913 |
self.b_duration,
|
914 |
self.b_filename,
|
|
|
915 |
self.b_power_line,
|
916 |
self.b_clock,
|
917 |
widgets.HBox([self.b_filter, self.b_detect, self.b_stimulate, self.b_record, self.b_lsl, self.b_display]),
|
@@ -941,6 +570,7 @@ class Capture:
|
|
941 |
self.b_radio_ch7.disabled = False
|
942 |
self.b_radio_ch8.disabled = False
|
943 |
self.b_power_line.disabled = False
|
|
|
944 |
self.b_channel_detect.disabled = False
|
945 |
self.b_spindle_freq.disabled = False
|
946 |
self.b_spindle_mode.disabled = False
|
@@ -981,6 +611,7 @@ class Capture:
|
|
981 |
self.b_channel_detect.disabled = True
|
982 |
self.b_spindle_freq.disabled = True
|
983 |
self.b_spindle_mode.disabled = True
|
|
|
984 |
self.b_power_line.disabled = True
|
985 |
self.b_polyak_mean.disabled = True
|
986 |
self.b_polyak_std.disabled = True
|
@@ -1067,8 +698,6 @@ class Capture:
|
|
1067 |
self._t_capture = None
|
1068 |
self.enable_buttons()
|
1069 |
|
1070 |
-
|
1071 |
-
|
1072 |
def on_b_custom_fir(self, value):
|
1073 |
val = value['new']
|
1074 |
if val == 'Default':
|
@@ -1083,13 +712,20 @@ class Capture:
|
|
1083 |
self.python_clock = True
|
1084 |
elif val == 'ADS':
|
1085 |
self.python_clock = False
|
1086 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1087 |
def on_b_power_line(self, value):
|
1088 |
val = value['new']
|
1089 |
if val == '60 Hz':
|
1090 |
self.power_line = 60
|
1091 |
elif val == '50 Hz':
|
1092 |
-
self.
|
1093 |
|
1094 |
def on_b_frequency(self, value):
|
1095 |
val = value['new']
|
@@ -1243,42 +879,6 @@ class Capture:
|
|
1243 |
elif val == 'Paused':
|
1244 |
with self._pause_detect_lock:
|
1245 |
self._pause_detect = True
|
1246 |
-
|
1247 |
-
def open_recording_file(self):
|
1248 |
-
nb_signals = self.nb_signals
|
1249 |
-
samples_per_datarecord_array = self.samples_per_datarecord_array
|
1250 |
-
physical_max = self.physical_max
|
1251 |
-
physical_min = self.physical_min
|
1252 |
-
signal_labels = self.signal_labels
|
1253 |
-
|
1254 |
-
print(f"Will store edf recording in {self.filename}")
|
1255 |
-
|
1256 |
-
self.edf_writer = EDFwriter(p_path=str(self.filename),
|
1257 |
-
f_file_type=EDFwriter.EDFLIB_FILETYPE_EDFPLUS,
|
1258 |
-
number_of_signals=nb_signals)
|
1259 |
-
|
1260 |
-
for signal in range(nb_signals):
|
1261 |
-
assert self.edf_writer.setSampleFrequency(signal, samples_per_datarecord_array) == 0
|
1262 |
-
assert self.edf_writer.setPhysicalMaximum(signal, physical_max) == 0
|
1263 |
-
assert self.edf_writer.setPhysicalMinimum(signal, physical_min) == 0
|
1264 |
-
assert self.edf_writer.setDigitalMaximum(signal, 32767) == 0
|
1265 |
-
assert self.edf_writer.setDigitalMinimum(signal, -32768) == 0
|
1266 |
-
assert self.edf_writer.setSignalLabel(signal, signal_labels[signal]) == 0
|
1267 |
-
assert self.edf_writer.setPhysicalDimension(signal, 'V') == 0
|
1268 |
-
|
1269 |
-
def close_recording_file(self):
|
1270 |
-
assert self.edf_writer.close() == 0
|
1271 |
-
|
1272 |
-
def add_recording_data(self, data):
|
1273 |
-
self.edf_buffer += data
|
1274 |
-
if len(self.edf_buffer) >= self.samples_per_datarecord_array:
|
1275 |
-
datarecord_array = self.edf_buffer[:self.samples_per_datarecord_array]
|
1276 |
-
self.edf_buffer = self.edf_buffer[self.samples_per_datarecord_array:]
|
1277 |
-
datarecord_array = np.array(datarecord_array).transpose()
|
1278 |
-
assert len(datarecord_array) == self.nb_signals, f"len(data)={len(data)}!={self.nb_signals}"
|
1279 |
-
for d in datarecord_array:
|
1280 |
-
assert len(d) == self.samples_per_datarecord_array, f"{len(d)}!={self.samples_per_datarecord_array}"
|
1281 |
-
assert self.edf_writer.writeSamples(d) == 0
|
1282 |
|
1283 |
def start_capture(self,
|
1284 |
filter,
|
@@ -1292,15 +892,17 @@ class Capture:
|
|
1292 |
viz,
|
1293 |
width,
|
1294 |
python_clock):
|
1295 |
-
if self.__capture_on:
|
1296 |
-
warnings.warn("Capture is already ongoing, ignoring command.")
|
1297 |
-
return
|
1298 |
-
else:
|
1299 |
-
self.__capture_on = True
|
1300 |
-
p_msg_io, p_msg_io_2 = mp.Pipe()
|
1301 |
-
p_data_i, p_data_o = mp.Pipe(duplex=False)
|
1302 |
-
SAMPLE_TIME = 1 / self.frequency
|
1303 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1304 |
if filter:
|
1305 |
fp = FilterPipeline(nb_channels=8,
|
1306 |
sampling_rate=self.frequency,
|
@@ -1312,28 +914,37 @@ class Capture:
|
|
1312 |
alpha_std=self.polyak_std,
|
1313 |
epsilon=self.epsilon,
|
1314 |
filter_args=filter_args)
|
1315 |
-
|
|
|
1316 |
detector = detector_cls(threshold, channel=channel) if detector_cls is not None else None
|
1317 |
stimulator = stimulator_cls() if stimulator_cls is not None else None
|
1318 |
|
1319 |
-
|
1320 |
-
|
1321 |
-
|
1322 |
-
|
1323 |
-
|
1324 |
-
|
1325 |
-
|
1326 |
-
|
1327 |
-
|
1328 |
-
|
1329 |
-
|
|
|
|
|
|
|
|
|
|
|
1330 |
|
|
|
1331 |
if viz:
|
1332 |
live_disp = LiveDisplay(channel_names = self.signal_labels, window_len=width)
|
1333 |
|
|
|
1334 |
if record:
|
1335 |
-
self.
|
1336 |
|
|
|
1337 |
if lsl:
|
1338 |
from pylsl import StreamInfo, StreamOutlet
|
1339 |
lsl_info = StreamInfo(name='Portiloop Filtered',
|
@@ -1353,53 +964,71 @@ class Capture:
|
|
1353 |
|
1354 |
buffer = []
|
1355 |
|
|
|
1356 |
if not self.spindle_detection_mode == 'Fast' and stimulator is not None:
|
1357 |
-
stimulation_delayer = UpStateDelayer(self.frequency, self.spindle_freq, self.spindle_detection_mode == 'Peak')
|
1358 |
stimulator.add_delayer(stimulation_delayer)
|
1359 |
else:
|
1360 |
stimulation_delayer = None
|
1361 |
|
|
|
1362 |
while True:
|
1363 |
-
|
1364 |
-
if
|
1365 |
-
|
1366 |
-
self._msg_out
|
1367 |
-
|
1368 |
-
|
1369 |
-
|
1370 |
-
|
1371 |
-
|
1372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1373 |
|
1374 |
-
|
1375 |
-
|
1376 |
-
|
1377 |
-
|
1378 |
-
else:
|
1379 |
-
continue
|
1380 |
-
|
1381 |
-
n_array = np.array([point])
|
1382 |
-
n_array_raw = filter_np(n_array)
|
1383 |
|
|
|
1384 |
if filter:
|
1385 |
n_array = fp.filter(deepcopy(n_array_raw))
|
1386 |
else:
|
1387 |
-
n_array = n_array_raw
|
1388 |
-
|
|
|
1389 |
filtered_point = n_array.tolist()
|
1390 |
|
|
|
1391 |
if lsl:
|
1392 |
raw_point = n_array_raw.tolist()
|
1393 |
lsl_outlet_raw.push_sample(raw_point[-1])
|
1394 |
lsl_outlet.push_sample(filtered_point[-1])
|
1395 |
-
|
|
|
1396 |
if stimulation_delayer is not None:
|
1397 |
-
stimulation_delayer.
|
1398 |
|
|
|
1399 |
with self._pause_detect_lock:
|
1400 |
pause = self._pause_detect
|
|
|
|
|
1401 |
if detector is not None and not pause:
|
|
|
1402 |
detection_signal = detector.detect(filtered_point)
|
|
|
|
|
1403 |
if stimulator is not None:
|
1404 |
stimulator.stimulate(detection_signal)
|
1405 |
with self._test_stimulus_lock:
|
@@ -1407,35 +1036,36 @@ class Capture:
|
|
1407 |
self._test_stimulus = False
|
1408 |
if test_stimulus:
|
1409 |
stimulator.test_stimulus()
|
|
|
|
|
|
|
1410 |
|
|
|
1411 |
buffer += filtered_point
|
1412 |
if len(buffer) >= 50:
|
1413 |
-
|
1414 |
if viz:
|
1415 |
live_disp.add_datapoints(buffer)
|
1416 |
-
|
1417 |
if record:
|
1418 |
-
|
1419 |
-
|
1420 |
buffer = []
|
1421 |
|
1422 |
-
|
1423 |
-
|
1424 |
-
|
1425 |
-
|
1426 |
-
|
1427 |
-
|
1428 |
-
|
1429 |
-
|
|
|
1430 |
|
1431 |
-
|
1432 |
-
|
|
|
|
|
1433 |
|
1434 |
if record:
|
1435 |
-
|
1436 |
-
|
1437 |
-
self._p_capture.join()
|
1438 |
-
self.__capture_on = False
|
1439 |
|
1440 |
|
1441 |
if __name__ == "__main__":
|
|
|
1 |
+
|
|
|
2 |
|
3 |
from time import sleep
|
4 |
import time
|
5 |
import numpy as np
|
|
|
6 |
from copy import deepcopy
|
7 |
+
from datetime import datetime
|
|
|
8 |
import multiprocessing as mp
|
9 |
import warnings
|
|
|
10 |
from threading import Thread, Lock
|
11 |
import alsaaudio
|
12 |
|
13 |
+
from portiloop.src.stimulation import UpStateDelayer
|
|
|
|
|
|
|
|
|
|
|
14 |
|
15 |
+
from portiloop.src.hardware.frontend import Frontend
|
16 |
+
from portiloop.src.hardware.leds import LEDs, Color
|
17 |
+
from portiloop.src.processing import FilterPipeline, int_to_float
|
18 |
+
from portiloop.src.config import mod_config, LEADOFF_CONFIG, FRONTEND_CONFIG, to_ads_frequency
|
19 |
+
from portiloop.src.utils import FileReader, LiveDisplay, DummyAlsaMixer, EDFRecorder, EDF_PATH
|
20 |
from IPython.display import clear_output, display
|
21 |
import ipywidgets as widgets
|
22 |
|
23 |
|
24 |
+
def capture_process(p_data_o, p_msg_io, duration, frequency, python_clock, time_msg_in, channel_states):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
"""
|
26 |
Args:
|
27 |
p_data_o: multiprocessing.Pipe: captured datapoints are put here
|
|
|
116 |
p_msg_io.send('STOP')
|
117 |
p_msg_io.close()
|
118 |
p_data_o.close()
|
|
|
|
|
|
|
|
|
|
|
119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
121 |
|
122 |
class Capture:
|
123 |
def __init__(self, detector_cls=None, stimulator_cls=None):
|
|
|
142 |
self.threshold = 0.5
|
143 |
self.lsl = False
|
144 |
self.display = False
|
145 |
+
self.signal_input = "ADS"
|
146 |
self.python_clock = True
|
147 |
self.edf_writer = None
|
148 |
self.edf_buffer = []
|
|
|
|
|
|
|
|
|
149 |
self.signal_labels = ['Common Mode', 'ch2', 'ch3', 'ch4', 'ch5', 'ch6', 'ch7', 'ch8']
|
150 |
self._lock_msg_out = Lock()
|
151 |
self._msg_out = None
|
|
|
296 |
)
|
297 |
|
298 |
self.b_clock = widgets.ToggleButtons(
|
299 |
+
options=['ADS', 'Coral'],
|
300 |
description='Clock:',
|
301 |
disabled=False,
|
302 |
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
|
|
312 |
tooltips=['North America 60 Hz',
|
313 |
'Europe 50 Hz'],
|
314 |
)
|
315 |
+
|
316 |
+
self.b_signal_input = widgets.ToggleButtons(
|
317 |
+
options=['ADS', 'File'],
|
318 |
+
description='Signal Input:',
|
319 |
+
disabled=False,
|
320 |
+
button_style='', # 'success', 'info', 'warning', 'danger' or ''
|
321 |
+
tooltips=['Read data from ADS.',
|
322 |
+
'Read data from file.'],
|
323 |
+
)
|
324 |
|
325 |
self.b_custom_fir = widgets.ToggleButtons(
|
326 |
options=['Default', 'Custom'],
|
|
|
517 |
self.b_spindle_mode.observe(self.on_b_spindle_mode, 'value')
|
518 |
self.b_spindle_freq.observe(self.on_b_spindle_freq, 'value')
|
519 |
self.b_power_line.observe(self.on_b_power_line, 'value')
|
520 |
+
self.b_signal_input.observe(self.on_b_power_line, 'value')
|
521 |
self.b_custom_fir.observe(self.on_b_custom_fir, 'value')
|
522 |
self.b_custom_fir_order.observe(self.on_b_custom_fir_order, 'value')
|
523 |
self.b_custom_fir_cutoff.observe(self.on_b_custom_fir_cutoff, 'value')
|
|
|
540 |
self.b_frequency,
|
541 |
self.b_duration,
|
542 |
self.b_filename,
|
543 |
+
self.b_signal_input,
|
544 |
self.b_power_line,
|
545 |
self.b_clock,
|
546 |
widgets.HBox([self.b_filter, self.b_detect, self.b_stimulate, self.b_record, self.b_lsl, self.b_display]),
|
|
|
570 |
self.b_radio_ch7.disabled = False
|
571 |
self.b_radio_ch8.disabled = False
|
572 |
self.b_power_line.disabled = False
|
573 |
+
self.b_signal_input.disabled = False
|
574 |
self.b_channel_detect.disabled = False
|
575 |
self.b_spindle_freq.disabled = False
|
576 |
self.b_spindle_mode.disabled = False
|
|
|
611 |
self.b_channel_detect.disabled = True
|
612 |
self.b_spindle_freq.disabled = True
|
613 |
self.b_spindle_mode.disabled = True
|
614 |
+
self.b_signal_input.disabled = True
|
615 |
self.b_power_line.disabled = True
|
616 |
self.b_polyak_mean.disabled = True
|
617 |
self.b_polyak_std.disabled = True
|
|
|
698 |
self._t_capture = None
|
699 |
self.enable_buttons()
|
700 |
|
|
|
|
|
701 |
def on_b_custom_fir(self, value):
|
702 |
val = value['new']
|
703 |
if val == 'Default':
|
|
|
712 |
self.python_clock = True
|
713 |
elif val == 'ADS':
|
714 |
self.python_clock = False
|
715 |
+
|
716 |
+
def on_b_signal_input(self, value):
|
717 |
+
val = value['new']
|
718 |
+
if val == "ADS":
|
719 |
+
self.signal_input = "ADS"
|
720 |
+
elif val == "File":
|
721 |
+
self.signal_input = "File"
|
722 |
+
|
723 |
def on_b_power_line(self, value):
|
724 |
val = value['new']
|
725 |
if val == '60 Hz':
|
726 |
self.power_line = 60
|
727 |
elif val == '50 Hz':
|
728 |
+
self.power_line = 50
|
729 |
|
730 |
def on_b_frequency(self, value):
|
731 |
val = value['new']
|
|
|
879 |
elif val == 'Paused':
|
880 |
with self._pause_detect_lock:
|
881 |
self._pause_detect = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
882 |
|
883 |
def start_capture(self,
|
884 |
filter,
|
|
|
892 |
viz,
|
893 |
width,
|
894 |
python_clock):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
895 |
|
896 |
+
if self.signal_input == "ADS":
|
897 |
+
if self.__capture_on:
|
898 |
+
warnings.warn("Capture is already ongoing, ignoring command.")
|
899 |
+
return
|
900 |
+
else:
|
901 |
+
self.__capture_on = True
|
902 |
+
p_msg_io, p_msg_io_2 = mp.Pipe()
|
903 |
+
p_data_i, p_data_o = mp.Pipe(duplex=False)
|
904 |
+
|
905 |
+
# Initialize filtering pipeline
|
906 |
if filter:
|
907 |
fp = FilterPipeline(nb_channels=8,
|
908 |
sampling_rate=self.frequency,
|
|
|
914 |
alpha_std=self.polyak_std,
|
915 |
epsilon=self.epsilon,
|
916 |
filter_args=filter_args)
|
917 |
+
|
918 |
+
# Initialize detector and stimulator
|
919 |
detector = detector_cls(threshold, channel=channel) if detector_cls is not None else None
|
920 |
stimulator = stimulator_cls() if stimulator_cls is not None else None
|
921 |
|
922 |
+
# Launch the capture process
|
923 |
+
if self.signal_input == "ADS":
|
924 |
+
self._p_capture = mp.Process(target=capture_process,
|
925 |
+
args=(p_data_o,
|
926 |
+
p_msg_io_2,
|
927 |
+
self.duration,
|
928 |
+
self.frequency,
|
929 |
+
python_clock,
|
930 |
+
1.0,
|
931 |
+
self.channel_states)
|
932 |
+
)
|
933 |
+
self._p_capture.start()
|
934 |
+
print(f"PID capture: {self._p_capture.pid}")
|
935 |
+
else:
|
936 |
+
filename = "INSERT FILENAME" # TODO
|
937 |
+
file_reader = FileReader(filename)
|
938 |
|
939 |
+
# Initialize display if requested
|
940 |
if viz:
|
941 |
live_disp = LiveDisplay(channel_names = self.signal_labels, window_len=width)
|
942 |
|
943 |
+
# Initialize recording if requested
|
944 |
if record:
|
945 |
+
recorder = EDFRecorder(self.signal_label)
|
946 |
|
947 |
+
# Initialize LSL to stream if requested
|
948 |
if lsl:
|
949 |
from pylsl import StreamInfo, StreamOutlet
|
950 |
lsl_info = StreamInfo(name='Portiloop Filtered',
|
|
|
964 |
|
965 |
buffer = []
|
966 |
|
967 |
+
# Initialize stimulation delayer if requested
|
968 |
if not self.spindle_detection_mode == 'Fast' and stimulator is not None:
|
969 |
+
stimulation_delayer = UpStateDelayer(self.frequency, self.spindle_freq, self.spindle_detection_mode == 'Peak', time_to_buffer=0.1)
|
970 |
stimulator.add_delayer(stimulation_delayer)
|
971 |
else:
|
972 |
stimulation_delayer = None
|
973 |
|
974 |
+
# Main capture loop
|
975 |
while True:
|
976 |
+
if self.signal_input == "ADS":
|
977 |
+
# Send message in communication pipe if we have one
|
978 |
+
with self._lock_msg_out:
|
979 |
+
if self._msg_out is not None:
|
980 |
+
p_msg_io.send(self._msg_out)
|
981 |
+
self._msg_out = None
|
982 |
+
|
983 |
+
# Check if we have received a message in communication pipe
|
984 |
+
if p_msg_io.poll():
|
985 |
+
mess = p_msg_io.recv()
|
986 |
+
if mess == 'STOP':
|
987 |
+
break
|
988 |
+
elif mess[0] == 'PRT':
|
989 |
+
print(mess[1])
|
990 |
+
|
991 |
+
# Retrieve all data points from data pipe p_data
|
992 |
+
point = None
|
993 |
+
if p_data_i.poll(timeout=(1 / self.frequency)):
|
994 |
+
point = p_data_i.recv()
|
995 |
+
else:
|
996 |
+
continue
|
997 |
|
998 |
+
# Convert point from int to corresponding value in microvolts
|
999 |
+
n_array_raw = int_to_float(np.array([point]))
|
1000 |
+
elif self.signal_input == "File":
|
1001 |
+
n_array_raw, gt_stimulation = file_reader.get_point()
|
|
|
|
|
|
|
|
|
|
|
1002 |
|
1003 |
+
# Go through filtering pipeline
|
1004 |
if filter:
|
1005 |
n_array = fp.filter(deepcopy(n_array_raw))
|
1006 |
else:
|
1007 |
+
n_array = deepcopy(n_array_raw)
|
1008 |
+
|
1009 |
+
# Contains the filtered point (if filtering is off, contains a copy of the raw point)
|
1010 |
filtered_point = n_array.tolist()
|
1011 |
|
1012 |
+
# Send both raw and filtered points over LSL
|
1013 |
if lsl:
|
1014 |
raw_point = n_array_raw.tolist()
|
1015 |
lsl_outlet_raw.push_sample(raw_point[-1])
|
1016 |
lsl_outlet.push_sample(filtered_point[-1])
|
1017 |
+
|
1018 |
+
# Adds point to buffer for delayed stimulation
|
1019 |
if stimulation_delayer is not None:
|
1020 |
+
stimulation_delayer.step(filtered_point[0][channel-1])
|
1021 |
|
1022 |
+
# Check if detection is on or off
|
1023 |
with self._pause_detect_lock:
|
1024 |
pause = self._pause_detect
|
1025 |
+
|
1026 |
+
# If detection is on
|
1027 |
if detector is not None and not pause:
|
1028 |
+
# Detect using the latest point
|
1029 |
detection_signal = detector.detect(filtered_point)
|
1030 |
+
|
1031 |
+
# Stimulate
|
1032 |
if stimulator is not None:
|
1033 |
stimulator.stimulate(detection_signal)
|
1034 |
with self._test_stimulus_lock:
|
|
|
1036 |
self._test_stimulus = False
|
1037 |
if test_stimulus:
|
1038 |
stimulator.test_stimulus()
|
1039 |
+
|
1040 |
+
if self.signal_input == "File" and gt_stimulation:
|
1041 |
+
stimulator.send_stimulation("GROUND_TRUTH_STIM", False)
|
1042 |
|
1043 |
+
# Add point to the buffer to send to viz and recorder
|
1044 |
buffer += filtered_point
|
1045 |
if len(buffer) >= 50:
|
|
|
1046 |
if viz:
|
1047 |
live_disp.add_datapoints(buffer)
|
|
|
1048 |
if record:
|
1049 |
+
recorder.add_recording_data(buffer)
|
|
|
1050 |
buffer = []
|
1051 |
|
1052 |
+
if self.signal_input == "ADS":
|
1053 |
+
# Empty pipes
|
1054 |
+
while True:
|
1055 |
+
if p_data_i.poll():
|
1056 |
+
_ = p_data_i.recv()
|
1057 |
+
elif p_msg_io.poll():
|
1058 |
+
_ = p_msg_io.recv()
|
1059 |
+
else:
|
1060 |
+
break
|
1061 |
|
1062 |
+
p_data_i.close()
|
1063 |
+
p_msg_io.close()
|
1064 |
+
self._p_capture.join()
|
1065 |
+
self.__capture_on = False
|
1066 |
|
1067 |
if record:
|
1068 |
+
recorder.close_recording_file()
|
|
|
|
|
|
|
1069 |
|
1070 |
|
1071 |
if __name__ == "__main__":
|
portiloop/src/config.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
DEFAULT_FRONTEND_CONFIG = [
|
2 |
+
# nomenclature: name [default setting] [bits 7-0] : description
|
3 |
+
# Read only ID:
|
4 |
+
0x3E, # ID [xx] [REV_ID[2:0], 1, DEV_ID[1:0], NU_CH[1:0]] : (RO)
|
5 |
+
# Global Settings Across Channels:
|
6 |
+
0x96, # CONFIG1 [96] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 250 SPS
|
7 |
+
0xC0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]] : No tests
|
8 |
+
0x60, # CONFIG3 [60] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
9 |
+
0x00, # LOFF [00] [COMP_TH[2:0], 0, ILEAD_OFF[1:0], FLEAD_OFF[1:0]] : No lead-off
|
10 |
+
# Channel-Specific Settings:
|
11 |
+
0x61, # CH1SET [61] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] : Channel 1 active, 24 gain, no SRB2 & input shorted
|
12 |
+
0x61, # CH2SET [61] [PD2, GAIN2[2:0], SRB2, MUX2[2:0]] : Channel 2 active, 24 gain, no SRB2 & input shorted
|
13 |
+
0x61, # CH3SET [61] [PD3, GAIN3[2:0], SRB2, MUX3[2:0]] : Channel 3 active, 24 gain, no SRB2 & input shorted
|
14 |
+
0x61, # CH4SET [61] [PD4, GAIN4[2:0], SRB2, MUX4[2:0]] : Channel 4 active, 24 gain, no SRB2 & input shorted
|
15 |
+
0x61, # CH5SET [61] [PD5, GAIN5[2:0], SRB2, MUX5[2:0]] : Channel 5 active, 24 gain, no SRB2 & input shorted
|
16 |
+
0x61, # CH6SET [61] [PD6, GAIN6[2:0], SRB2, MUX6[2:0]] : Channel 6 active, 24 gain, no SRB2 & input shorted
|
17 |
+
0x61, # CH7SET [61] [PD7, GAIN7[2:0], SRB2, MUX7[2:0]] : Channel 7 active, 24 gain, no SRB2 & input shorted
|
18 |
+
0x61, # CH8SET [61] [PD8, GAIN8[2:0], SRB2, MUX8[2:0]] : Channel 8 active, 24 gain, no SRB2 & input shorted
|
19 |
+
0x00, # BIAS_SENSP [00] [BIASP8, BIASP7, BIASP6, BIASP5, BIASP4, BIASP3, BIASP2, BIASP1] : No bias
|
20 |
+
0x00, # BIAS_SENSN [00] [BIASN8, BIASN7, BIASN6, BIASN5, BIASN4, BIASN3, BIASN2, BIASN1] No bias
|
21 |
+
0x00, # LOFF_SENSP [00] [LOFFP8, LOFFP7, LOFFP6, LOFFP5, LOFFP4, LOFFP3, LOFFP2, LOFFP1] : No lead-off
|
22 |
+
0x00, # LOFF_SENSN [00] [LOFFM8, LOFFM7, LOFFM6, LOFFM5, LOFFM4, LOFFM3, LOFFM2, LOFFM1] : No lead-off
|
23 |
+
0x00, # LOFF_FLIP [00] [LOFF_FLIP8, LOFF_FLIP7, LOFF_FLIP6, LOFF_FLIP5, LOFF_FLIP4, LOFF_FLIP3, LOFF_FLIP2, LOFF_FLIP1] : No lead-off flip
|
24 |
+
# Lead-Off Status Registers (Read-Only Registers):
|
25 |
+
0x00, # LOFF_STATP [00] [IN8P_OFF, IN7P_OFF, IN6P_OFF, IN5P_OFF, IN4P_OFF, IN3P_OFF, IN2P_OFF, IN1P_OFF] : Lead-off positive status (RO)
|
26 |
+
0x00, # LOFF_STATN [00] [IN8M_OFF, IN7M_OFF, IN6M_OFF, IN5M_OFF, IN4M_OFF, IN3M_OFF, IN2M_OFF, IN1M_OFF] : Laed-off negative status (RO)
|
27 |
+
# GPIO and OTHER Registers:
|
28 |
+
0x0F, # GPIO [0F] [GPIOD[4:1], GPIOC[4:1]] : All GPIOs as inputs
|
29 |
+
0x00, # MISC1 [00] [0, 0, SRB1, 0, 0, 0, 0, 0] : Disable SRBM
|
30 |
+
0x00, # MISC2 [00] [00] : Unused
|
31 |
+
0x00, # CONFIG4 [00] [0, 0, 0, 0, SINGLE_SHOT, 0, PD_LOFF_COMP(bar), 0] : Single-shot, lead-off comparator disabled
|
32 |
+
]
|
33 |
+
|
34 |
+
FRONTEND_CONFIG = [
|
35 |
+
0x3E, # ID (RO)
|
36 |
+
0x95, # CONFIG1 [95] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 500 SPS
|
37 |
+
0xD0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]]
|
38 |
+
0xFC, # CONFIG3 [E0] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
39 |
+
0x00, # No lead-off
|
40 |
+
0x62, # CH1SET [60] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] set to measure BIAS signal
|
41 |
+
0x60, # CH2SET
|
42 |
+
0x60, # CH3SET
|
43 |
+
0x60, # CH4SET
|
44 |
+
0x60, # CH5SET
|
45 |
+
0x60, # CH6SET
|
46 |
+
0x60, # CH7SET
|
47 |
+
0x60, # CH8SET
|
48 |
+
0x00, # BIAS_SENSP 00
|
49 |
+
0x00, # BIAS_SENSN 00
|
50 |
+
0x00, # LOFF_SENSP Lead-off on all positive pins?
|
51 |
+
0x00, # LOFF_SENSN Lead-off on all negative pins?
|
52 |
+
0x00, # Normal lead-off
|
53 |
+
0x00, # Lead-off positive status (RO)
|
54 |
+
0x00, # Lead-off negative status (RO)
|
55 |
+
0x00, # All GPIOs as output ?
|
56 |
+
0x20, # Enable SRB1
|
57 |
+
]
|
58 |
+
|
59 |
+
|
60 |
+
LEADOFF_CONFIG = [
|
61 |
+
0x3E, # ID (RO)
|
62 |
+
0x95, # CONFIG1 [95] [1, DAISY_EN(bar), CLK_EN, 1, 0, DR[2:0]] : Datarate = 500 SPS
|
63 |
+
0xC0, # CONFIG2 [C0] [1, 1, 0, INT_CAL, 0, CAL_AMP0, CAL_FREQ[1:0]]
|
64 |
+
0xFC, # CONFIG3 [E0] [PD_REFBUF(bar), 1, 1, BIAS_MEAS, BIASREF_INT, PD_BIAS(bar), BIAS_LOFF_SENS, BIAS_STAT] : Power-down reference buffer, no bias
|
65 |
+
0x00, # No lead-off
|
66 |
+
0x60, # CH1SET [60] [PD1, GAIN1[2:0], SRB2, MUX1[2:0]] set to measure BIAS signal
|
67 |
+
0x60, # CH2SET
|
68 |
+
0x60, # CH3SET
|
69 |
+
0x60, # CH4SET
|
70 |
+
0x60, # CH5SET
|
71 |
+
0x60, # CH6SET
|
72 |
+
0x60, # CH7SET
|
73 |
+
0x60, # CH8SET
|
74 |
+
0x00, # BIAS_SENSP 00
|
75 |
+
0x00, # BIAS_SENSN 00
|
76 |
+
0xFF, # LOFF_SENSP Lead-off on all positive pins?
|
77 |
+
0xFF, # LOFF_SENSN Lead-off on all negative pins?
|
78 |
+
0x00, # Normal lead-off
|
79 |
+
0x00, # Lead-off positive status (RO)
|
80 |
+
0x00, # Lead-off negative status (RO)
|
81 |
+
0x00, # All GPIOs as output ?
|
82 |
+
0x20, # Enable SRB1
|
83 |
+
0x00,
|
84 |
+
0x02,
|
85 |
+
]
|
86 |
+
|
87 |
+
def to_ads_frequency(frequency):
|
88 |
+
possible_datarates = [250, 500, 1000, 2000, 4000, 8000, 16000]
|
89 |
+
dr = 16000
|
90 |
+
for i in possible_datarates:
|
91 |
+
if i >= frequency:
|
92 |
+
dr = i
|
93 |
+
break
|
94 |
+
return dr
|
95 |
+
|
96 |
+
def mod_config(config, datarate, channel_modes):
|
97 |
+
|
98 |
+
# datarate:
|
99 |
+
|
100 |
+
possible_datarates = [(250, 0x06),
|
101 |
+
(500, 0x05),
|
102 |
+
(1000, 0x04),
|
103 |
+
(2000, 0x03),
|
104 |
+
(4000, 0x02),
|
105 |
+
(8000, 0x01),
|
106 |
+
(16000, 0x00)]
|
107 |
+
mod_dr = 0x00
|
108 |
+
for i, j in possible_datarates:
|
109 |
+
if i >= datarate:
|
110 |
+
mod_dr = j
|
111 |
+
break
|
112 |
+
|
113 |
+
new_cf1 = config[1] & 0xF8
|
114 |
+
new_cf1 = new_cf1 | mod_dr
|
115 |
+
config[1] = new_cf1
|
116 |
+
|
117 |
+
# bias:
|
118 |
+
assert len(channel_modes) == 7
|
119 |
+
config[13] = 0x00 # clear BIAS_SENSP
|
120 |
+
config[14] = 0x00 # clear BIAS_SENSN
|
121 |
+
for chan_i, chan_mode in enumerate(channel_modes):
|
122 |
+
n = 6 + chan_i
|
123 |
+
mod = config[n] & 0x78 # clear PDn and MUX[2:0]
|
124 |
+
if chan_mode == 'simple':
|
125 |
+
# If channel is activated, we send the channel's output to the BIAS mechanism
|
126 |
+
bit_i = 1 << chan_i + 1
|
127 |
+
config[13] = config[13] | bit_i
|
128 |
+
config[14] = config[14] | bit_i
|
129 |
+
elif chan_mode == 'disabled':
|
130 |
+
mod = mod | 0x81 # PDn = 1 and input shorted (001)
|
131 |
+
else:
|
132 |
+
assert False, f"Wrong key: {chan_mode}."
|
133 |
+
config[n] = mod
|
134 |
+
# for n, c in enumerate(config): # print ADS1299 configuration registers
|
135 |
+
# print(f"config[{n}]:\t{c:08b}\t({hex(c)})")
|
136 |
+
return config
|
portiloop/{detection.py β src/detection.py}
RENAMED
@@ -39,7 +39,7 @@ class Detector(ABC):
|
|
39 |
|
40 |
# Example implementation for sleep spindles:
|
41 |
|
42 |
-
DEFAULT_MODEL_PATH = str(Path(__file__).parent / "models/portiloop_model_quant.tflite")
|
43 |
# print(DEFAULT_MODEL_PATH)
|
44 |
|
45 |
class SleepSpindleRealTimeDetector(Detector):
|
|
|
39 |
|
40 |
# Example implementation for sleep spindles:
|
41 |
|
42 |
+
DEFAULT_MODEL_PATH = str(Path(__file__).parent.parent / "models/portiloop_model_quant.tflite")
|
43 |
# print(DEFAULT_MODEL_PATH)
|
44 |
|
45 |
class SleepSpindleRealTimeDetector(Detector):
|
portiloop/{hardware β src/hardware}/__init__.py
RENAMED
File without changes
|
portiloop/{hardware β src/hardware}/frontend.py
RENAMED
File without changes
|
portiloop/{hardware β src/hardware}/leds.py
RENAMED
File without changes
|
portiloop/src/processing.py
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
from scipy.signal import firwin
|
3 |
+
|
4 |
+
|
5 |
+
def filter_24(value):
|
6 |
+
return (value * 4.5) / (2**23 - 1) / 24.0 * 1e6 # 23 because 1 bit is lost for sign
|
7 |
+
|
8 |
+
def filter_2scomplement_np(value):
|
9 |
+
return np.where((value & (1 << 23)) != 0, value - (1 << 24), value)
|
10 |
+
|
11 |
+
|
12 |
+
def int_to_float(value):
|
13 |
+
"""
|
14 |
+
Convert the int value out of the ADS into a value in microvolts
|
15 |
+
"""
|
16 |
+
return filter_24(filter_2scomplement_np(value))
|
17 |
+
|
18 |
+
|
19 |
+
def shift_numpy(arr, num, fill_value=np.nan):
|
20 |
+
result = np.empty_like(arr)
|
21 |
+
if num > 0:
|
22 |
+
result[:num] = fill_value
|
23 |
+
result[num:] = arr[:-num]
|
24 |
+
elif num < 0:
|
25 |
+
result[num:] = fill_value
|
26 |
+
result[:num] = arr[-num:]
|
27 |
+
else:
|
28 |
+
result[:] = arr
|
29 |
+
return result
|
30 |
+
|
31 |
+
|
32 |
+
class FIR:
|
33 |
+
def __init__(self, nb_channels, coefficients, buffer=None):
|
34 |
+
|
35 |
+
self.coefficients = np.expand_dims(np.array(coefficients), axis=1)
|
36 |
+
self.taps = len(self.coefficients)
|
37 |
+
self.nb_channels = nb_channels
|
38 |
+
self.buffer = np.array(buffer) if buffer is not None else np.zeros((self.taps, self.nb_channels))
|
39 |
+
|
40 |
+
def filter(self, x):
|
41 |
+
self.buffer = shift_numpy(self.buffer, 1, x)
|
42 |
+
filtered = np.sum(self.buffer * self.coefficients, axis=0)
|
43 |
+
return filtered
|
44 |
+
|
45 |
+
|
46 |
+
class FilterPipeline:
|
47 |
+
def __init__(self,
|
48 |
+
nb_channels,
|
49 |
+
sampling_rate,
|
50 |
+
power_line_fq=60,
|
51 |
+
use_custom_fir=False,
|
52 |
+
custom_fir_order=20,
|
53 |
+
custom_fir_cutoff=30,
|
54 |
+
alpha_avg=0.1,
|
55 |
+
alpha_std=0.001,
|
56 |
+
epsilon=0.000001,
|
57 |
+
filter_args=[]):
|
58 |
+
if len(filter_args) > 0:
|
59 |
+
use_fir, use_notch, use_std = filter_args
|
60 |
+
else:
|
61 |
+
use_fir=True,
|
62 |
+
use_notch=True,
|
63 |
+
use_std=True
|
64 |
+
self.use_fir = use_fir
|
65 |
+
self.use_notch = use_notch
|
66 |
+
self.use_std = use_std
|
67 |
+
self.nb_channels = nb_channels
|
68 |
+
assert power_line_fq in [50, 60], f"The only supported power line frequencies are 50 Hz and 60 Hz"
|
69 |
+
if power_line_fq == 60:
|
70 |
+
self.notch_coeff1 = -0.12478308884588535
|
71 |
+
self.notch_coeff2 = 0.98729186796473023
|
72 |
+
self.notch_coeff3 = 0.99364593398236511
|
73 |
+
self.notch_coeff4 = -0.12478308884588535
|
74 |
+
self.notch_coeff5 = 0.99364593398236511
|
75 |
+
else:
|
76 |
+
self.notch_coeff1 = -0.61410695998423581
|
77 |
+
self.notch_coeff2 = 0.98729186796473023
|
78 |
+
self.notch_coeff3 = 0.99364593398236511
|
79 |
+
self.notch_coeff4 = -0.61410695998423581
|
80 |
+
self.notch_coeff5 = 0.99364593398236511
|
81 |
+
self.dfs = [np.zeros(self.nb_channels), np.zeros(self.nb_channels)]
|
82 |
+
|
83 |
+
self.moving_average = None
|
84 |
+
self.moving_variance = np.zeros(self.nb_channels)
|
85 |
+
self.ALPHA_AVG = alpha_avg
|
86 |
+
self.ALPHA_STD = alpha_std
|
87 |
+
self.EPSILON = epsilon
|
88 |
+
|
89 |
+
if use_custom_fir:
|
90 |
+
self.fir_coef = firwin(numtaps=custom_fir_order+1, cutoff=custom_fir_cutoff, fs=sampling_rate)
|
91 |
+
else:
|
92 |
+
self.fir_coef = [
|
93 |
+
0.001623780150148094927192721215192250384,
|
94 |
+
0.014988684599373741992978104065059596905,
|
95 |
+
0.021287595318265635502275046064823982306,
|
96 |
+
0.007349500393709578957568417933998716762,
|
97 |
+
-0.025127515717112181709014251396183681209,
|
98 |
+
-0.052210507359822452833064687638398027048,
|
99 |
+
-0.039273839505489904766477593511808663607,
|
100 |
+
0.033021568427940004020193498490698402748,
|
101 |
+
0.147606943281569008563636202779889572412,
|
102 |
+
0.254000252034505602516389899392379447818,
|
103 |
+
0.297330876398883392486283128164359368384,
|
104 |
+
0.254000252034505602516389899392379447818,
|
105 |
+
0.147606943281569008563636202779889572412,
|
106 |
+
0.033021568427940004020193498490698402748,
|
107 |
+
-0.039273839505489904766477593511808663607,
|
108 |
+
-0.052210507359822452833064687638398027048,
|
109 |
+
-0.025127515717112181709014251396183681209,
|
110 |
+
0.007349500393709578957568417933998716762,
|
111 |
+
0.021287595318265635502275046064823982306,
|
112 |
+
0.014988684599373741992978104065059596905,
|
113 |
+
0.001623780150148094927192721215192250384]
|
114 |
+
self.fir = FIR(self.nb_channels, self.fir_coef)
|
115 |
+
|
116 |
+
def filter(self, value):
|
117 |
+
"""
|
118 |
+
value: a numpy array of shape (data series, channels)
|
119 |
+
"""
|
120 |
+
for i, x in enumerate(value): # loop over the data series
|
121 |
+
# FIR:
|
122 |
+
if self.use_fir:
|
123 |
+
x = self.fir.filter(x)
|
124 |
+
# notch:
|
125 |
+
if self.use_notch:
|
126 |
+
denAccum = (x - self.notch_coeff1 * self.dfs[0]) - self.notch_coeff2 * self.dfs[1]
|
127 |
+
x = (self.notch_coeff3 * denAccum + self.notch_coeff4 * self.dfs[0]) + self.notch_coeff5 * self.dfs[1]
|
128 |
+
self.dfs[1] = self.dfs[0]
|
129 |
+
self.dfs[0] = denAccum
|
130 |
+
# standardization:
|
131 |
+
if self.use_std:
|
132 |
+
if self.moving_average is not None:
|
133 |
+
delta = x - self.moving_average
|
134 |
+
self.moving_average = self.moving_average + self.ALPHA_AVG * delta
|
135 |
+
self.moving_variance = (1 - self.ALPHA_STD) * (self.moving_variance + self.ALPHA_STD * delta**2)
|
136 |
+
moving_std = np.sqrt(self.moving_variance)
|
137 |
+
x = (x - self.moving_average) / (moving_std + self.EPSILON)
|
138 |
+
else:
|
139 |
+
self.moving_average = x
|
140 |
+
value[i] = x
|
141 |
+
return value
|
portiloop/{stimulation.py β src/stimulation.py}
RENAMED
@@ -1,10 +1,12 @@
|
|
1 |
from abc import ABC, abstractmethod
|
|
|
2 |
import time
|
3 |
from threading import Thread, Lock
|
4 |
from pathlib import Path
|
5 |
import alsaaudio
|
6 |
import wave
|
7 |
import pylsl
|
|
|
8 |
|
9 |
|
10 |
# Abstract interface for developers:
|
@@ -32,13 +34,11 @@ class Stimulator(ABC):
|
|
32 |
|
33 |
class SleepSpindleRealTimeStimulator(Stimulator):
|
34 |
def __init__(self):
|
35 |
-
self._sound = Path(__file__).parent / 'sounds' / 'stimulus.wav'
|
36 |
print(f"DEBUG:{self._sound}")
|
37 |
self._thread = None
|
38 |
self._lock = Lock()
|
39 |
self.last_detected_ts = time.time()
|
40 |
-
self.wait_counter = 0
|
41 |
-
self.delayed = False
|
42 |
self.wait_t = 0.4 # 400 ms
|
43 |
|
44 |
lsl_markers_info = pylsl.StreamInfo(name='Portiloop_stimuli',
|
@@ -55,8 +55,6 @@ class SleepSpindleRealTimeStimulator(Stimulator):
|
|
55 |
|
56 |
self.lsl_outlet_markers = pylsl.StreamOutlet(lsl_markers_info)
|
57 |
self.lsl_outlet_markers_fast = pylsl.StreamOutlet(lsl_markers_info_fast)
|
58 |
-
|
59 |
-
self.delayer = None
|
60 |
|
61 |
# Initialize Alsa stuff
|
62 |
# Open WAV file and set PCM device
|
@@ -88,7 +86,7 @@ class SleepSpindleRealTimeStimulator(Stimulator):
|
|
88 |
while data:
|
89 |
self.wav_list.append(data)
|
90 |
data = f.readframes(self.periodsize)
|
91 |
-
|
92 |
def play_sound(self):
|
93 |
'''
|
94 |
Open the wav file and play a sound
|
@@ -98,39 +96,35 @@ class SleepSpindleRealTimeStimulator(Stimulator):
|
|
98 |
|
99 |
def stimulate(self, detection_signal):
|
100 |
for sig in detection_signal:
|
101 |
-
# We are waiting for a delayed stimulation
|
102 |
-
if self.delayed:
|
103 |
-
if self.wait_counter >= self.wait_time:
|
104 |
-
with self._lock:
|
105 |
-
if self._thread is None:
|
106 |
-
self._thread = Thread(target=self._t_sound, daemon=True)
|
107 |
-
self._thread.start()
|
108 |
-
self.delayed = False
|
109 |
-
else:
|
110 |
-
self.wait_counter += 1
|
111 |
# We detect a stimulation
|
112 |
-
|
113 |
# Record time of stimulation
|
114 |
-
self.lsl_outlet_markers_fast.push_sample(['FASTSTIM'])
|
115 |
ts = time.time()
|
116 |
|
117 |
-
#
|
118 |
-
if self.delayer is not None:
|
119 |
-
self.wait_time = self.delayer.stimulate()
|
120 |
-
self.delayed = True
|
121 |
-
self.wait_counter = 0
|
122 |
-
continue
|
123 |
-
|
124 |
-
# Stimulate if allowed
|
125 |
if ts - self.last_detected_ts > self.wait_t:
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
|
|
|
|
|
|
|
|
130 |
self.last_detected_ts = ts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
|
132 |
def _t_sound(self):
|
133 |
-
self.lsl_outlet_markers.push_sample(['STIM'])
|
134 |
self.play_sound()
|
135 |
with self._lock:
|
136 |
self._thread = None
|
@@ -140,6 +134,94 @@ class SleepSpindleRealTimeStimulator(Stimulator):
|
|
140 |
if self._thread is None:
|
141 |
self._thread = Thread(target=self._t_sound, daemon=True)
|
142 |
self._thread.start()
|
143 |
-
|
144 |
def add_delayer(self, delayer):
|
145 |
self.delayer = delayer
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
from abc import ABC, abstractmethod
|
2 |
+
from enum import Enum
|
3 |
import time
|
4 |
from threading import Thread, Lock
|
5 |
from pathlib import Path
|
6 |
import alsaaudio
|
7 |
import wave
|
8 |
import pylsl
|
9 |
+
from scipy.signal import find_peaks
|
10 |
|
11 |
|
12 |
# Abstract interface for developers:
|
|
|
34 |
|
35 |
class SleepSpindleRealTimeStimulator(Stimulator):
|
36 |
def __init__(self):
|
37 |
+
self._sound = Path(__file__).parent.parent / 'sounds' / 'stimulus.wav'
|
38 |
print(f"DEBUG:{self._sound}")
|
39 |
self._thread = None
|
40 |
self._lock = Lock()
|
41 |
self.last_detected_ts = time.time()
|
|
|
|
|
42 |
self.wait_t = 0.4 # 400 ms
|
43 |
|
44 |
lsl_markers_info = pylsl.StreamInfo(name='Portiloop_stimuli',
|
|
|
55 |
|
56 |
self.lsl_outlet_markers = pylsl.StreamOutlet(lsl_markers_info)
|
57 |
self.lsl_outlet_markers_fast = pylsl.StreamOutlet(lsl_markers_info_fast)
|
|
|
|
|
58 |
|
59 |
# Initialize Alsa stuff
|
60 |
# Open WAV file and set PCM device
|
|
|
86 |
while data:
|
87 |
self.wav_list.append(data)
|
88 |
data = f.readframes(self.periodsize)
|
89 |
+
|
90 |
def play_sound(self):
|
91 |
'''
|
92 |
Open the wav file and play a sound
|
|
|
96 |
|
97 |
def stimulate(self, detection_signal):
|
98 |
for sig in detection_signal:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
99 |
# We detect a stimulation
|
100 |
+
if sig:
|
101 |
# Record time of stimulation
|
|
|
102 |
ts = time.time()
|
103 |
|
104 |
+
# Check if time since last stimulation is long enough
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
if ts - self.last_detected_ts > self.wait_t:
|
106 |
+
if self.delayer is not None:
|
107 |
+
# If we have a delayer, notify it
|
108 |
+
self.delayer.detected()
|
109 |
+
# Send the LSL marer for the fast stimulation
|
110 |
+
self.send_stimulation("FAST_STIM", False)
|
111 |
+
else:
|
112 |
+
self.send_stimulation("STIM", True)
|
113 |
+
|
114 |
self.last_detected_ts = ts
|
115 |
+
|
116 |
+
def send_stimulation(self, lsl_text, sound):
|
117 |
+
# Send lsl stimulation
|
118 |
+
self.lsl_outlet_markers.push_sample([lsl_text])
|
119 |
+
# Send sound to patient
|
120 |
+
if sound:
|
121 |
+
with self._lock:
|
122 |
+
if self._thread is None:
|
123 |
+
self._thread = Thread(target=self._t_sound, daemon=True)
|
124 |
+
self._thread.start()
|
125 |
+
|
126 |
|
127 |
def _t_sound(self):
|
|
|
128 |
self.play_sound()
|
129 |
with self._lock:
|
130 |
self._thread = None
|
|
|
134 |
if self._thread is None:
|
135 |
self._thread = Thread(target=self._t_sound, daemon=True)
|
136 |
self._thread.start()
|
137 |
+
|
138 |
def add_delayer(self, delayer):
|
139 |
self.delayer = delayer
|
140 |
+
self.delayer.stimulate = lambda x: self.send_stimulation("DELAY_STIM", True)
|
141 |
+
|
142 |
+
# Class that delays stimulation to always stimulate peak or through
|
143 |
+
class UpStateDelayer:
|
144 |
+
def __init__(self, sample_freq, spindle_freq, peak, time_to_buffer):
|
145 |
+
'''
|
146 |
+
args:
|
147 |
+
sample_freq: int -> Sampling frequency of signal in Hz
|
148 |
+
time_to_wait: float -> Time to wait to build buffer in seconds
|
149 |
+
'''
|
150 |
+
# Get number of timesteps for a whole spindle
|
151 |
+
self.spindle_timesteps = (1/spindle_freq) * sample_freq # s *
|
152 |
+
self.sample_freq = sample_freq
|
153 |
+
self.buffer_size = 1.5 * self.spindle_timesteps
|
154 |
+
self.peak = peak
|
155 |
+
self.buffer = []
|
156 |
+
self.time_to_buffer = time_to_buffer
|
157 |
+
self.stimulate = None
|
158 |
+
|
159 |
+
self.state = States.NO_SPINDLE
|
160 |
+
|
161 |
+
def step(self, point):
|
162 |
+
'''
|
163 |
+
Step the delayer, ads a point to buffer if necessary.
|
164 |
+
Returns True if stimulation is actually done
|
165 |
+
'''
|
166 |
+
if self.state == States.NO_SPINDLE:
|
167 |
+
return False
|
168 |
+
elif self.state == States.BUFFERING:
|
169 |
+
self.buffer.append(point)
|
170 |
+
# If we are done buffering, move on to the waiting stage
|
171 |
+
if time.time() - self.time_started >= self.time_to_buffer:
|
172 |
+
# Compute the necessary time to wait
|
173 |
+
self.time_to_wait = self.compute_time_to_wait()
|
174 |
+
self.state = States.DELAYING
|
175 |
+
self.buffer = []
|
176 |
+
self.time_started = time.time()
|
177 |
+
return False
|
178 |
+
elif self.state == States.DELAYING:
|
179 |
+
# Check if we are done delaying
|
180 |
+
if time.time() - self.time_started >= self.time_to_wait():
|
181 |
+
# Actually stimulate the patient after the delay
|
182 |
+
if self.stimulate is not None:
|
183 |
+
self.stimulate()
|
184 |
+
# Reset state
|
185 |
+
self.time_to_wait = -1
|
186 |
+
self.state = States.NO_SPINDLE
|
187 |
+
return True
|
188 |
+
return False
|
189 |
+
|
190 |
+
def detected(self):
|
191 |
+
if self.state == States.NO_SPINDLE:
|
192 |
+
self.state = States.BUFFERING
|
193 |
+
self.time_started = time.time()
|
194 |
+
|
195 |
+
def compute_time_to_wait(self):
|
196 |
+
"""
|
197 |
+
Computes the time we want to wait in total based on the spindle frequency and the buffer
|
198 |
+
"""
|
199 |
+
# If we want to look at the valleys, we search for peaks on the inversed signal
|
200 |
+
if not self.peak:
|
201 |
+
self.buffer = -self.buffer
|
202 |
+
|
203 |
+
# Returns the index of the last peak in the buffer
|
204 |
+
peaks, _ = find_peaks(self.buffer, prominence=1)
|
205 |
+
|
206 |
+
# Compute the time until next peak and return it
|
207 |
+
return (len(self.buffer) - peaks[-1]) * (1 / self.sample_freq)
|
208 |
+
|
209 |
+
class States(Enum):
|
210 |
+
NO_SPINDLE = 0
|
211 |
+
BUFFERING = 1
|
212 |
+
DELAYING = 2
|
213 |
+
|
214 |
+
|
215 |
+
if __name__ == "__main__":
|
216 |
+
import numpy as np
|
217 |
+
import matplotlib.pyplot as plt
|
218 |
+
|
219 |
+
freq = 250
|
220 |
+
spindle_freq = 10
|
221 |
+
time = 10
|
222 |
+
x = np.linspace(0, time * np.pi, num=time*freq)
|
223 |
+
n = np.random.normal(scale=1, size=x.size)
|
224 |
+
y = np.sin(x) + n
|
225 |
+
plt.plot(x, y)
|
226 |
+
plt.show()
|
227 |
+
|
portiloop/src/utils.py
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from EDFlib.edfwriter import EDFwriter
|
2 |
+
from portilooplot.jupyter_plot import ProgressPlot
|
3 |
+
from pathlib import Path
|
4 |
+
import numpy as np
|
5 |
+
|
6 |
+
EDF_PATH = Path.home() / 'workspace' / 'edf_recording'
|
7 |
+
|
8 |
+
|
9 |
+
|
10 |
+
class DummyAlsaMixer:
|
11 |
+
def __init__(self):
|
12 |
+
self.volume = 50
|
13 |
+
|
14 |
+
def getvolume(self):
|
15 |
+
return [self.volume]
|
16 |
+
|
17 |
+
def setvolume(self, volume):
|
18 |
+
self.volume = volume
|
19 |
+
|
20 |
+
|
21 |
+
class EDFRecorder:
|
22 |
+
def __init__(self, signal_labels):
|
23 |
+
self.filename = EDF_PATH / 'recording.edf'
|
24 |
+
self.nb_signals = 8
|
25 |
+
self.samples_per_datarecord_array = self.frequency
|
26 |
+
self.physical_max = 5
|
27 |
+
self.physical_min = -5
|
28 |
+
self.signal_labels = signal_labels
|
29 |
+
|
30 |
+
def open_recording_file(self):
|
31 |
+
nb_signals = self.nb_signals
|
32 |
+
samples_per_datarecord_array = self.samples_per_datarecord_array
|
33 |
+
physical_max = self.physical_max
|
34 |
+
physical_min = self.physical_min
|
35 |
+
signal_labels = self.signal_labels
|
36 |
+
|
37 |
+
print(f"Will store edf recording in {self.filename}")
|
38 |
+
|
39 |
+
self.edf_writer = EDFwriter(p_path=str(self.filename),
|
40 |
+
f_file_type=EDFwriter.EDFLIB_FILETYPE_EDFPLUS,
|
41 |
+
number_of_signals=nb_signals)
|
42 |
+
|
43 |
+
for signal in range(nb_signals):
|
44 |
+
assert self.edf_writer.setSampleFrequency(signal, samples_per_datarecord_array) == 0
|
45 |
+
assert self.edf_writer.setPhysicalMaximum(signal, physical_max) == 0
|
46 |
+
assert self.edf_writer.setPhysicalMinimum(signal, physical_min) == 0
|
47 |
+
assert self.edf_writer.setDigitalMaximum(signal, 32767) == 0
|
48 |
+
assert self.edf_writer.setDigitalMinimum(signal, -32768) == 0
|
49 |
+
assert self.edf_writer.setSignalLabel(signal, signal_labels[signal]) == 0
|
50 |
+
assert self.edf_writer.setPhysicalDimension(signal, 'V') == 0
|
51 |
+
|
52 |
+
def close_recording_file(self):
|
53 |
+
assert self.edf_writer.close() == 0
|
54 |
+
|
55 |
+
def add_recording_data(self, data):
|
56 |
+
self.edf_buffer += data
|
57 |
+
if len(self.edf_buffer) >= self.samples_per_datarecord_array:
|
58 |
+
datarecord_array = self.edf_buffer[:self.samples_per_datarecord_array]
|
59 |
+
self.edf_buffer = self.edf_buffer[self.samples_per_datarecord_array:]
|
60 |
+
datarecord_array = np.array(datarecord_array).transpose()
|
61 |
+
assert len(datarecord_array) == self.nb_signals, f"len(data)={len(data)}!={self.nb_signals}"
|
62 |
+
for d in datarecord_array:
|
63 |
+
assert len(d) == self.samples_per_datarecord_array, f"{len(d)}!={self.samples_per_datarecord_array}"
|
64 |
+
assert self.edf_writer.writeSamples(d) == 0
|
65 |
+
|
66 |
+
|
67 |
+
class LiveDisplay():
|
68 |
+
def __init__(self, channel_names, window_len=100):
|
69 |
+
self.datapoint_dim = len(channel_names)
|
70 |
+
self.history = []
|
71 |
+
self.pp = ProgressPlot(plot_names=channel_names, max_window_len=window_len)
|
72 |
+
self.matplotlib = False
|
73 |
+
|
74 |
+
def add_datapoints(self, datapoints):
|
75 |
+
"""
|
76 |
+
Adds 8 lists of datapoints to the plot
|
77 |
+
|
78 |
+
Args:
|
79 |
+
datapoints: list of 8 lists of floats (or list of 8 floats)
|
80 |
+
"""
|
81 |
+
if self.matplotlib:
|
82 |
+
import matplotlib.pyplot as plt
|
83 |
+
disp_list = []
|
84 |
+
for datapoint in datapoints:
|
85 |
+
d = [[elt] for elt in datapoint]
|
86 |
+
disp_list.append(d)
|
87 |
+
|
88 |
+
if self.matplotlib:
|
89 |
+
self.history += d[1]
|
90 |
+
|
91 |
+
if not self.matplotlib:
|
92 |
+
self.pp.update_with_datapoints(disp_list)
|
93 |
+
elif len(self.history) == 1000:
|
94 |
+
plt.plot(self.history)
|
95 |
+
plt.show()
|
96 |
+
self.history = []
|
97 |
+
|
98 |
+
def add_datapoint(self, datapoint):
|
99 |
+
disp_list = [[elt] for elt in datapoint]
|
100 |
+
self.pp.update(disp_list)
|
101 |
+
|
102 |
+
|
103 |
+
class FileReader:
|
104 |
+
def __init__(self, filename):
|
105 |
+
raise NotImplementedError
|
106 |
+
|
107 |
+
def get_point(self):
|
108 |
+
raise NotImplementedError
|