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.
Files changed (42) hide show
  1. accusleepy/__init__.py +0 -0
  2. accusleepy/__main__.py +4 -0
  3. accusleepy/bouts.py +142 -0
  4. accusleepy/brain_state_set.py +89 -0
  5. accusleepy/classification.py +285 -0
  6. accusleepy/config.json +24 -0
  7. accusleepy/constants.py +46 -0
  8. accusleepy/fileio.py +179 -0
  9. accusleepy/gui/__init__.py +0 -0
  10. accusleepy/gui/icons/brightness_down.png +0 -0
  11. accusleepy/gui/icons/brightness_up.png +0 -0
  12. accusleepy/gui/icons/double_down_arrow.png +0 -0
  13. accusleepy/gui/icons/double_up_arrow.png +0 -0
  14. accusleepy/gui/icons/down_arrow.png +0 -0
  15. accusleepy/gui/icons/home.png +0 -0
  16. accusleepy/gui/icons/question.png +0 -0
  17. accusleepy/gui/icons/save.png +0 -0
  18. accusleepy/gui/icons/up_arrow.png +0 -0
  19. accusleepy/gui/icons/zoom_in.png +0 -0
  20. accusleepy/gui/icons/zoom_out.png +0 -0
  21. accusleepy/gui/images/primary_window.png +0 -0
  22. accusleepy/gui/images/viewer_window.png +0 -0
  23. accusleepy/gui/images/viewer_window_annotated.png +0 -0
  24. accusleepy/gui/main.py +1494 -0
  25. accusleepy/gui/manual_scoring.py +1096 -0
  26. accusleepy/gui/mplwidget.py +386 -0
  27. accusleepy/gui/primary_window.py +2577 -0
  28. accusleepy/gui/primary_window.ui +3831 -0
  29. accusleepy/gui/resources.qrc +16 -0
  30. accusleepy/gui/resources_rc.py +6710 -0
  31. accusleepy/gui/text/config_guide.txt +27 -0
  32. accusleepy/gui/text/main_guide.md +167 -0
  33. accusleepy/gui/text/manual_scoring_guide.md +23 -0
  34. accusleepy/gui/viewer_window.py +610 -0
  35. accusleepy/gui/viewer_window.ui +926 -0
  36. accusleepy/models.py +108 -0
  37. accusleepy/multitaper.py +661 -0
  38. accusleepy/signal_processing.py +469 -0
  39. accusleepy/temperature_scaling.py +157 -0
  40. accusleepy-0.6.0.dist-info/METADATA +106 -0
  41. accusleepy-0.6.0.dist-info/RECORD +42 -0
  42. 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)]