misleep 0.3.1__tar.gz → 0.3.3__tar.gz
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-0.3.1 → misleep-0.3.3}/CHANGELOG.md +16 -0
- {misleep-0.3.1/src/misleep.egg-info → misleep-0.3.3}/PKG-INFO +1 -1
- {misleep-0.3.1 → misleep-0.3.3}/docs/config_file.md +4 -4
- {misleep-0.3.1 → misleep-0.3.3}/pyproject.toml +1 -1
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/__init__.py +1 -1
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/models/benchmark_models.pkl +1 -1
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/config/default_config.ini +2 -2
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/data/midata.py +15 -5
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/dialogs.py +50 -20
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/about.ui +2 -2
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/about_ui.py +2 -2
- {misleep-0.3.1 → misleep-0.3.3/src/misleep.egg-info}/PKG-INFO +1 -1
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_data.py +53 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_gui.py +60 -1
- {misleep-0.3.1 → misleep-0.3.3}/LICENSE +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/MANIFEST.in +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/README.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/api_reference.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/data_formats.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/developer_guide.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/getting_started.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/imgs/entire_logo.png +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/imgs/gui/main_light.png +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/imgs/logo.png +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/index.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/docs/user_guide.md +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/examples/auto_stage_example.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/examples/basic_usage.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/examples/extend_io_example.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/setup.cfg +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/__main__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/_compat.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/auto_stage.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/benchmark.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/features.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/hmm.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/model.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/autostage/postprocess.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/detection.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/features.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/models/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/checkpoints/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/configs.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/data.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/inference.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/models.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/analysis/transformer/preprocessing.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/config/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/data/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/data/annotation.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/app.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/config_dialog.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/event_filters.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/main_window.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/qt_utils.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/entire_logo.png +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/logo.png +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/misleep.ico +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/misleep.qrc +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/resources/misleep_rc.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/spec_window.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/style.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/SWA_detect_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/SWA_detect_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/auto_stage_lightGBM_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/horizontal_line_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/horizontal_line_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/label_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/label_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/main_window.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/main_window_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/save_data_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/save_data_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/spec_window.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/spec_window_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/spindle_detect_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/spindle_detect_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/state_spectral_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/state_spectral_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/transfer_result_dialog.ui +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/transfer_result_dialog_ui.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/workers.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/annotation.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/array.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/base.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/edf.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/io/mat.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/logger.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/preprocessing/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/preprocessing/artifacts.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/preprocessing/filtering.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/preprocessing/segment.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/preprocessing/spectral.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/utils/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/utils/annotation.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/utils/entropy.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/utils/misc.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/utils/time_utils.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/viz/__init__.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/viz/hypnogram.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/viz/signals.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep/viz/spectral.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep.egg-info/SOURCES.txt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep.egg-info/dependency_links.txt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep.egg-info/entry_points.txt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep.egg-info/requires.txt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/src/misleep.egg-info/top_level.txt +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_analysis.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_io.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_preprocessing.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_segment.py +0 -0
- {misleep-0.3.1 → misleep-0.3.3}/tests/test_viz_config.py +0 -0
|
@@ -5,6 +5,22 @@ All notable changes to MiSleep are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.2] — 2026-08-24
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Filtering broke on fractional sampling frequencies**: `MiData.add()` (used
|
|
13
|
+
by `MiData.filter()`) recomputed the duration with
|
|
14
|
+
`math.floor(len(signal) / sf)` from a signal that had already been
|
|
15
|
+
truncated to `int(duration * sf)` samples. When `sf` is a decimal (e.g.
|
|
16
|
+
`99.9927` Hz), that came out one second short, so filtering either raised a
|
|
17
|
+
spurious "new signal is shorter" error or produced a channel whose length /
|
|
18
|
+
duration did not match the original signal, which then failed the signal /
|
|
19
|
+
label consistency checks in the GUI. The check now compares sample counts
|
|
20
|
+
against `int(duration * sf)` directly, so filtering preserves the sample
|
|
21
|
+
count and duration for any positive `sf` while still rejecting genuinely
|
|
22
|
+
short signals.
|
|
23
|
+
|
|
8
24
|
## [0.3.1] — 2026-08-18
|
|
9
25
|
|
|
10
26
|
### Added
|
|
@@ -23,8 +23,8 @@ wipe your personal settings. Open the user file from the GUI via
|
|
|
23
23
|
|
|
24
24
|
| key | default | meaning |
|
|
25
25
|
|-----|---------|---------|
|
|
26
|
-
| `version` | `v0.3.
|
|
27
|
-
| `updatetime` | `2026/08/
|
|
26
|
+
| `version` | `v0.3.2` | displayed in the About dialog |
|
|
27
|
+
| `updatetime` | `2026/08/24` | displayed in the About dialog |
|
|
28
28
|
| `marker` | `['half_signal', 'first REM', ...]` | marker label list (label picker) |
|
|
29
29
|
| `startend` | `['high_theta', 'REM', ...]` | start-end label list (label picker) |
|
|
30
30
|
| `statemap` | `{"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT", ...}` | state code -> name mapping (JSON) |
|
|
@@ -48,8 +48,8 @@ wipe your personal settings. Open the user file from the GUI via
|
|
|
48
48
|
|
|
49
49
|
```ini
|
|
50
50
|
[gui]
|
|
51
|
-
version = v0.3.
|
|
52
|
-
updatetime = 2026/08/
|
|
51
|
+
version = v0.3.2
|
|
52
|
+
updatetime = 2026/08/24
|
|
53
53
|
marker = ['half_signal', 'first REM', 'WindEEG', 'W-R', 'maker']
|
|
54
54
|
startend = ['high_theta', 'REM', 'Wake', 'Spindle', 'SWA', 'start end label', 'start end label']
|
|
55
55
|
statemap = {"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT", "5": "IS", "6": "MicroArousal"}
|
|
@@ -22,7 +22,7 @@ works without PySide6; import ``misleep.gui`` explicitly when needed.
|
|
|
22
22
|
from importlib import import_module
|
|
23
23
|
|
|
24
24
|
__author__ = "Xueqiang Wang <swang9194@gmail.com>"
|
|
25
|
-
__version__ = "0.3.
|
|
25
|
+
__version__ = "0.3.2"
|
|
26
26
|
|
|
27
27
|
# Importing ``misleep`` used to import scipy, pandas, sklearn and matplotlib
|
|
28
28
|
# immediately. The GUI necessarily imports some of those later, but making
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
|
-
name: misleep-0.3.
|
|
2
|
+
name: misleep-0.3.3/src/misleep/analysis/models/benchmark_models.pkl
|
|
3
3
|
size: 50435195 bytes
|
|
4
4
|
sha256: 8390ffa5ef8d45701a09244ec75dea46d2e4bff6a4c8242b0de84da8ea9cd31c
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
# overwritten on the next package update.
|
|
16
16
|
|
|
17
17
|
[gui]
|
|
18
|
-
version = v0.3.
|
|
19
|
-
updatetime = 2026/08/
|
|
18
|
+
version = v0.3.2
|
|
19
|
+
updatetime = 2026/08/24
|
|
20
20
|
marker = ['half_signal', 'first REM', 'WindEEG', 'W-R', 'maker']
|
|
21
21
|
startend = ['high_theta', 'REM', 'Wake', 'Spindle', 'SWA', 'start end label', 'start end label']
|
|
22
22
|
statemap = {"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT"}
|
|
@@ -210,15 +210,25 @@ class MiData:
|
|
|
210
210
|
if not isinstance(sf, (int, float)) or not math.isfinite(sf) or sf <= 0:
|
|
211
211
|
raise TypeError(f"Sample frequency should be a float, got {type(sf)}")
|
|
212
212
|
|
|
213
|
-
|
|
214
|
-
|
|
213
|
+
# Every channel shares the same integer duration: the sample count a
|
|
214
|
+
# channel of ``sf`` Hz needs to cover ``self._duration`` seconds is
|
|
215
|
+
# ``int(self._duration * sf)`` (the same truncation used in
|
|
216
|
+
# :meth:`__init__`). Compare sample counts directly instead of
|
|
217
|
+
# re-deriving the duration from an already-truncated length -- with a
|
|
218
|
+
# fractional sampling frequency (e.g. 99.9927 Hz) the naive
|
|
219
|
+
# ``math.floor(len(signal) / sf)`` can come out one second short and
|
|
220
|
+
# wrongly reject signals that are exactly as long as the existing
|
|
221
|
+
# channels (this made ``MiData.filter()`` fail on fractional-sf data).
|
|
222
|
+
expected_samples = int(self._duration * sf)
|
|
223
|
+
if len(signal) < expected_samples:
|
|
215
224
|
raise ValueError(
|
|
216
|
-
f"The new signal is shorter ({
|
|
217
|
-
f"({self._duration}s
|
|
225
|
+
f"The new signal is shorter ({len(signal)} samples) than the "
|
|
226
|
+
f"existing data ({self._duration}s, {expected_samples} samples "
|
|
227
|
+
f"at {sf} Hz)")
|
|
218
228
|
|
|
219
229
|
channel = _unique_name(channel, self.channels)
|
|
220
230
|
|
|
221
|
-
self._signals.append(signal[:
|
|
231
|
+
self._signals.append(signal[:expected_samples])
|
|
222
232
|
self._channels.append(channel)
|
|
223
233
|
self._n_channels = len(self._channels)
|
|
224
234
|
self._sf.append(sf)
|
|
@@ -7,6 +7,7 @@ the two auto-staging dialogs and the save-data dialog, plus the About box.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
import datetime
|
|
10
|
+
import math
|
|
10
11
|
import os
|
|
11
12
|
from copy import deepcopy
|
|
12
13
|
|
|
@@ -50,6 +51,33 @@ from misleep.logger import logger
|
|
|
50
51
|
from misleep.utils.annotation import lst2group
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
def _split_state_data_by_hour(channel_data, sleep_states, states, sf):
|
|
55
|
+
"""Return per-state signal segments in fixed, time-aligned hour slots.
|
|
56
|
+
|
|
57
|
+
Every state receives one entry per elapsed hour. An entry is ``None``
|
|
58
|
+
when that state has no usable data in the hour, so later hours never
|
|
59
|
+
shift left to fill an empty ZT column.
|
|
60
|
+
"""
|
|
61
|
+
hour_count = int(math.ceil(len(sleep_states) / 3600))
|
|
62
|
+
result = {state: [None] * hour_count for state in states}
|
|
63
|
+
|
|
64
|
+
for hour_idx in range(hour_count):
|
|
65
|
+
hour_start = hour_idx * 3600
|
|
66
|
+
hour_stop = min(hour_start + 3600, len(sleep_states))
|
|
67
|
+
groups = lst2group(enumerate(sleep_states[hour_start:hour_stop]))
|
|
68
|
+
for state in states:
|
|
69
|
+
parts = [
|
|
70
|
+
channel_data[
|
|
71
|
+
int((hour_start + group_start) * sf):
|
|
72
|
+
int((hour_start + group_stop) * sf)]
|
|
73
|
+
for group_start, group_stop, value in groups
|
|
74
|
+
if value == state
|
|
75
|
+
]
|
|
76
|
+
if parts:
|
|
77
|
+
result[state][hour_idx] = np.concatenate(parts)
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
53
81
|
class AboutDialog(QDialog, Ui_AboutDialog):
|
|
54
82
|
"""The About box of MiSleep."""
|
|
55
83
|
|
|
@@ -400,11 +428,13 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
400
428
|
end_sec = mianno.anno_length
|
|
401
429
|
|
|
402
430
|
midata = midata.crop([start_sec, end_sec])
|
|
403
|
-
|
|
431
|
+
# ``end_sec`` is exclusive, matching MiData.crop(). Using end+1 here
|
|
432
|
+
# created a spurious sample at exact hour boundaries.
|
|
433
|
+
sleep_state_values = mianno.sleep_state[start_sec:end_sec]
|
|
404
434
|
|
|
405
435
|
channel_idx = self.ChannelSelector.currentIndex()
|
|
406
436
|
channel_data = midata.signals[channel_idx]
|
|
407
|
-
sleep_state = lst2group(
|
|
437
|
+
sleep_state = lst2group(enumerate(sleep_state_values))
|
|
408
438
|
sf = midata.sf[channel_idx]
|
|
409
439
|
|
|
410
440
|
# Band-pass filter if checked
|
|
@@ -422,7 +452,7 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
422
452
|
|
|
423
453
|
gaussian_sigma = self.GaussianSpinBox.value() if self.GaussianCheckBox.isChecked() else None
|
|
424
454
|
|
|
425
|
-
state_codes = sorted(set(
|
|
455
|
+
state_codes = sorted(set(sleep_state_values))
|
|
426
456
|
state_data = {
|
|
427
457
|
state: np.concatenate([
|
|
428
458
|
channel_data[int(each[0] * sf): int(each[1] * sf)]
|
|
@@ -451,21 +481,21 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
451
481
|
# Optional per-hour spectral segmentation
|
|
452
482
|
hour_spec = {state: [] for state in spectra}
|
|
453
483
|
if self.HourSegmentCheckBox.isChecked():
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
data
|
|
462
|
-
|
|
463
|
-
data = reject_artifact(data, sf=sf, threshold=threshold)
|
|
464
|
-
if len(data) > sf * 10:
|
|
465
|
-
hour_spec[state].append(cal_draw_spectrum(
|
|
484
|
+
hourly_data = _split_state_data_by_hour(
|
|
485
|
+
channel_data, sleep_state_values, spectra, sf)
|
|
486
|
+
for state, state_hours in hourly_data.items():
|
|
487
|
+
for data in state_hours:
|
|
488
|
+
if data is not None and self.RejectArtifactCheckBox.isChecked():
|
|
489
|
+
data = reject_artifact(
|
|
490
|
+
data, sf=sf, threshold=threshold)
|
|
491
|
+
if data is not None and len(data) > sf * 10:
|
|
492
|
+
power = cal_draw_spectrum(
|
|
466
493
|
data=data, sf=sf, nperseg=nperseg,
|
|
467
494
|
freq_band=freq_band, relative=relative, nfft=nfft,
|
|
468
|
-
gaussian_sigma=gaussian_sigma)[0][1]
|
|
495
|
+
gaussian_sigma=gaussian_sigma)[0][1]
|
|
496
|
+
hour_spec[state].append(power)
|
|
497
|
+
else:
|
|
498
|
+
hour_spec[state].append(None)
|
|
469
499
|
|
|
470
500
|
fd = QFileDialog.getExistingDirectory(self, "Select a folder to save states' data",
|
|
471
501
|
f"{config['gui']['openpath']}")
|
|
@@ -485,10 +515,10 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
485
515
|
figure.savefig(fd + "/" + safe_name + "_spectrum.pdf")
|
|
486
516
|
_df = pd.DataFrame(
|
|
487
517
|
data=spec.T, columns=["frequency", "power"])
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
pd.
|
|
518
|
+
for hour_idx, hour_power in enumerate(hour_spec[state]):
|
|
519
|
+
column = f"ZT{hour_idx}"
|
|
520
|
+
_df[column] = np.nan if hour_power is None \
|
|
521
|
+
else pd.Series(hour_power)
|
|
492
522
|
_df.to_excel(
|
|
493
523
|
excel_writer=writer, sheet_name=safe_name[:31],
|
|
494
524
|
index=False)
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
<item>
|
|
59
59
|
<widget class="QLabel" name="VersionLabel">
|
|
60
60
|
<property name="text">
|
|
61
|
-
<string>Version: 0.3.
|
|
61
|
+
<string>Version: 0.3.2</string>
|
|
62
62
|
</property>
|
|
63
63
|
</widget>
|
|
64
64
|
</item>
|
|
65
65
|
<item>
|
|
66
66
|
<widget class="QLabel" name="UpdateLabel">
|
|
67
67
|
<property name="text">
|
|
68
|
-
<string>Update: 2026/08/
|
|
68
|
+
<string>Update: 2026/08/24</string>
|
|
69
69
|
</property>
|
|
70
70
|
</widget>
|
|
71
71
|
</item>
|
|
@@ -73,8 +73,8 @@ class Ui_AboutDialog(object):
|
|
|
73
73
|
AboutDialog.setWindowTitle(QCoreApplication.translate("AboutDialog", u"About", None))
|
|
74
74
|
self.label_4.setText(QCoreApplication.translate("AboutDialog", u"<html><head/><body><p><img src=\":/logo/entire_logo.png\"/></p></body></html>", None))
|
|
75
75
|
self.groupBox.setTitle(QCoreApplication.translate("AboutDialog", u"MiSleep", None))
|
|
76
|
-
self.VersionLabel.setText(QCoreApplication.translate("AboutDialog", u"Version: 0.3.
|
|
77
|
-
self.UpdateLabel.setText(QCoreApplication.translate("AboutDialog", u"Update: 2026/08/
|
|
76
|
+
self.VersionLabel.setText(QCoreApplication.translate("AboutDialog", u"Version: 0.3.2", None))
|
|
77
|
+
self.UpdateLabel.setText(QCoreApplication.translate("AboutDialog", u"Update: 2026/08/24", None))
|
|
78
78
|
self.CopyrightLabel.setText(QCoreApplication.translate("AboutDialog", u"Copyright \u00a9 2023\u20132026 Xueqiang Wang.", None))
|
|
79
79
|
# retranslateUi
|
|
80
80
|
|
|
@@ -88,6 +88,59 @@ def test_midata_filter(midata):
|
|
|
88
88
|
assert midata.channels[-1].startswith("EEG_bandpass")
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
@pytest.mark.parametrize("sf", [99.9927, 100.5, 256.256, 305.005])
|
|
92
|
+
def test_midata_filter_fractional_sf_preserves_length(sf):
|
|
93
|
+
"""Filtering must not change the sample count or duration when sf is fractional.
|
|
94
|
+
|
|
95
|
+
Regression: ``MiData.add`` re-derived the duration with
|
|
96
|
+
``math.floor(len(signal) / sf)`` from a signal already truncated to
|
|
97
|
+
``int(duration * sf)`` samples; with a fractional sf that could come out
|
|
98
|
+
one second short and made ``filter()`` raise a spurious error (or produce
|
|
99
|
+
a shorter channel).
|
|
100
|
+
"""
|
|
101
|
+
rng = np.random.default_rng(42)
|
|
102
|
+
n_raw = int(1000 * sf)
|
|
103
|
+
md = MiData(signals=[rng.standard_normal(n_raw),
|
|
104
|
+
rng.standard_normal(n_raw)],
|
|
105
|
+
channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
|
|
106
|
+
duration, lengths = md.duration, [len(s) for s in md.signals]
|
|
107
|
+
|
|
108
|
+
md.filter(chans=["EEG"], btype="bandpass", low=0.5, high=30)
|
|
109
|
+
|
|
110
|
+
assert md.n_channels == 3
|
|
111
|
+
assert md.duration == duration
|
|
112
|
+
assert len(md.signals) == 3
|
|
113
|
+
assert all(len(sig) == length for sig, length in zip(md.signals, lengths))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_midata_add_fractional_sf():
|
|
117
|
+
"""A channel with ``int(duration * sf)`` samples must be accepted."""
|
|
118
|
+
sf = 99.9927
|
|
119
|
+
md = MiData(signals=[np.zeros(int(1000 * sf)), np.zeros(int(1000 * sf))],
|
|
120
|
+
channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
|
|
121
|
+
n = md.n_channels
|
|
122
|
+
duration, expected = md.duration, int(md.duration * sf)
|
|
123
|
+
|
|
124
|
+
# Exactly as many samples as the existing channels -> accepted as-is
|
|
125
|
+
md.add(np.zeros(expected), "EEG2", sf)
|
|
126
|
+
assert md.n_channels == n + 1
|
|
127
|
+
assert len(md.signals[-1]) == expected
|
|
128
|
+
assert md.duration == duration # duration unchanged by add()
|
|
129
|
+
|
|
130
|
+
# A genuinely shorter signal is still rejected
|
|
131
|
+
with pytest.raises(ValueError):
|
|
132
|
+
md.add(np.zeros(expected - 10), "TOO_SHORT", sf)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_midata_add_fractional_sf_rejects_short_signal():
|
|
136
|
+
"""Fractional-sf channels that are truly too short must still be rejected."""
|
|
137
|
+
sf = 256.256
|
|
138
|
+
md = MiData(signals=[np.zeros(int(1000 * sf)), np.zeros(int(1000 * sf))],
|
|
139
|
+
channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
|
|
140
|
+
with pytest.raises(ValueError):
|
|
141
|
+
md.add(np.zeros(500 * 256), "TOO_SHORT", 256.0)
|
|
142
|
+
|
|
143
|
+
|
|
91
144
|
def test_midata_differential(midata):
|
|
92
145
|
midata.differential(chan1="EEG", chan2="EMG")
|
|
93
146
|
assert midata.channels[-1] == "EEG_EMG_DIFF"
|
|
@@ -21,7 +21,7 @@ def _pyside6_available():
|
|
|
21
21
|
def test_public_api():
|
|
22
22
|
import misleep
|
|
23
23
|
|
|
24
|
-
assert misleep.__version__ == "0.3.
|
|
24
|
+
assert misleep.__version__ == "0.3.2"
|
|
25
25
|
for name in [
|
|
26
26
|
"MiData",
|
|
27
27
|
"MiAnnotation",
|
|
@@ -766,6 +766,65 @@ def test_data_export_always_restores_main_window(tmp_path, monkeypatch):
|
|
|
766
766
|
app.processEvents()
|
|
767
767
|
|
|
768
768
|
|
|
769
|
+
@pytest.mark.skipif(not _pyside6_available(), reason="PySide6 not installed")
|
|
770
|
+
def test_state_spectral_hour_columns_preserve_empty_zt_slots(
|
|
771
|
+
fresh_config, tmp_path, monkeypatch):
|
|
772
|
+
"""Hourly state spectra stay aligned to ZT0..ZTn despite empty hours."""
|
|
773
|
+
import numpy as np
|
|
774
|
+
import pandas as pd
|
|
775
|
+
from PySide6.QtWidgets import QApplication, QFileDialog
|
|
776
|
+
|
|
777
|
+
app = QApplication.instance() or QApplication([])
|
|
778
|
+
from misleep.data import MiAnnotation, MiData
|
|
779
|
+
from misleep.gui.dialogs import StateSpectralDialog
|
|
780
|
+
|
|
781
|
+
class FakeFigure:
|
|
782
|
+
def savefig(self, *args, **kwargs):
|
|
783
|
+
pass
|
|
784
|
+
|
|
785
|
+
def clear(self):
|
|
786
|
+
pass
|
|
787
|
+
|
|
788
|
+
def fake_spectrum(data, **kwargs):
|
|
789
|
+
return np.array([[1.0, 2.0], [len(data), float(np.mean(data))]]), \
|
|
790
|
+
FakeFigure()
|
|
791
|
+
|
|
792
|
+
monkeypatch.setattr(
|
|
793
|
+
"misleep.gui.dialogs.cal_draw_spectrum", fake_spectrum)
|
|
794
|
+
monkeypatch.setattr(
|
|
795
|
+
QFileDialog, "getExistingDirectory",
|
|
796
|
+
staticmethod(lambda *args, **kwargs: str(tmp_path)))
|
|
797
|
+
|
|
798
|
+
# State 1 is absent from ZT1; state 2 is absent from ZT0 and ZT2.
|
|
799
|
+
states = [1] * 3600 + [2] * 3600 + [1] * 3600
|
|
800
|
+
midata = MiData(
|
|
801
|
+
signals=[np.arange(len(states), dtype=float)],
|
|
802
|
+
channels=["EEG"], sf=[1.0], time="20240409-18:00:00")
|
|
803
|
+
mianno = MiAnnotation(states, state_map={1: "NREM", 2: "REM"})
|
|
804
|
+
fresh_config["gui"]["openpath"] = "recording.mat"
|
|
805
|
+
|
|
806
|
+
dialog = StateSpectralDialog(config=fresh_config)
|
|
807
|
+
dialog.dialog_show(midata.channels)
|
|
808
|
+
dialog.BPFilterCheckBox.setChecked(False)
|
|
809
|
+
dialog.HourSegmentCheckBox.setChecked(True)
|
|
810
|
+
assert dialog.spectral_analysis(midata, mianno, fresh_config) is True
|
|
811
|
+
|
|
812
|
+
workbook = tmp_path / "recording_power_results.xlsx"
|
|
813
|
+
nrem = pd.read_excel(workbook, sheet_name="NREM")
|
|
814
|
+
rem = pd.read_excel(workbook, sheet_name="REM")
|
|
815
|
+
expected = ["frequency", "power", "ZT0", "ZT1", "ZT2"]
|
|
816
|
+
assert list(nrem.columns) == expected
|
|
817
|
+
assert list(rem.columns) == expected
|
|
818
|
+
assert nrem["ZT1"].isna().all()
|
|
819
|
+
assert rem["ZT0"].isna().all()
|
|
820
|
+
assert rem["ZT2"].isna().all()
|
|
821
|
+
assert nrem["ZT0"].notna().all() and nrem["ZT2"].notna().all()
|
|
822
|
+
assert rem["ZT1"].notna().all()
|
|
823
|
+
|
|
824
|
+
dialog.close()
|
|
825
|
+
app.processEvents()
|
|
826
|
+
|
|
827
|
+
|
|
769
828
|
@pytest.mark.skipif(not _pyside6_available(), reason="PySide6 not installed")
|
|
770
829
|
def test_gui_with_data(tmp_path):
|
|
771
830
|
from PySide6.QtWidgets import QApplication
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{misleep-0.3.1 → misleep-0.3.3}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|