accusleepy 0.6.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.
- accusleepy/__init__.py +0 -0
- accusleepy/__main__.py +4 -0
- accusleepy/bouts.py +142 -0
- accusleepy/brain_state_set.py +89 -0
- accusleepy/classification.py +285 -0
- accusleepy/config.json +24 -0
- accusleepy/constants.py +46 -0
- accusleepy/fileio.py +179 -0
- accusleepy/gui/__init__.py +0 -0
- accusleepy/gui/icons/brightness_down.png +0 -0
- accusleepy/gui/icons/brightness_up.png +0 -0
- accusleepy/gui/icons/double_down_arrow.png +0 -0
- accusleepy/gui/icons/double_up_arrow.png +0 -0
- accusleepy/gui/icons/down_arrow.png +0 -0
- accusleepy/gui/icons/home.png +0 -0
- accusleepy/gui/icons/question.png +0 -0
- accusleepy/gui/icons/save.png +0 -0
- accusleepy/gui/icons/up_arrow.png +0 -0
- accusleepy/gui/icons/zoom_in.png +0 -0
- accusleepy/gui/icons/zoom_out.png +0 -0
- accusleepy/gui/images/primary_window.png +0 -0
- accusleepy/gui/images/viewer_window.png +0 -0
- accusleepy/gui/images/viewer_window_annotated.png +0 -0
- accusleepy/gui/main.py +1494 -0
- accusleepy/gui/manual_scoring.py +1096 -0
- accusleepy/gui/mplwidget.py +386 -0
- accusleepy/gui/primary_window.py +2577 -0
- accusleepy/gui/primary_window.ui +3831 -0
- accusleepy/gui/resources.qrc +16 -0
- accusleepy/gui/resources_rc.py +6710 -0
- accusleepy/gui/text/config_guide.txt +27 -0
- accusleepy/gui/text/main_guide.md +167 -0
- accusleepy/gui/text/manual_scoring_guide.md +23 -0
- accusleepy/gui/viewer_window.py +610 -0
- accusleepy/gui/viewer_window.ui +926 -0
- accusleepy/models.py +108 -0
- accusleepy/multitaper.py +661 -0
- accusleepy/signal_processing.py +469 -0
- accusleepy/temperature_scaling.py +157 -0
- accusleepy-0.6.0.dist-info/METADATA +106 -0
- accusleepy-0.6.0.dist-info/RECORD +42 -0
- accusleepy-0.6.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Widget with a matplotlib FigureCanvas for manual scoring
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
import matplotlib.ticker as mticker
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib.backend_bases import MouseButton
|
|
7
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
|
8
|
+
from matplotlib.figure import Figure
|
|
9
|
+
from matplotlib.gridspec import GridSpec
|
|
10
|
+
from matplotlib.patches import Rectangle
|
|
11
|
+
from matplotlib.widgets import RectangleSelector
|
|
12
|
+
|
|
13
|
+
# from PySide6.QtWidgets import *
|
|
14
|
+
from PySide6 import QtWidgets
|
|
15
|
+
|
|
16
|
+
from accusleepy.brain_state_set import BrainStateSet
|
|
17
|
+
|
|
18
|
+
# upper limit of spectrogram y-axis, in Hz
|
|
19
|
+
SPEC_UPPER_F = 30
|
|
20
|
+
# interval of spectrogram y-axis ticks, in Hz
|
|
21
|
+
SPEC_Y_TICK_INTERVAL = 10
|
|
22
|
+
|
|
23
|
+
# margins around subplots in the figure
|
|
24
|
+
SUBPLOT_TOP_MARGIN = 0.98
|
|
25
|
+
SUBPLOT_BOTTOM_MARGIN = 0.02
|
|
26
|
+
SUBPLOT_LEFT_MARGIN = 0.07
|
|
27
|
+
SUBPLOT_RIGHT_MARGIN = 0.95
|
|
28
|
+
|
|
29
|
+
# maximum number of x-axis ticks to show on the lower plot
|
|
30
|
+
MAX_LOWER_X_TICK_N = 7
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MplWidget(QtWidgets.QWidget):
|
|
34
|
+
"""Widget that displays a matplotlib FigureCanvas"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent=None):
|
|
37
|
+
QtWidgets.QWidget.__init__(self, parent)
|
|
38
|
+
# set up the canvas and store a reference to its axes
|
|
39
|
+
self.canvas = FigureCanvas(Figure())
|
|
40
|
+
self.canvas.axes = None
|
|
41
|
+
|
|
42
|
+
# set the widget layout and remove the margins
|
|
43
|
+
vertical_layout = QtWidgets.QVBoxLayout()
|
|
44
|
+
vertical_layout.addWidget(self.canvas)
|
|
45
|
+
vertical_layout.setContentsMargins(0, 0, 0, 0)
|
|
46
|
+
self.setLayout(vertical_layout)
|
|
47
|
+
|
|
48
|
+
# given during the setup process
|
|
49
|
+
self.epoch_length = None
|
|
50
|
+
|
|
51
|
+
# upper plot references
|
|
52
|
+
self.upper_marker = None
|
|
53
|
+
self.label_img_ref = None
|
|
54
|
+
self.spec_ref = None
|
|
55
|
+
self.roi = None
|
|
56
|
+
self.editing_patch = None
|
|
57
|
+
self.roi_patch = None
|
|
58
|
+
|
|
59
|
+
# lower plot references
|
|
60
|
+
self.eeg_line = None
|
|
61
|
+
self.emg_line = None
|
|
62
|
+
self.top_marker = None
|
|
63
|
+
self.bottom_marker = None
|
|
64
|
+
|
|
65
|
+
def setup_upper_figure(
|
|
66
|
+
self,
|
|
67
|
+
n_epochs: int,
|
|
68
|
+
label_img: np.array,
|
|
69
|
+
confidence_scores: np.array,
|
|
70
|
+
confidence_img: np.array,
|
|
71
|
+
spec: np.array,
|
|
72
|
+
f: np.array,
|
|
73
|
+
emg: np.array,
|
|
74
|
+
epochs_to_show: int,
|
|
75
|
+
label_display_options: np.array,
|
|
76
|
+
brain_state_set: BrainStateSet,
|
|
77
|
+
roi_function: Callable,
|
|
78
|
+
):
|
|
79
|
+
"""Initialize upper FigureCanvas for the manual scoring GUI
|
|
80
|
+
|
|
81
|
+
:param n_epochs: number of epochs in the recording
|
|
82
|
+
:param label_img: brain state labels, as an image
|
|
83
|
+
:param confidence_scores: confidence scores
|
|
84
|
+
:param confidence_img: confidence scores, as an image
|
|
85
|
+
:param spec: EEG spectrogram image
|
|
86
|
+
:param f: EEG spectrogram frequency axis
|
|
87
|
+
:param emg: EMG RMS per epoch
|
|
88
|
+
:param epochs_to_show: number of epochs to show in the lower plot
|
|
89
|
+
:param label_display_options: valid brain state y-axis locations
|
|
90
|
+
:param brain_state_set: set of brain states options
|
|
91
|
+
:param roi_function: callback for ROI selection
|
|
92
|
+
"""
|
|
93
|
+
# references to parts of the epoch marker
|
|
94
|
+
self.upper_marker = list()
|
|
95
|
+
|
|
96
|
+
# subplot layout
|
|
97
|
+
height_ratios = [2, 8, 2, 12, 13]
|
|
98
|
+
gs1 = GridSpec(5, 1, hspace=0.02, height_ratios=height_ratios)
|
|
99
|
+
gs2 = GridSpec(5, 1, hspace=0.4, height_ratios=height_ratios)
|
|
100
|
+
axes = list()
|
|
101
|
+
axes.append(self.canvas.figure.add_subplot(gs1[0]))
|
|
102
|
+
axes.append(self.canvas.figure.add_subplot(gs1[1]))
|
|
103
|
+
axes.append(self.canvas.figure.add_subplot(gs1[2]))
|
|
104
|
+
axes.append(self.canvas.figure.add_subplot(gs1[3]))
|
|
105
|
+
axes.append(self.canvas.figure.add_subplot(gs2[4]))
|
|
106
|
+
|
|
107
|
+
# subplots have different axes limits
|
|
108
|
+
for i in [0, 1, 2, 4]:
|
|
109
|
+
axes[i].set_xlim((-0.5, n_epochs - 0.5))
|
|
110
|
+
axes[3].set_xlim(0, n_epochs)
|
|
111
|
+
|
|
112
|
+
# confidence score subplot
|
|
113
|
+
if confidence_scores is None:
|
|
114
|
+
axes[0].set_visible(False)
|
|
115
|
+
else:
|
|
116
|
+
axes[0].set_ylim([-0.5, 0.5])
|
|
117
|
+
axes[0].set_xticks([])
|
|
118
|
+
axes[0].set_yticks([0])
|
|
119
|
+
axes[0].set_yticklabels(["Conf."])
|
|
120
|
+
axes[0].tick_params(axis="y", color="white")
|
|
121
|
+
axes[0].imshow(
|
|
122
|
+
confidence_img, aspect="auto", origin="lower", interpolation="None"
|
|
123
|
+
)
|
|
124
|
+
confidence_x = (
|
|
125
|
+
np.repeat(list(range(len(confidence_scores) + 1)), 2)[1:-1] - 0.5
|
|
126
|
+
)
|
|
127
|
+
confidence_y = np.repeat(confidence_scores, 2) - 0.5
|
|
128
|
+
axes[0].plot(confidence_x, confidence_y, "k", linewidth=0.5)
|
|
129
|
+
for side in ["left", "right", "bottom", "top"]:
|
|
130
|
+
axes[0].spines[side].set_visible(False)
|
|
131
|
+
|
|
132
|
+
# brain state subplot
|
|
133
|
+
axes[1].set_ylim(
|
|
134
|
+
[-0.5, np.max(label_display_options) - np.min(label_display_options) + 0.5]
|
|
135
|
+
)
|
|
136
|
+
axes[1].set_xticks([])
|
|
137
|
+
axes[1].set_yticks(
|
|
138
|
+
label_display_options - np.min(label_display_options),
|
|
139
|
+
)
|
|
140
|
+
axes[1].set_yticklabels([b.name for b in brain_state_set.brain_states])
|
|
141
|
+
ax2 = axes[1].secondary_yaxis("right")
|
|
142
|
+
ax2.set_yticks(
|
|
143
|
+
label_display_options - np.min(label_display_options),
|
|
144
|
+
)
|
|
145
|
+
ax2.set_yticklabels([b.digit for b in brain_state_set.brain_states])
|
|
146
|
+
self.label_img_ref = axes[1].imshow(
|
|
147
|
+
label_img, aspect="auto", origin="lower", interpolation="None"
|
|
148
|
+
)
|
|
149
|
+
# add patch to dim the display when creating an ROI
|
|
150
|
+
self.editing_patch = axes[1].add_patch(
|
|
151
|
+
Rectangle(
|
|
152
|
+
xy=(-0.5, -0.5),
|
|
153
|
+
width=n_epochs,
|
|
154
|
+
height=np.max(label_display_options)
|
|
155
|
+
- np.min(label_display_options)
|
|
156
|
+
+ 1,
|
|
157
|
+
color="white",
|
|
158
|
+
edgecolor=None,
|
|
159
|
+
alpha=0.4,
|
|
160
|
+
visible=False,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
# add the ROI selection widget, but disable it until it's needed
|
|
164
|
+
self.roi = RectangleSelector(
|
|
165
|
+
ax=axes[1],
|
|
166
|
+
onselect=roi_function,
|
|
167
|
+
interactive=False,
|
|
168
|
+
button=MouseButton(1),
|
|
169
|
+
)
|
|
170
|
+
self.roi.set_active(False)
|
|
171
|
+
# keep a reference to the ROI patch so we can change its color later
|
|
172
|
+
# index 0 is the "editing_patch" created earlier
|
|
173
|
+
self.roi_patch = [c for c in axes[1].get_children() if type(c) is Rectangle][1]
|
|
174
|
+
|
|
175
|
+
# epoch marker subplot
|
|
176
|
+
axes[2].set_ylim((0, 1))
|
|
177
|
+
axes[2].axis("off")
|
|
178
|
+
self.upper_marker.append(
|
|
179
|
+
axes[2].plot([-0.5, epochs_to_show - 0.5], [0.5, 0.5], "r")[0]
|
|
180
|
+
)
|
|
181
|
+
self.upper_marker.append(axes[2].plot([0], [0.5], "rD")[0])
|
|
182
|
+
|
|
183
|
+
# EEG spectrogram subplot
|
|
184
|
+
# select subset of frequencies to show
|
|
185
|
+
f = f[f <= SPEC_UPPER_F]
|
|
186
|
+
spec = spec[0 : len(f), :]
|
|
187
|
+
axes[3].set_ylabel("EEG", rotation="horizontal", ha="right")
|
|
188
|
+
axes[3].set_yticks(
|
|
189
|
+
np.linspace(
|
|
190
|
+
0,
|
|
191
|
+
len(f),
|
|
192
|
+
1 + round(SPEC_UPPER_F / SPEC_Y_TICK_INTERVAL),
|
|
193
|
+
),
|
|
194
|
+
)
|
|
195
|
+
axes[3].set_yticklabels(
|
|
196
|
+
np.arange(0, SPEC_UPPER_F + SPEC_Y_TICK_INTERVAL, SPEC_Y_TICK_INTERVAL)
|
|
197
|
+
)
|
|
198
|
+
axes[3].tick_params(axis="both", which="major", labelsize=8)
|
|
199
|
+
axes[3].xaxis.set_major_formatter(mticker.FuncFormatter(self.time_formatter))
|
|
200
|
+
self.spec_ref = axes[3].imshow(
|
|
201
|
+
spec,
|
|
202
|
+
vmin=np.percentile(spec, 2),
|
|
203
|
+
vmax=np.percentile(spec, 98),
|
|
204
|
+
aspect="auto",
|
|
205
|
+
origin="lower",
|
|
206
|
+
interpolation="None",
|
|
207
|
+
extent=(
|
|
208
|
+
0,
|
|
209
|
+
n_epochs,
|
|
210
|
+
-0.5,
|
|
211
|
+
len(f) + 0.5,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# EMG subplot
|
|
216
|
+
axes[4].set_xticks([])
|
|
217
|
+
axes[4].set_yticks([])
|
|
218
|
+
axes[4].set_ylabel("EMG", rotation="horizontal", ha="right")
|
|
219
|
+
axes[4].plot(
|
|
220
|
+
emg,
|
|
221
|
+
"k",
|
|
222
|
+
linewidth=0.5,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
self.canvas.figure.subplots_adjust(
|
|
226
|
+
left=SUBPLOT_LEFT_MARGIN,
|
|
227
|
+
right=SUBPLOT_RIGHT_MARGIN,
|
|
228
|
+
top=SUBPLOT_TOP_MARGIN,
|
|
229
|
+
bottom=SUBPLOT_BOTTOM_MARGIN,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
self.canvas.axes = axes
|
|
233
|
+
|
|
234
|
+
def setup_lower_figure(
|
|
235
|
+
self,
|
|
236
|
+
label_img: np.array,
|
|
237
|
+
sampling_rate: int | float,
|
|
238
|
+
epochs_to_show: int,
|
|
239
|
+
brain_state_set: BrainStateSet,
|
|
240
|
+
label_display_options: np.array,
|
|
241
|
+
):
|
|
242
|
+
"""Initialize lower FigureCanvas for the manual scoring GUI
|
|
243
|
+
|
|
244
|
+
:param label_img: brain state labels, as an image
|
|
245
|
+
:param sampling_rate: EEG/EMG sampling rate, in Hz
|
|
246
|
+
:param epochs_to_show: number of epochs to show in the lower plot
|
|
247
|
+
:param brain_state_set: set of brain states options
|
|
248
|
+
:param label_display_options: valid brain state y-axis locations
|
|
249
|
+
"""
|
|
250
|
+
# number of samples in one epoch
|
|
251
|
+
# we can expect this to be very close to an integer
|
|
252
|
+
samples_per_epoch = round(sampling_rate * self.epoch_length)
|
|
253
|
+
# number of EEG/EMG samples to plot
|
|
254
|
+
samples_shown = round(samples_per_epoch * epochs_to_show)
|
|
255
|
+
|
|
256
|
+
# references to parts of the epoch markers
|
|
257
|
+
self.top_marker = list()
|
|
258
|
+
self.bottom_marker = list()
|
|
259
|
+
# epoch marker display parameters
|
|
260
|
+
marker_dy = 0.25
|
|
261
|
+
marker_y_offset_top = 0.02
|
|
262
|
+
marker_y_offset_bottom = 0.01
|
|
263
|
+
|
|
264
|
+
# subplot layout
|
|
265
|
+
gs1 = GridSpec(3, 1, hspace=0)
|
|
266
|
+
gs2 = GridSpec(3, 1, hspace=0.5)
|
|
267
|
+
axes = list()
|
|
268
|
+
axes.append(self.canvas.figure.add_subplot(gs1[0]))
|
|
269
|
+
axes.append(self.canvas.figure.add_subplot(gs1[1]))
|
|
270
|
+
axes.append(self.canvas.figure.add_subplot(gs2[2]))
|
|
271
|
+
|
|
272
|
+
# EEG subplot
|
|
273
|
+
axes[0].set_xticks([])
|
|
274
|
+
axes[0].set_yticks([])
|
|
275
|
+
axes[0].set_xlim((0, samples_shown))
|
|
276
|
+
axes[0].set_ylim((-1, 1))
|
|
277
|
+
axes[0].set_ylabel("EEG", rotation="horizontal", ha="right")
|
|
278
|
+
self.eeg_line = axes[0].plot(
|
|
279
|
+
np.zeros(samples_shown),
|
|
280
|
+
"k",
|
|
281
|
+
linewidth=0.5,
|
|
282
|
+
)[0]
|
|
283
|
+
# top epoch marker
|
|
284
|
+
marker_x = [
|
|
285
|
+
[0, 0],
|
|
286
|
+
[0, samples_per_epoch],
|
|
287
|
+
[samples_per_epoch, samples_per_epoch],
|
|
288
|
+
]
|
|
289
|
+
marker_y = np.array(
|
|
290
|
+
[
|
|
291
|
+
[1 - marker_dy, 1],
|
|
292
|
+
[1, 1],
|
|
293
|
+
[1 - marker_dy, 1],
|
|
294
|
+
]
|
|
295
|
+
)
|
|
296
|
+
for x, y in zip(marker_x, marker_y):
|
|
297
|
+
self.top_marker.append(axes[0].plot(x, y - marker_y_offset_top, "r")[0])
|
|
298
|
+
|
|
299
|
+
# EMG subplot
|
|
300
|
+
axes[1].set_xticks(
|
|
301
|
+
resample_x_ticks(
|
|
302
|
+
np.arange(
|
|
303
|
+
0,
|
|
304
|
+
samples_shown,
|
|
305
|
+
samples_per_epoch,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
axes[1].tick_params(axis="x", which="major", labelsize=8)
|
|
310
|
+
axes[1].set_yticks([])
|
|
311
|
+
axes[1].set_xlim((0, samples_shown))
|
|
312
|
+
axes[1].set_ylim((-1, 1))
|
|
313
|
+
axes[1].set_ylabel("EMG", rotation="horizontal", ha="right")
|
|
314
|
+
self.emg_line = axes[1].plot(
|
|
315
|
+
np.zeros(samples_shown),
|
|
316
|
+
"k",
|
|
317
|
+
linewidth=0.5,
|
|
318
|
+
)[0]
|
|
319
|
+
|
|
320
|
+
for x, y in zip(marker_x, marker_y):
|
|
321
|
+
self.bottom_marker.append(
|
|
322
|
+
axes[1].plot(x, -1 * (y - marker_y_offset_bottom), "r")[0]
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# brain state subplot
|
|
326
|
+
axes[2].set_xticks([])
|
|
327
|
+
axes[2].set_yticks(
|
|
328
|
+
label_display_options - np.min(label_display_options),
|
|
329
|
+
)
|
|
330
|
+
axes[2].set_yticklabels([b.name for b in brain_state_set.brain_states])
|
|
331
|
+
ax2 = axes[2].secondary_yaxis("right")
|
|
332
|
+
ax2.set_yticks(
|
|
333
|
+
label_display_options - np.min(label_display_options),
|
|
334
|
+
)
|
|
335
|
+
ax2.set_yticklabels([b.digit for b in brain_state_set.brain_states])
|
|
336
|
+
axes[2].set_xlim((-0.5, epochs_to_show - 0.5))
|
|
337
|
+
axes[2].set_ylim(
|
|
338
|
+
[-0.5, np.max(label_display_options) - np.min(label_display_options) + 0.5]
|
|
339
|
+
)
|
|
340
|
+
self.label_img_ref = axes[2].imshow(
|
|
341
|
+
label_img[:, 0:epochs_to_show, :],
|
|
342
|
+
aspect="auto",
|
|
343
|
+
origin="lower",
|
|
344
|
+
interpolation="None",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
self.canvas.figure.subplots_adjust(
|
|
348
|
+
left=SUBPLOT_LEFT_MARGIN,
|
|
349
|
+
right=SUBPLOT_RIGHT_MARGIN,
|
|
350
|
+
top=SUBPLOT_TOP_MARGIN,
|
|
351
|
+
bottom=SUBPLOT_BOTTOM_MARGIN,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
self.canvas.axes = axes
|
|
355
|
+
|
|
356
|
+
def time_formatter(self, x, pos):
|
|
357
|
+
x = x * self.epoch_length
|
|
358
|
+
return "{:02d}:{:02d}:{:05.2f}".format(
|
|
359
|
+
int(x // 3600), int(x // 60) % 60, (x % 60)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def resample_x_ticks(x_ticks: np.array) -> np.array:
|
|
364
|
+
"""Choose a subset of x_ticks to display
|
|
365
|
+
|
|
366
|
+
The x-axis can get crowded if there are too many timestamps shown.
|
|
367
|
+
This function finds a subset of evenly spaced x-axis ticks that
|
|
368
|
+
includes the one at the beginning of the central epoch.
|
|
369
|
+
|
|
370
|
+
:param x_ticks: full set of x_ticks
|
|
371
|
+
:return: smaller subset of x_ticks
|
|
372
|
+
"""
|
|
373
|
+
if len(x_ticks) <= MAX_LOWER_X_TICK_N:
|
|
374
|
+
return x_ticks
|
|
375
|
+
|
|
376
|
+
# number of ticks to the left of the central epoch
|
|
377
|
+
# this will always be an integer
|
|
378
|
+
nl = round((len(x_ticks) - 1) / 2)
|
|
379
|
+
|
|
380
|
+
# search for even tick spacings that include the central epoch
|
|
381
|
+
# if necessary, skip the leftmost tick
|
|
382
|
+
for offset in [0, 1]:
|
|
383
|
+
if (nl - offset) % 3 == 0:
|
|
384
|
+
return x_ticks[offset :: round((nl - offset) / 3)]
|
|
385
|
+
elif (nl - offset) % 2 == 0:
|
|
386
|
+
return x_ticks[offset :: round((nl - offset) / 2)]
|