misleep 0.3.2__tar.gz → 0.3.4__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.2 → misleep-0.3.4}/CHANGELOG.md +34 -0
- {misleep-0.3.2/src/misleep.egg-info → misleep-0.3.4}/PKG-INFO +3 -2
- {misleep-0.3.2 → misleep-0.3.4}/docs/config_file.md +4 -4
- {misleep-0.3.2 → misleep-0.3.4}/pyproject.toml +1 -1
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/__init__.py +1 -1
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/benchmark.py +40 -1
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/models/benchmark_models.pkl +1 -1
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/config/default_config.ini +2 -2
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/dialogs.py +55 -21
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/main_window.py +9 -5
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/qt_utils.py +6 -5
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/spec_window.py +2 -2
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/about.ui +2 -2
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/about_ui.py +2 -2
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/preprocessing/spectral.py +64 -16
- {misleep-0.3.2 → misleep-0.3.4/src/misleep.egg-info}/PKG-INFO +3 -2
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_analysis.py +14 -0
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_gui.py +100 -1
- misleep-0.3.4/tests/test_preprocessing.py +170 -0
- misleep-0.3.2/tests/test_preprocessing.py +0 -95
- {misleep-0.3.2 → misleep-0.3.4}/LICENSE +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/MANIFEST.in +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/README.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/api_reference.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/data_formats.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/developer_guide.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/getting_started.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/imgs/entire_logo.png +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/imgs/gui/main_light.png +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/imgs/logo.png +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/index.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/docs/user_guide.md +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/examples/auto_stage_example.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/examples/basic_usage.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/examples/extend_io_example.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/setup.cfg +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/__main__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/_compat.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/auto_stage.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/features.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/hmm.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/model.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/autostage/postprocess.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/detection.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/features.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/models/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/checkpoints/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/configs.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/data.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/inference.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/models.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/analysis/transformer/preprocessing.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/config/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/data/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/data/annotation.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/data/midata.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/app.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/config_dialog.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/event_filters.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/entire_logo.png +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/logo.png +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/misleep.ico +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/misleep.qrc +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/resources/misleep_rc.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/style.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/SWA_detect_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/SWA_detect_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/auto_stage_lightGBM_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/horizontal_line_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/horizontal_line_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/label_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/label_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/main_window.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/main_window_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/save_data_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/save_data_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/spec_window.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/spec_window_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/spindle_detect_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/spindle_detect_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/state_spectral_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/state_spectral_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/transfer_result_dialog.ui +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/uis/transfer_result_dialog_ui.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/gui/workers.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/annotation.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/array.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/base.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/edf.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/io/mat.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/logger.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/preprocessing/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/preprocessing/artifacts.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/preprocessing/filtering.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/preprocessing/segment.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/utils/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/utils/annotation.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/utils/entropy.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/utils/misc.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/utils/time_utils.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/viz/__init__.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/viz/hypnogram.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/viz/signals.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep/viz/spectral.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep.egg-info/SOURCES.txt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep.egg-info/dependency_links.txt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep.egg-info/entry_points.txt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep.egg-info/requires.txt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/src/misleep.egg-info/top_level.txt +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_data.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_io.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_segment.py +0 -0
- {misleep-0.3.2 → misleep-0.3.4}/tests/test_viz_config.py +0 -0
|
@@ -5,6 +5,40 @@ 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
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Regular spectral export frequency grid**: State Spectral and the spectrum
|
|
13
|
+
window interpolate power onto the nominal frequency spacing selected by the
|
|
14
|
+
FFT duration (e.g. 0.1 Hz for 10 s), including exact band endpoints.
|
|
15
|
+
Fractional sample rates no longer produce drifting frequency labels or
|
|
16
|
+
duplicate near-boundary rows. Excel contains the regular numeric values,
|
|
17
|
+
rather than merely displaying rounded labels.
|
|
18
|
+
|
|
19
|
+
## [0.3.4] — 2026-09-30
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Spectral export frequency boundaries**: retain the true FFT frequency
|
|
24
|
+
precision and interpolate power at the exact requested band endpoints.
|
|
25
|
+
Fractional sample rates no longer drop the 30 Hz endpoint because a nearby
|
|
26
|
+
FFT bin rounds to 30.01 Hz. Applies to State Spectral, spectrum and
|
|
27
|
+
spectrogram exports. CSV frequency axes retain full precision, and power
|
|
28
|
+
integration uses the actual frequency coordinates.
|
|
29
|
+
|
|
30
|
+
## [0.3.3] — 2026-09-22
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Auto-staging model compatibility**: benchmark models serialized with
|
|
35
|
+
NumPy 2.x now load under NumPy 1.x as well. This fixes
|
|
36
|
+
`No module named 'numpy._core'` for users installing MiSleep into an
|
|
37
|
+
environment with an older supported NumPy release.
|
|
38
|
+
- **Hourly State Spectral export**: columns stay aligned to `ZT0`, `ZT1`,
|
|
39
|
+
... from the selected start time. Hours in which a state is absent remain
|
|
40
|
+
empty instead of shifting later spectra into earlier columns.
|
|
41
|
+
|
|
8
42
|
## [0.3.2] — 2026-08-24
|
|
9
43
|
|
|
10
44
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: misleep
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: MiSleep: Mice sleep EEG/EMG visualization, scoring, and analysis
|
|
5
5
|
Author-email: Xueqiang Wang <swang9194@gmail.com>
|
|
6
6
|
Maintainer-email: Xueqiang Wang <swang9194@gmail.com>
|
|
@@ -78,6 +78,7 @@ Requires-Dist: lightgbm>=3.3; extra == "full"
|
|
|
78
78
|
Provides-Extra: dev
|
|
79
79
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
80
80
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
81
|
+
Dynamic: license-file
|
|
81
82
|
|
|
82
83
|
# MiSleep
|
|
83
84
|
|
|
@@ -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/
|
|
26
|
+
| `version` | `v0.3.4` | displayed in the About dialog |
|
|
27
|
+
| `updatetime` | `2026/09/30` | 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/
|
|
51
|
+
version = v0.3.4
|
|
52
|
+
updatetime = 2026/09/30
|
|
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.4"
|
|
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
|
|
@@ -22,6 +22,7 @@ hypnogram.
|
|
|
22
22
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
+
from contextlib import contextmanager
|
|
25
26
|
from functools import lru_cache
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
@@ -48,6 +49,43 @@ STRIDE = 1.0
|
|
|
48
49
|
COMBO_BASE = {"F": "eegf", "P": "eegp"}
|
|
49
50
|
|
|
50
51
|
|
|
52
|
+
@contextmanager
|
|
53
|
+
def _numpy_pickle_compat(joblib_module):
|
|
54
|
+
"""Make NumPy-2-created model pickles readable with NumPy 1.x.
|
|
55
|
+
|
|
56
|
+
NumPy 2 renamed its private ``numpy.core`` package to ``numpy._core``.
|
|
57
|
+
The bundled benchmark was serialized under NumPy 2 and therefore stores
|
|
58
|
+
the latter module path. NumPy 1.x cannot import it, even though the
|
|
59
|
+
referenced scalar implementation is otherwise compatible. Remap that
|
|
60
|
+
one private module path inside Joblib's unpickler, without changing the
|
|
61
|
+
process-wide NumPy module namespace.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
numpy_major = int(np.__version__.split(".", 1)[0])
|
|
65
|
+
except (TypeError, ValueError): # pragma: no cover - non-standard build
|
|
66
|
+
numpy_major = 2
|
|
67
|
+
|
|
68
|
+
if numpy_major >= 2:
|
|
69
|
+
yield
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
unpickler = joblib_module.numpy_pickle.NumpyUnpickler
|
|
73
|
+
original_find_class = unpickler.find_class
|
|
74
|
+
|
|
75
|
+
def find_class_compat(self, module, name):
|
|
76
|
+
if module == "numpy._core":
|
|
77
|
+
module = "numpy.core"
|
|
78
|
+
elif module.startswith("numpy._core."):
|
|
79
|
+
module = "numpy.core." + module[len("numpy._core."):]
|
|
80
|
+
return original_find_class(self, module, name)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
unpickler.find_class = find_class_compat
|
|
84
|
+
yield
|
|
85
|
+
finally:
|
|
86
|
+
unpickler.find_class = original_find_class
|
|
87
|
+
|
|
88
|
+
|
|
51
89
|
def models_path() -> Path:
|
|
52
90
|
"""Return the path of the packaged benchmark models file."""
|
|
53
91
|
return resource_dir("misleep.analysis.models") / "benchmark_models.pkl"
|
|
@@ -63,7 +101,8 @@ def load_models(path=None):
|
|
|
63
101
|
raise FileNotFoundError(
|
|
64
102
|
f"Model file not found: {path}. Make sure the package data is "
|
|
65
103
|
f"installed (pip install misleep).")
|
|
66
|
-
|
|
104
|
+
with _numpy_pickle_compat(joblib):
|
|
105
|
+
return joblib.load(path)
|
|
67
106
|
|
|
68
107
|
|
|
69
108
|
def model_combo(site="F", use_emg=False, use_acc=False):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
|
-
name: misleep-0.3.
|
|
2
|
+
name: misleep-0.3.4/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/
|
|
18
|
+
version = v0.3.4
|
|
19
|
+
updatetime = 2026/09/30
|
|
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"}
|
|
@@ -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
|
|
@@ -419,10 +449,12 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
419
449
|
nfft = int(self.nfftSpinBox.value() * sf) if self.nfftCheckBox.isChecked() else None
|
|
420
450
|
if nfft is not None and nfft < nperseg:
|
|
421
451
|
nfft = None
|
|
452
|
+
frequency_step = 1.0 / (
|
|
453
|
+
self.nfftSpinBox.value() if nfft is not None else win_length)
|
|
422
454
|
|
|
423
455
|
gaussian_sigma = self.GaussianSpinBox.value() if self.GaussianCheckBox.isChecked() else None
|
|
424
456
|
|
|
425
|
-
state_codes = sorted(set(
|
|
457
|
+
state_codes = sorted(set(sleep_state_values))
|
|
426
458
|
state_data = {
|
|
427
459
|
state: np.concatenate([
|
|
428
460
|
channel_data[int(each[0] * sf): int(each[1] * sf)]
|
|
@@ -442,7 +474,8 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
442
474
|
spectra = {
|
|
443
475
|
state: cal_draw_spectrum(data=data, sf=sf, nperseg=nperseg,
|
|
444
476
|
freq_band=freq_band, relative=relative,
|
|
445
|
-
nfft=nfft, gaussian_sigma=gaussian_sigma
|
|
477
|
+
nfft=nfft, gaussian_sigma=gaussian_sigma,
|
|
478
|
+
frequency_step=frequency_step)
|
|
446
479
|
for state, data in state_data.items()
|
|
447
480
|
}
|
|
448
481
|
|
|
@@ -451,21 +484,22 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
451
484
|
# Optional per-hour spectral segmentation
|
|
452
485
|
hour_spec = {state: [] for state in spectra}
|
|
453
486
|
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(
|
|
487
|
+
hourly_data = _split_state_data_by_hour(
|
|
488
|
+
channel_data, sleep_state_values, spectra, sf)
|
|
489
|
+
for state, state_hours in hourly_data.items():
|
|
490
|
+
for data in state_hours:
|
|
491
|
+
if data is not None and self.RejectArtifactCheckBox.isChecked():
|
|
492
|
+
data = reject_artifact(
|
|
493
|
+
data, sf=sf, threshold=threshold)
|
|
494
|
+
if data is not None and len(data) > sf * 10:
|
|
495
|
+
power = cal_draw_spectrum(
|
|
466
496
|
data=data, sf=sf, nperseg=nperseg,
|
|
467
497
|
freq_band=freq_band, relative=relative, nfft=nfft,
|
|
468
|
-
gaussian_sigma=gaussian_sigma
|
|
498
|
+
gaussian_sigma=gaussian_sigma,
|
|
499
|
+
frequency_step=frequency_step)[0][1]
|
|
500
|
+
hour_spec[state].append(power)
|
|
501
|
+
else:
|
|
502
|
+
hour_spec[state].append(None)
|
|
469
503
|
|
|
470
504
|
fd = QFileDialog.getExistingDirectory(self, "Select a folder to save states' data",
|
|
471
505
|
f"{config['gui']['openpath']}")
|
|
@@ -485,10 +519,10 @@ class StateSpectralDialog(QDialog, Ui_StateSpectralDialog):
|
|
|
485
519
|
figure.savefig(fd + "/" + safe_name + "_spectrum.pdf")
|
|
486
520
|
_df = pd.DataFrame(
|
|
487
521
|
data=spec.T, columns=["frequency", "power"])
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
pd.
|
|
522
|
+
for hour_idx, hour_power in enumerate(hour_spec[state]):
|
|
523
|
+
column = f"ZT{hour_idx}"
|
|
524
|
+
_df[column] = np.nan if hour_power is None \
|
|
525
|
+
else pd.Series(hour_power)
|
|
492
526
|
_df.to_excel(
|
|
493
527
|
excel_writer=writer, sheet_name=safe_name[:31],
|
|
494
528
|
index=False)
|
|
@@ -2274,8 +2274,10 @@ class MainWindow(QMainWindow, Ui_MiSleep):
|
|
|
2274
2274
|
nfft = int(float(self.config["spec"]["nfft_sec"]) * self.midata.sf[channel])
|
|
2275
2275
|
if win_sec > (end_ - start_):
|
|
2276
2276
|
win_sec = end_ - start_
|
|
2277
|
-
if nfft < int(win_sec * self.midata.sf[channel]):
|
|
2278
|
-
nfft = int(win_sec * self.midata.sf[channel])
|
|
2277
|
+
if nfft < int(win_sec * self.midata.sf[channel]):
|
|
2278
|
+
nfft = int(win_sec * self.midata.sf[channel])
|
|
2279
|
+
frequency_step = 1.0 / max(
|
|
2280
|
+
float(self.config["spec"]["nfft_sec"]), win_sec)
|
|
2279
2281
|
|
|
2280
2282
|
try:
|
|
2281
2283
|
spec_window = self._ensure_dialog("spec_window", SpecWindow)
|
|
@@ -2286,14 +2288,16 @@ class MainWindow(QMainWindow, Ui_MiSleep):
|
|
|
2286
2288
|
band=freq_range,
|
|
2287
2289
|
nfft=nfft,
|
|
2288
2290
|
gaussian_sigma=float(self.config["spec"]["gaussian_sigma"]),
|
|
2289
|
-
win_sec=win_sec,
|
|
2290
|
-
|
|
2291
|
+
win_sec=win_sec,
|
|
2292
|
+
frequency_step=frequency_step,
|
|
2293
|
+
relative=False)
|
|
2291
2294
|
|
|
2292
2295
|
f, t, Sxx = spectrogram(
|
|
2293
2296
|
signal=signal_data,
|
|
2294
2297
|
sf=self.midata.sf[channel],
|
|
2295
2298
|
band=freq_range,
|
|
2296
|
-
step=1, win_sec=win_sec, norm=False, nfft=nfft
|
|
2299
|
+
step=1, win_sec=win_sec, norm=False, nfft=nfft,
|
|
2300
|
+
frequency_step=frequency_step)
|
|
2297
2301
|
|
|
2298
2302
|
bandPower = band_power(psd=psd, freq=freq,
|
|
2299
2303
|
bands=[[0.5, 4, "delta"], [4, 9, "theta"]])
|
|
@@ -184,7 +184,8 @@ class ChannelListModel(QAbstractListModel):
|
|
|
184
184
|
self.endResetModel()
|
|
185
185
|
|
|
186
186
|
|
|
187
|
-
def cal_draw_spectrum(data, sf, nperseg, freq_band=None, relative=None, nfft=None,
|
|
187
|
+
def cal_draw_spectrum(data, sf, nperseg, freq_band=None, relative=None, nfft=None,
|
|
188
|
+
gaussian_sigma=None, frequency_step=None):
|
|
188
189
|
"""Calculate a power spectrum and return it together with a figure.
|
|
189
190
|
|
|
190
191
|
Parameters
|
|
@@ -203,6 +204,8 @@ def cal_draw_spectrum(data, sf, nperseg, freq_band=None, relative=None, nfft=Non
|
|
|
203
204
|
Number of FFT points.
|
|
204
205
|
gaussian_sigma : float, optional
|
|
205
206
|
Gaussian smoothing sigma.
|
|
207
|
+
frequency_step : float, optional
|
|
208
|
+
Regular frequency spacing derived from the selected FFT duration.
|
|
206
209
|
|
|
207
210
|
Returns
|
|
208
211
|
-------
|
|
@@ -218,19 +221,17 @@ def cal_draw_spectrum(data, sf, nperseg, freq_band=None, relative=None, nfft=Non
|
|
|
218
221
|
from matplotlib.figure import Figure
|
|
219
222
|
from scipy.ndimage import gaussian_filter1d
|
|
220
223
|
from scipy.signal import welch
|
|
224
|
+
from misleep.preprocessing.spectral import _select_frequency_band
|
|
221
225
|
|
|
222
226
|
if freq_band is None:
|
|
223
227
|
freq_band = [0.5, 30]
|
|
224
228
|
F, P = welch(data, sf, nperseg=nperseg, nfft=nfft, scaling="density")
|
|
225
229
|
|
|
226
|
-
F = np.array([round(each, 2) for each in F])
|
|
227
230
|
if gaussian_sigma is not None:
|
|
228
231
|
P = gaussian_filter1d(P, sigma=gaussian_sigma)
|
|
229
232
|
|
|
230
233
|
if freq_band is not None:
|
|
231
|
-
|
|
232
|
-
F = F[idx_band]
|
|
233
|
-
P = P[idx_band]
|
|
234
|
+
F, P = _select_frequency_band(F, P, freq_band, frequency_step)
|
|
234
235
|
|
|
235
236
|
if relative:
|
|
236
237
|
total = sum(P)
|
|
@@ -136,7 +136,7 @@ class SpecWindow(QMainWindow, Ui_spec_window):
|
|
|
136
136
|
data_path = fd[:-4]
|
|
137
137
|
fd = data_path + "_data.csv"
|
|
138
138
|
_df = pd.DataFrame(
|
|
139
|
-
data=np.array([
|
|
139
|
+
data=np.array([self.spectrum[1], self.spectrum[0]]).T,
|
|
140
140
|
columns=["frequency", "power"])
|
|
141
141
|
_df.to_csv(fd, index=False)
|
|
142
142
|
except OSError:
|
|
@@ -166,7 +166,7 @@ class SpecWindow(QMainWindow, Ui_spec_window):
|
|
|
166
166
|
_df = pd.DataFrame(
|
|
167
167
|
self.spectrogram[2].T,
|
|
168
168
|
index=[f"{value:.2f}" for value in self.spectrogram[1]],
|
|
169
|
-
columns=
|
|
169
|
+
columns=self.spectrogram[0])
|
|
170
170
|
_df.to_csv(fd, index=True)
|
|
171
171
|
except OSError:
|
|
172
172
|
QMessageBox.about(
|
|
@@ -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.4</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/
|
|
68
|
+
<string>Update: 2026/09/30</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/
|
|
76
|
+
self.VersionLabel.setText(QCoreApplication.translate("AboutDialog", u"Version: 0.3.4", None))
|
|
77
|
+
self.UpdateLabel.setText(QCoreApplication.translate("AboutDialog", u"Update: 2026/09/30", None))
|
|
78
78
|
self.CopyrightLabel.setText(QCoreApplication.translate("AboutDialog", u"Copyright \u00a9 2023\u20132026 Xueqiang Wang.", None))
|
|
79
79
|
# retranslateUi
|
|
80
80
|
|
|
@@ -9,7 +9,58 @@ from scipy.signal import stft, welch
|
|
|
9
9
|
from misleep.preprocessing.filtering import signal_filter
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def _select_frequency_band(freq, power, band, frequency_step=None):
|
|
13
|
+
"""Crop on the true FFT axis and interpolate exact band endpoints.
|
|
14
|
+
|
|
15
|
+
Fractional sample rates and integer FFT lengths need not place a bin
|
|
16
|
+
exactly on the requested limits. Never round bins before selecting them
|
|
17
|
+
or relabel an out-of-band bin as the boundary. Interpolate power between
|
|
18
|
+
the two surrounding bins instead, without extrapolating beyond the axis.
|
|
19
|
+
The frequency dimension is the first axis of ``power``.
|
|
20
|
+
With ``frequency_step``, interpolate onto a regular zero-anchored grid
|
|
21
|
+
(e.g. 0.5, 0.6, ..., 30 Hz for a ten-second FFT setting).
|
|
22
|
+
"""
|
|
23
|
+
low, high = band
|
|
24
|
+
if frequency_step is not None:
|
|
25
|
+
if not np.isfinite(frequency_step) or frequency_step <= 0:
|
|
26
|
+
raise ValueError("frequency_step must be finite and positive")
|
|
27
|
+
low, high = max(low, freq[0]), min(high, freq[-1])
|
|
28
|
+
if low > high:
|
|
29
|
+
return freq[:0], power[:0]
|
|
30
|
+
first = int(np.ceil(low / frequency_step))
|
|
31
|
+
last = int(np.floor(high / frequency_step))
|
|
32
|
+
grid = np.round(np.arange(first, last + 1) * frequency_step, 12)
|
|
33
|
+
grid = grid[(grid > low) & (grid < high)]
|
|
34
|
+
grid = np.unique(np.concatenate(([low], grid, [high])))
|
|
35
|
+
right = np.clip(np.searchsorted(freq, grid), 1, len(freq) - 1)
|
|
36
|
+
left = right - 1
|
|
37
|
+
weight = (grid - freq[left]) / (freq[right] - freq[left])
|
|
38
|
+
weight = weight.reshape((-1,) + (1,) * (power.ndim - 1))
|
|
39
|
+
values = power[left] + weight * (power[right] - power[left])
|
|
40
|
+
return grid, values
|
|
41
|
+
selected = (freq >= low) & (freq <= high)
|
|
42
|
+
result_freq = freq[selected]
|
|
43
|
+
result_power = power[selected]
|
|
44
|
+
for boundary, prepend in ((low, True), (high, False)):
|
|
45
|
+
if not freq[0] <= boundary <= freq[-1]:
|
|
46
|
+
continue
|
|
47
|
+
if np.any(result_freq == boundary):
|
|
48
|
+
continue
|
|
49
|
+
right = np.searchsorted(freq, boundary)
|
|
50
|
+
left = right - 1
|
|
51
|
+
weight = (boundary - freq[left]) / (freq[right] - freq[left])
|
|
52
|
+
value = power[left] + weight * (power[right] - power[left])
|
|
53
|
+
if prepend:
|
|
54
|
+
result_freq = np.concatenate(([boundary], result_freq))
|
|
55
|
+
result_power = np.concatenate((value[np.newaxis], result_power))
|
|
56
|
+
else:
|
|
57
|
+
result_freq = np.concatenate((result_freq, [boundary]))
|
|
58
|
+
result_power = np.concatenate((result_power, value[np.newaxis]))
|
|
59
|
+
return result_freq, result_power
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def spectrum(signal, sf, band=None, relative=True, win_sec=1, nfft=None, gaussian_sigma=None,
|
|
63
|
+
frequency_step=None):
|
|
13
64
|
"""Calculate the (Welch) power spectrum of a signal.
|
|
14
65
|
|
|
15
66
|
The signal is band-pass filtered to ``band`` first, then the power
|
|
@@ -31,6 +82,8 @@ def spectrum(signal, sf, band=None, relative=True, win_sec=1, nfft=None, gaussia
|
|
|
31
82
|
Number of FFT points.
|
|
32
83
|
gaussian_sigma : float, optional
|
|
33
84
|
Sigma for Gaussian smoothing of the PSD.
|
|
85
|
+
frequency_step : float, optional
|
|
86
|
+
Interpolate onto this regular frequency grid for display/export.
|
|
34
87
|
|
|
35
88
|
Returns
|
|
36
89
|
-------
|
|
@@ -54,21 +107,19 @@ def spectrum(signal, sf, band=None, relative=True, win_sec=1, nfft=None, gaussia
|
|
|
54
107
|
signal, _ = signal_filter(data=signal, sf=sf, btype="bandpass", low=band[0], high=band[1])
|
|
55
108
|
|
|
56
109
|
freq, psd = welch(signal, sf, nperseg=int(sf * win_sec), nfft=nfft, scaling="density")
|
|
57
|
-
freq = np.array([round(each, 2) for each in freq])
|
|
58
110
|
psd = gaussian_filter1d(psd, sigma=gaussian_sigma) if gaussian_sigma is not None else psd
|
|
59
111
|
|
|
60
|
-
|
|
61
|
-
freq = freq[idx_freq]
|
|
62
|
-
psd = psd[idx_freq]
|
|
112
|
+
freq, psd = _select_frequency_band(freq, psd, band, frequency_step)
|
|
63
113
|
|
|
64
|
-
total_power = simpson(psd,
|
|
114
|
+
total_power = simpson(psd, x=freq)
|
|
65
115
|
if relative and total_power > 0:
|
|
66
116
|
psd /= total_power
|
|
67
117
|
|
|
68
118
|
return freq, psd
|
|
69
119
|
|
|
70
120
|
|
|
71
|
-
def spectrogram(signal, sf, band=None, step=0.2, win_sec=2, norm=False, nfft=None
|
|
121
|
+
def spectrogram(signal, sf, band=None, step=0.2, win_sec=2, norm=False, nfft=None,
|
|
122
|
+
frequency_step=None):
|
|
72
123
|
"""Calculate the spectrogram of a signal with the STFT.
|
|
73
124
|
|
|
74
125
|
Parameters
|
|
@@ -87,6 +138,8 @@ def spectrogram(signal, sf, band=None, step=0.2, win_sec=2, norm=False, nfft=Non
|
|
|
87
138
|
Whether to normalize the power across frequencies at each time point.
|
|
88
139
|
nfft : int, optional
|
|
89
140
|
Number of FFT points.
|
|
141
|
+
frequency_step : float, optional
|
|
142
|
+
Interpolate power onto this regular frequency grid for display/export.
|
|
90
143
|
|
|
91
144
|
Returns
|
|
92
145
|
-------
|
|
@@ -120,12 +173,8 @@ def spectrogram(signal, sf, band=None, step=0.2, win_sec=2, norm=False, nfft=Non
|
|
|
120
173
|
f, t, Sxx = stft(signal, sf, nperseg=nperseg, nfft=nfft,
|
|
121
174
|
noverlap=noverlap, padded=False, boundary="zeros")
|
|
122
175
|
|
|
123
|
-
f = np.array([round(each, 2) for each in f])
|
|
124
|
-
|
|
125
|
-
idx_f = np.logical_and(f >= band[0], f <= band[1])
|
|
126
|
-
f = f[idx_f]
|
|
127
|
-
Sxx = Sxx[idx_f, :]
|
|
128
176
|
Sxx = np.square(np.abs(Sxx))
|
|
177
|
+
f, Sxx = _select_frequency_band(f, Sxx, band, frequency_step)
|
|
129
178
|
|
|
130
179
|
if norm:
|
|
131
180
|
sum_power = Sxx.sum(0).reshape(1, -1)
|
|
@@ -153,14 +202,13 @@ def band_power(psd, freq, bands=None, relative=False):
|
|
|
153
202
|
dict
|
|
154
203
|
Band name -> band power.
|
|
155
204
|
"""
|
|
156
|
-
freq_res = freq[1] - freq[0]
|
|
157
205
|
band_dict = {}
|
|
158
206
|
for each in bands:
|
|
159
|
-
|
|
160
|
-
bp = simpson(
|
|
207
|
+
band_freq, band_psd = _select_frequency_band(freq, psd, each[:2])
|
|
208
|
+
bp = simpson(band_psd, x=band_freq)
|
|
161
209
|
|
|
162
210
|
if relative:
|
|
163
|
-
total = simpson(psd,
|
|
211
|
+
total = simpson(psd, x=freq)
|
|
164
212
|
if total > 0:
|
|
165
213
|
bp /= total
|
|
166
214
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: misleep
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: MiSleep: Mice sleep EEG/EMG visualization, scoring, and analysis
|
|
5
5
|
Author-email: Xueqiang Wang <swang9194@gmail.com>
|
|
6
6
|
Maintainer-email: Xueqiang Wang <swang9194@gmail.com>
|
|
@@ -78,6 +78,7 @@ Requires-Dist: lightgbm>=3.3; extra == "full"
|
|
|
78
78
|
Provides-Extra: dev
|
|
79
79
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
80
80
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
81
|
+
Dynamic: license-file
|
|
81
82
|
|
|
82
83
|
# MiSleep
|
|
83
84
|
|
|
@@ -96,6 +96,20 @@ def test_model_path():
|
|
|
96
96
|
assert p.exists()
|
|
97
97
|
|
|
98
98
|
|
|
99
|
+
def test_benchmark_model_loads_through_numpy1_compat(monkeypatch):
|
|
100
|
+
"""NumPy 1.x users can read the model serialized under NumPy 2.x."""
|
|
101
|
+
from misleep.analysis.autostage import benchmark
|
|
102
|
+
|
|
103
|
+
benchmark.load_models.cache_clear()
|
|
104
|
+
monkeypatch.setattr(benchmark.np, "__version__", "1.26.4")
|
|
105
|
+
models = benchmark.load_models()
|
|
106
|
+
assert set(models) == {
|
|
107
|
+
"eegf", "eegp", "eegf_emg", "eegp_emg",
|
|
108
|
+
"eegf_emg_acc", "eegp_emg_acc",
|
|
109
|
+
}
|
|
110
|
+
benchmark.load_models.cache_clear()
|
|
111
|
+
|
|
112
|
+
|
|
99
113
|
def test_auto_stage_gbm():
|
|
100
114
|
sf = 256.0
|
|
101
115
|
eeg = make_signal(sf=sf, duration=100, seed=2)
|