misleep 0.3.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.
- misleep/__init__.py +111 -0
- misleep/__main__.py +16 -0
- misleep/analysis/__init__.py +23 -0
- misleep/analysis/auto_stage.py +158 -0
- misleep/analysis/detection.py +198 -0
- misleep/analysis/features.py +131 -0
- misleep/analysis/models/P30_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/P30_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/__init__.py +0 -0
- misleep/analysis/models/ado_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/ado_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_F_lightgbm.pkl +0 -0
- misleep/analysis/models/adult_EEG_P_lightgbm.pkl +0 -0
- misleep/analysis/transformer/__init__.py +46 -0
- misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
- misleep/analysis/transformer/checkpoints/__init__.py +0 -0
- misleep/analysis/transformer/configs.py +52 -0
- misleep/analysis/transformer/data.py +362 -0
- misleep/analysis/transformer/inference.py +328 -0
- misleep/analysis/transformer/models.py +220 -0
- misleep/analysis/transformer/preprocessing.py +105 -0
- misleep/config/__init__.py +105 -0
- misleep/config/default_config.ini +38 -0
- misleep/data/__init__.py +13 -0
- misleep/data/annotation.py +114 -0
- misleep/data/midata.py +387 -0
- misleep/gui/__init__.py +23 -0
- misleep/gui/app.py +183 -0
- misleep/gui/config_dialog.py +429 -0
- misleep/gui/dialogs.py +1107 -0
- misleep/gui/event_filters.py +26 -0
- misleep/gui/main_window.py +2699 -0
- misleep/gui/qt_utils.py +277 -0
- misleep/gui/resources/__init__.py +9 -0
- misleep/gui/resources/entire_logo.png +0 -0
- misleep/gui/resources/logo.png +0 -0
- misleep/gui/resources/misleep.ico +0 -0
- misleep/gui/resources/misleep.qrc +7 -0
- misleep/gui/resources/misleep_rc.py +1324 -0
- misleep/gui/spec_window.py +187 -0
- misleep/gui/style.py +994 -0
- misleep/gui/uis/SWA_detect_dialog.ui +181 -0
- misleep/gui/uis/SWA_detect_dialog_ui.py +176 -0
- misleep/gui/uis/__init__.py +6 -0
- misleep/gui/uis/about.ui +88 -0
- misleep/gui/uis/about_ui.py +80 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +77 -0
- misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +90 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog.ui +124 -0
- misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +124 -0
- misleep/gui/uis/horizontal_line_dialog.ui +166 -0
- misleep/gui/uis/horizontal_line_dialog_ui.py +157 -0
- misleep/gui/uis/label_dialog.ui +65 -0
- misleep/gui/uis/label_dialog_ui.py +72 -0
- misleep/gui/uis/main_window.ui +871 -0
- misleep/gui/uis/main_window_ui.py +616 -0
- misleep/gui/uis/save_data_dialog.ui +95 -0
- misleep/gui/uis/save_data_dialog_ui.py +98 -0
- misleep/gui/uis/spec_window.ui +109 -0
- misleep/gui/uis/spec_window_ui.py +103 -0
- misleep/gui/uis/spindle_detect_dialog.ui +202 -0
- misleep/gui/uis/spindle_detect_dialog_ui.py +194 -0
- misleep/gui/uis/state_spectral_dialog.ui +240 -0
- misleep/gui/uis/state_spectral_dialog_ui.py +204 -0
- misleep/gui/uis/transfer_result_dialog.ui +104 -0
- misleep/gui/uis/transfer_result_dialog_ui.py +94 -0
- misleep/gui/workers.py +69 -0
- misleep/io/__init__.py +68 -0
- misleep/io/annotation.py +367 -0
- misleep/io/array.py +209 -0
- misleep/io/base.py +174 -0
- misleep/io/edf.py +109 -0
- misleep/io/mat.py +189 -0
- misleep/logger.py +79 -0
- misleep/preprocessing/__init__.py +23 -0
- misleep/preprocessing/artifacts.py +76 -0
- misleep/preprocessing/filtering.py +94 -0
- misleep/preprocessing/segment.py +59 -0
- misleep/preprocessing/spectral.py +169 -0
- misleep/utils/__init__.py +45 -0
- misleep/utils/annotation.py +130 -0
- misleep/utils/entropy.py +135 -0
- misleep/utils/misc.py +69 -0
- misleep/utils/time_utils.py +61 -0
- misleep/viz/__init__.py +12 -0
- misleep/viz/hypnogram.py +48 -0
- misleep/viz/signals.py +51 -0
- misleep/viz/spectral.py +98 -0
- misleep-0.3.0.dist-info/METADATA +249 -0
- misleep-0.3.0.dist-info/RECORD +94 -0
- misleep-0.3.0.dist-info/WHEEL +5 -0
- misleep-0.3.0.dist-info/entry_points.txt +15 -0
- misleep-0.3.0.dist-info/licenses/LICENSE +29 -0
- misleep-0.3.0.dist-info/top_level.txt +1 -0
misleep/gui/qt_utils.py
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""GUI utility functions and Qt helper classes.
|
|
3
|
+
|
|
4
|
+
Pure helpers that the GUI uses for drawing spectra and formatting time;
|
|
5
|
+
the general-purpose helpers are re-exported from :mod:`misleep.utils` so
|
|
6
|
+
there is a single source of truth.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QAbstractListModel, QModelIndex, Qt
|
|
10
|
+
from PySide6.QtGui import QIcon
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QSizePolicy,
|
|
13
|
+
QToolButton,
|
|
14
|
+
QVBoxLayout,
|
|
15
|
+
QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def app_icon() -> QIcon:
|
|
20
|
+
"""Return the MiSleep application icon from the Qt resources.
|
|
21
|
+
|
|
22
|
+
The square ``misleep.ico`` is the primary icon (crisp at small title-bar
|
|
23
|
+
/ taskbar sizes); ``logo.png`` is kept as the large-size fallback.
|
|
24
|
+
"""
|
|
25
|
+
icon = QIcon()
|
|
26
|
+
icon.addFile(":/logo/misleep.ico")
|
|
27
|
+
icon.addFile(":/logo/logo.png")
|
|
28
|
+
if icon.isNull():
|
|
29
|
+
icon = QIcon(":/logo/logo.png")
|
|
30
|
+
return icon
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CollapsibleSection(QWidget):
|
|
34
|
+
"""A titled, collapsible panel used in the right-hand sidebar.
|
|
35
|
+
|
|
36
|
+
One header row with a fold/unfold arrow plus a content widget that can
|
|
37
|
+
be hidden. Replaces the old draggable docks with a tidy, fixed panel.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, title, content=None, parent=None, collapsed=False):
|
|
41
|
+
super().__init__(parent)
|
|
42
|
+
self._title = title
|
|
43
|
+
self._content = None
|
|
44
|
+
|
|
45
|
+
layout = QVBoxLayout(self)
|
|
46
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
47
|
+
layout.setSpacing(0)
|
|
48
|
+
|
|
49
|
+
self.header = QToolButton()
|
|
50
|
+
self.header.setObjectName("SidebarSectionHeader")
|
|
51
|
+
self.header.setCheckable(True)
|
|
52
|
+
self.header.setChecked(not collapsed)
|
|
53
|
+
self.header.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
54
|
+
self.header.setSizePolicy(QSizePolicy.Policy.Expanding,
|
|
55
|
+
QSizePolicy.Policy.Fixed)
|
|
56
|
+
self.header.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
|
57
|
+
self.header.setToolTip("Click to expand / collapse")
|
|
58
|
+
self.header.clicked.connect(self._on_header_clicked)
|
|
59
|
+
layout.addWidget(self.header)
|
|
60
|
+
|
|
61
|
+
if content is not None:
|
|
62
|
+
self.set_content(content, collapsed=collapsed)
|
|
63
|
+
self._update_arrow()
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------
|
|
66
|
+
# Public API
|
|
67
|
+
# ------------------------------------------------------------------
|
|
68
|
+
def set_content(self, widget, collapsed=None):
|
|
69
|
+
"""Attach the content widget (re-parents it into this section)."""
|
|
70
|
+
if self._content is not None:
|
|
71
|
+
self._content.setParent(None)
|
|
72
|
+
self._content = widget
|
|
73
|
+
if widget is not None:
|
|
74
|
+
# Dock contents often retain a large layout size hint from their
|
|
75
|
+
# former floating-dock geometry. Ignore that horizontal hint so
|
|
76
|
+
# the unified sidebar can actually constrain every section to its
|
|
77
|
+
# viewport instead of silently clipping the rightmost controls.
|
|
78
|
+
widget.setMinimumWidth(0)
|
|
79
|
+
widget.setSizePolicy(QSizePolicy.Policy.Ignored,
|
|
80
|
+
QSizePolicy.Policy.Preferred)
|
|
81
|
+
self.layout().addWidget(widget)
|
|
82
|
+
if collapsed is not None:
|
|
83
|
+
self.header.setChecked(not collapsed)
|
|
84
|
+
widget.setVisible(not collapsed)
|
|
85
|
+
self._update_arrow()
|
|
86
|
+
|
|
87
|
+
def set_expanded(self, expanded):
|
|
88
|
+
"""Programmatically expand (True) or collapse (False) the section."""
|
|
89
|
+
self.header.setChecked(expanded)
|
|
90
|
+
if self._content is not None:
|
|
91
|
+
self._content.setVisible(expanded)
|
|
92
|
+
self._update_arrow()
|
|
93
|
+
|
|
94
|
+
def is_expanded(self):
|
|
95
|
+
return self.header.isChecked()
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def title(self):
|
|
99
|
+
return self._title
|
|
100
|
+
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
# Internals
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
def _on_header_clicked(self):
|
|
105
|
+
if self._content is not None:
|
|
106
|
+
self._content.setVisible(self.header.isChecked())
|
|
107
|
+
self._update_arrow()
|
|
108
|
+
|
|
109
|
+
def _update_arrow(self):
|
|
110
|
+
self.header.setArrowType(
|
|
111
|
+
Qt.ArrowType.DownArrow if self.header.isChecked()
|
|
112
|
+
else Qt.ArrowType.RightArrow)
|
|
113
|
+
self.header.setText(self._title)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ChannelListModel(QAbstractListModel):
|
|
117
|
+
"""A simple editable, drag-reorderable string list model.
|
|
118
|
+
|
|
119
|
+
Unlike ``QStringListModel`` this implements ``moveRows``, so a
|
|
120
|
+
drag & drop emits ``rowsMoved`` (a genuine move) instead of being
|
|
121
|
+
performed as remove+insert -- which previously confused the rename
|
|
122
|
+
logic.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def __init__(self, channels=None, parent=None):
|
|
126
|
+
super().__init__(parent)
|
|
127
|
+
self._channels = list(channels or [])
|
|
128
|
+
|
|
129
|
+
def rowCount(self, parent=QModelIndex()):
|
|
130
|
+
return 0 if parent.isValid() else len(self._channels)
|
|
131
|
+
|
|
132
|
+
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
|
133
|
+
if not index.isValid() or not (0 <= index.row() < len(self._channels)):
|
|
134
|
+
return None
|
|
135
|
+
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole):
|
|
136
|
+
return self._channels[index.row()]
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
def setData(self, index, value, role=Qt.ItemDataRole.EditRole):
|
|
140
|
+
if role == Qt.ItemDataRole.EditRole and index.isValid() \
|
|
141
|
+
and 0 <= index.row() < len(self._channels):
|
|
142
|
+
self._channels[index.row()] = str(value)
|
|
143
|
+
self.dataChanged.emit(index, index, [Qt.ItemDataRole.EditRole])
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def flags(self, index):
|
|
148
|
+
if not index.isValid():
|
|
149
|
+
return Qt.ItemFlag.ItemIsDropEnabled
|
|
150
|
+
return (Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsSelectable
|
|
151
|
+
| Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsDragEnabled)
|
|
152
|
+
|
|
153
|
+
def moveRows(self, sourceParent, sourceRow, count, destinationParent, destinationChild):
|
|
154
|
+
"""Move a single row within the same list (drag & drop reorder)."""
|
|
155
|
+
if sourceParent.isValid() or destinationParent.isValid():
|
|
156
|
+
return False
|
|
157
|
+
if count != 1 or sourceRow == destinationChild:
|
|
158
|
+
return False
|
|
159
|
+
if not (0 <= sourceRow < len(self._channels)):
|
|
160
|
+
return False
|
|
161
|
+
|
|
162
|
+
dest = destinationChild
|
|
163
|
+
if sourceRow < dest:
|
|
164
|
+
dest -= 1
|
|
165
|
+
if dest < 0:
|
|
166
|
+
dest = 0
|
|
167
|
+
dest = min(dest, len(self._channels) - 1)
|
|
168
|
+
|
|
169
|
+
last = sourceRow
|
|
170
|
+
self.beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, destinationChild)
|
|
171
|
+
item = self._channels.pop(sourceRow)
|
|
172
|
+
self._channels.insert(dest, item)
|
|
173
|
+
self.endMoveRows()
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
def channels(self):
|
|
177
|
+
"""Return a copy of the channel names in their current order."""
|
|
178
|
+
return list(self._channels)
|
|
179
|
+
|
|
180
|
+
def setChannels(self, channels):
|
|
181
|
+
"""Replace the whole list (no move/insert signals)."""
|
|
182
|
+
self.beginResetModel()
|
|
183
|
+
self._channels = list(channels)
|
|
184
|
+
self.endResetModel()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def cal_draw_spectrum(data, sf, nperseg, freq_band=None, relative=None, nfft=None, gaussian_sigma=None):
|
|
188
|
+
"""Calculate a power spectrum and return it together with a figure.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
data : ndarray
|
|
193
|
+
Signal to analyze.
|
|
194
|
+
sf : float
|
|
195
|
+
Sampling frequency.
|
|
196
|
+
nperseg : int
|
|
197
|
+
Window length (in samples) for the Welch FFT.
|
|
198
|
+
freq_band : list, optional
|
|
199
|
+
Frequency band ``[low, high]``. Defaults to ``[0.5, 30]``.
|
|
200
|
+
relative : bool, optional
|
|
201
|
+
Whether to normalize the PSD to a relative power.
|
|
202
|
+
nfft : int, optional
|
|
203
|
+
Number of FFT points.
|
|
204
|
+
gaussian_sigma : float, optional
|
|
205
|
+
Gaussian smoothing sigma.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
(spectrum, figure) : tuple
|
|
210
|
+
``spectrum`` is an array of shape ``(2, n_freq)`` (frequencies and
|
|
211
|
+
powers); ``figure`` is the matplotlib figure.
|
|
212
|
+
"""
|
|
213
|
+
# IMPORTANT: build the figure with matplotlib.figure.Figure directly
|
|
214
|
+
# (not pyplot). The old ``plt.close()`` here silently closed the main
|
|
215
|
+
# window's figures, which froze the signal/hypnogram panels after an
|
|
216
|
+
# export. A non-pyplot figure is invisible to pyplot bookkeeping.
|
|
217
|
+
import numpy as np
|
|
218
|
+
from matplotlib.figure import Figure
|
|
219
|
+
from scipy.ndimage import gaussian_filter1d
|
|
220
|
+
from scipy.signal import welch
|
|
221
|
+
|
|
222
|
+
if freq_band is None:
|
|
223
|
+
freq_band = [0.5, 30]
|
|
224
|
+
F, P = welch(data, sf, nperseg=nperseg, nfft=nfft, scaling="density")
|
|
225
|
+
|
|
226
|
+
F = np.array([round(each, 2) for each in F])
|
|
227
|
+
if gaussian_sigma is not None:
|
|
228
|
+
P = gaussian_filter1d(P, sigma=gaussian_sigma)
|
|
229
|
+
|
|
230
|
+
if freq_band is not None:
|
|
231
|
+
idx_band = np.logical_and(F >= freq_band[0], F <= freq_band[1])
|
|
232
|
+
F = F[idx_band]
|
|
233
|
+
P = P[idx_band]
|
|
234
|
+
|
|
235
|
+
if relative:
|
|
236
|
+
total = sum(P)
|
|
237
|
+
if total > 0:
|
|
238
|
+
P = [each / total for each in P]
|
|
239
|
+
|
|
240
|
+
major_ticks_top = np.linspace(0, freq_band[1] + 0.1, 10)
|
|
241
|
+
|
|
242
|
+
figure = Figure(figsize=(10, 7))
|
|
243
|
+
ax = figure.subplots(nrows=1, ncols=1)
|
|
244
|
+
figure.subplots_adjust(top=0.95, left=0.15, bottom=0.15, right=0.95)
|
|
245
|
+
|
|
246
|
+
ax.xaxis.set_ticks(major_ticks_top)
|
|
247
|
+
ax.grid(which="major", alpha=0.6)
|
|
248
|
+
|
|
249
|
+
ax.set_xlim(freq_band[0], freq_band[1] + 0.1)
|
|
250
|
+
ax.plot(F, P)
|
|
251
|
+
ax.set_xlabel("Frequency (Hz)")
|
|
252
|
+
ax.set_ylabel("Power spectral density (Power/Hz)")
|
|
253
|
+
|
|
254
|
+
return np.array([F, P]), figure
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Re-export shared helpers so existing imports of ``misleep.gui.utils`` keep working.
|
|
258
|
+
from misleep.utils.misc import ( # noqa: E402, F401
|
|
259
|
+
create_new_mianno,
|
|
260
|
+
identify_startend_color,
|
|
261
|
+
get_base_path,
|
|
262
|
+
downsample_by_most_frequent,
|
|
263
|
+
)
|
|
264
|
+
from misleep.utils.time_utils import second2time, transfer_time # noqa: E402, F401
|
|
265
|
+
from misleep.utils.annotation import insert_row, temp_loop4below_row # noqa: E402, F401
|
|
266
|
+
|
|
267
|
+
__all__ = [
|
|
268
|
+
"cal_draw_spectrum",
|
|
269
|
+
"create_new_mianno",
|
|
270
|
+
"identify_startend_color",
|
|
271
|
+
"get_base_path",
|
|
272
|
+
"downsample_by_most_frequent",
|
|
273
|
+
"second2time",
|
|
274
|
+
"transfer_time",
|
|
275
|
+
"insert_row",
|
|
276
|
+
"temp_loop4below_row",
|
|
277
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
"""GUI resource module.
|
|
3
|
+
|
|
4
|
+
Importing this package registers the compiled Qt resources
|
|
5
|
+
(``misleep_rc``) so that ``:/logo/...`` paths used by the generated UI
|
|
6
|
+
modules resolve correctly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from misleep.gui.resources import misleep_rc # noqa: F401
|
|
Binary file
|
|
Binary file
|
|
Binary file
|