accusleepy 0.10.0__tar.gz → 0.11.0__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.
- {accusleepy-0.10.0 → accusleepy-0.11.0}/PKG-INFO +10 -11
- {accusleepy-0.10.0 → accusleepy-0.11.0}/README.md +6 -6
- accusleepy-0.11.0/accusleepy/__init__.py +1 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/__main__.py +2 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/bouts.py +2 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/brain_state_set.py +2 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/classification.py +2 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/constants.py +5 -2
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/fileio.py +26 -11
- accusleepy-0.11.0/accusleepy/gui/__init__.py +1 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/images/primary_window.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/main.py +17 -24
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/manual_scoring.py +15 -12
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/mplwidget.py +5 -6
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/primary_window.py +3 -5
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/primary_window.ui +0 -1
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/recording_manager.py +24 -14
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/settings_widget.py +1 -1
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/viewer_window.py +2 -4
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/models.py +2 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/multitaper.py +13 -66
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/services.py +4 -4
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/signal_processing.py +9 -4
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/temperature_scaling.py +5 -3
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/validation.py +19 -19
- {accusleepy-0.10.0 → accusleepy-0.11.0}/pyproject.toml +8 -5
- accusleepy-0.10.0/accusleepy/__init__.py +0 -0
- accusleepy-0.10.0/accusleepy/gui/__init__.py +0 -0
- /accusleepy-0.10.0/accusleepy/config.json → /accusleepy-0.11.0/accusleepy/default_config.json +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/dialogs.py +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/brightness_down.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/brightness_up.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/double_down_arrow.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/double_up_arrow.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/down_arrow.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/home.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/question.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/save.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/up_arrow.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/zoom_in.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/icons/zoom_out.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/images/viewer_window.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/images/viewer_window_annotated.png +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/resources.qrc +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/resources_rc.py +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/text/dev_guide.md +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/text/main_guide.md +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/text/manual_scoring_guide.md +0 -0
- {accusleepy-0.10.0 → accusleepy-0.11.0}/accusleepy/gui/viewer_window.ui +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accusleepy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Python implementation of AccuSleep
|
|
5
5
|
License: GPL-3.0-only
|
|
6
6
|
Author: Zeke Barger
|
|
@@ -11,14 +11,13 @@ Classifier: Programming Language :: Python :: 3
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
-
Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
|
|
15
|
-
Requires-Dist: joblib (>=1.4.2,<2.0.0)
|
|
16
14
|
Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
|
|
17
15
|
Requires-Dist: numpy (>=2.2.4,<3.0.0)
|
|
18
16
|
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
19
17
|
Requires-Dist: pillow (>=11.1.0,<12.0.0)
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
18
|
+
Requires-Dist: platformdirs (>=4.0.0,<5.0.0)
|
|
19
|
+
Requires-Dist: pyarrow (>=23.0.0,<24.0.0)
|
|
20
|
+
Requires-Dist: pyside6 (>=6.10.1,<7.0.0)
|
|
22
21
|
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
23
22
|
Requires-Dist: torch (>=2.8.0,<3.0.0)
|
|
24
23
|
Requires-Dist: torchvision (>=0.23.0,<1.0.0)
|
|
@@ -62,13 +61,14 @@ etc.) with python >=3.11,<3.14
|
|
|
62
61
|
- `pip install accusleepy`
|
|
63
62
|
- (optional) download a classification model from https://osf.io/py5eb/ under /python_format/models/
|
|
64
63
|
|
|
65
|
-
Note that upgrading or reinstalling the package will overwrite any changes
|
|
66
|
-
to the [config file](accusleepy/config.json).
|
|
67
64
|
|
|
68
65
|
## Usage
|
|
69
66
|
|
|
70
67
|
`python -m accusleepy` will open the primary interface.
|
|
71
68
|
|
|
69
|
+
Your settings are saved to a platform-specific location
|
|
70
|
+
(e.g., `~/Library/Application Support/accusleepy/config.json` on macOS)
|
|
71
|
+
|
|
72
72
|
[Guide to the primary interface](accusleepy/gui/text/main_guide.md)
|
|
73
73
|
|
|
74
74
|
[Guide to the manual scoring interface](accusleepy/gui/text/manual_scoring_guide.md)
|
|
@@ -79,10 +79,9 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
79
79
|
|
|
80
80
|
## Changelog
|
|
81
81
|
|
|
82
|
-
- 0.
|
|
83
|
-
- 0.
|
|
84
|
-
- 0.
|
|
85
|
-
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
82
|
+
- 0.11.0: Store config file in a user directory
|
|
83
|
+
- 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
|
|
84
|
+
- 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
|
|
86
85
|
- 0.7.0: More settings can be configured in the UI
|
|
87
86
|
- 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
|
|
88
87
|
since the new calibration feature will make the confidence scores more accurate.
|
|
@@ -35,13 +35,14 @@ etc.) with python >=3.11,<3.14
|
|
|
35
35
|
- `pip install accusleepy`
|
|
36
36
|
- (optional) download a classification model from https://osf.io/py5eb/ under /python_format/models/
|
|
37
37
|
|
|
38
|
-
Note that upgrading or reinstalling the package will overwrite any changes
|
|
39
|
-
to the [config file](accusleepy/config.json).
|
|
40
38
|
|
|
41
39
|
## Usage
|
|
42
40
|
|
|
43
41
|
`python -m accusleepy` will open the primary interface.
|
|
44
42
|
|
|
43
|
+
Your settings are saved to a platform-specific location
|
|
44
|
+
(e.g., `~/Library/Application Support/accusleepy/config.json` on macOS)
|
|
45
|
+
|
|
45
46
|
[Guide to the primary interface](accusleepy/gui/text/main_guide.md)
|
|
46
47
|
|
|
47
48
|
[Guide to the manual scoring interface](accusleepy/gui/text/manual_scoring_guide.md)
|
|
@@ -52,10 +53,9 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
52
53
|
|
|
53
54
|
## Changelog
|
|
54
55
|
|
|
55
|
-
- 0.
|
|
56
|
-
- 0.
|
|
57
|
-
- 0.
|
|
58
|
-
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
56
|
+
- 0.11.0: Store config file in a user directory
|
|
57
|
+
- 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
|
|
58
|
+
- 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
|
|
59
59
|
- 0.7.0: More settings can be configured in the UI
|
|
60
60
|
- 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
|
|
61
61
|
since the new calibration feature will make the confidence scores more accurate.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AccuSleePy: Python implementation of AccuSleep for automated sleep scoring."""
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Application-wide constants and default settings."""
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
|
|
3
5
|
# probably don't change these unless you really need to
|
|
@@ -24,8 +26,9 @@ MIN_EPOCHS_PER_STATE = 3
|
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
# very unlikely you will want to change values from here onwards
|
|
27
|
-
# config file
|
|
28
|
-
|
|
29
|
+
# config file names
|
|
30
|
+
DEFAULT_CONFIG_FILE = "default_config.json"
|
|
31
|
+
USER_CONFIG_FILE = "config.json"
|
|
29
32
|
# number of times to include the EMG power in a training image
|
|
30
33
|
EMG_COPIES = 9
|
|
31
34
|
# minimum spectrogram window length, in seconds
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
"""File I/O for recordings, labels, calibration data, and configuration."""
|
|
2
|
+
|
|
1
3
|
import json
|
|
2
4
|
import os
|
|
5
|
+
import shutil
|
|
3
6
|
from dataclasses import dataclass
|
|
4
|
-
from importlib.metadata import
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from importlib.resources import files
|
|
5
9
|
|
|
6
10
|
import numpy as np
|
|
7
11
|
import pandas as pd
|
|
8
|
-
from
|
|
12
|
+
from platformdirs import user_config_dir
|
|
9
13
|
|
|
10
|
-
from accusleepy.brain_state_set import BRAIN_STATES_KEY, BrainState, BrainStateSet
|
|
11
14
|
import accusleepy.constants as c
|
|
15
|
+
from accusleepy.brain_state_set import BRAIN_STATES_KEY, BrainState, BrainStateSet
|
|
12
16
|
|
|
13
17
|
|
|
14
18
|
@dataclass
|
|
@@ -55,7 +59,6 @@ class Recording:
|
|
|
55
59
|
label_file: str = "" # path to label file
|
|
56
60
|
calibration_file: str = "" # path to calibration file
|
|
57
61
|
sampling_rate: int | float = 0.0 # sampling rate, in Hz
|
|
58
|
-
widget: QListWidgetItem = None # list item widget shown in the GUI
|
|
59
62
|
|
|
60
63
|
|
|
61
64
|
def load_calibration_file(filename: str) -> tuple[np.ndarray, np.ndarray]:
|
|
@@ -128,6 +131,16 @@ def save_labels(
|
|
|
128
131
|
pd.DataFrame({c.BRAIN_STATE_COL: labels}).to_csv(filename, index=False)
|
|
129
132
|
|
|
130
133
|
|
|
134
|
+
def _get_user_config_path() -> str:
|
|
135
|
+
"""Return the path to the user's config file in the platform config directory."""
|
|
136
|
+
return os.path.join(user_config_dir("accusleepy"), c.USER_CONFIG_FILE)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _get_default_config_path() -> str:
|
|
140
|
+
"""Return the path to the bundled default config file."""
|
|
141
|
+
return str(files("accusleepy").joinpath(c.DEFAULT_CONFIG_FILE))
|
|
142
|
+
|
|
143
|
+
|
|
131
144
|
def load_config() -> AccuSleePyConfig:
|
|
132
145
|
"""Load configuration file with brain state options
|
|
133
146
|
|
|
@@ -143,9 +156,11 @@ def load_config() -> AccuSleePyConfig:
|
|
|
143
156
|
default autoscroll state for manual scoring,
|
|
144
157
|
setting to delete training images automatically
|
|
145
158
|
"""
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
user_config = _get_user_config_path()
|
|
160
|
+
if not os.path.exists(user_config):
|
|
161
|
+
os.makedirs(os.path.dirname(user_config), exist_ok=True)
|
|
162
|
+
shutil.copy2(_get_default_config_path(), user_config)
|
|
163
|
+
with open(user_config) as f:
|
|
149
164
|
data = json.load(f)
|
|
150
165
|
|
|
151
166
|
return AccuSleePyConfig(
|
|
@@ -229,9 +244,9 @@ def save_config(
|
|
|
229
244
|
output_dict.update({c.EPOCHS_TO_SHOW_KEY: epochs_to_show})
|
|
230
245
|
output_dict.update({c.AUTOSCROLL_KEY: autoscroll_state})
|
|
231
246
|
output_dict.update({c.DELETE_TRAINING_IMAGES_KEY: delete_training_images})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
) as f:
|
|
247
|
+
user_config = _get_user_config_path()
|
|
248
|
+
os.makedirs(os.path.dirname(user_config), exist_ok=True)
|
|
249
|
+
with open(user_config, "w") as f:
|
|
235
250
|
json.dump(output_dict, f, indent=4)
|
|
236
251
|
f.write("\n")
|
|
237
252
|
|
|
@@ -242,7 +257,7 @@ def load_recording_list(filename: str) -> list[Recording]:
|
|
|
242
257
|
:param filename: filename of list of recordings
|
|
243
258
|
:return: list of recordings
|
|
244
259
|
"""
|
|
245
|
-
with open(filename
|
|
260
|
+
with open(filename) as f:
|
|
246
261
|
data = json.load(f)
|
|
247
262
|
recording_list = [Recording(**r) for r in data[c.RECORDING_LIST_NAME]]
|
|
248
263
|
for i, r in enumerate(recording_list):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Graphical user interface components for AccuSleePy."""
|
|
Binary file
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""AccuSleePy main window.
|
|
2
|
+
|
|
3
|
+
Icon source: Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
4
|
+
"""
|
|
3
5
|
|
|
4
6
|
import logging
|
|
5
7
|
import os
|
|
@@ -39,10 +41,10 @@ from accusleepy.constants import (
|
|
|
39
41
|
UNDEFINED_LABEL,
|
|
40
42
|
)
|
|
41
43
|
from accusleepy.fileio import (
|
|
44
|
+
get_version,
|
|
42
45
|
load_config,
|
|
43
46
|
load_labels,
|
|
44
47
|
load_recording,
|
|
45
|
-
get_version,
|
|
46
48
|
)
|
|
47
49
|
from accusleepy.gui.dialogs import select_existing_file, select_save_location
|
|
48
50
|
from accusleepy.gui.manual_scoring import ManualScoringWindow
|
|
@@ -56,9 +58,8 @@ from accusleepy.services import (
|
|
|
56
58
|
create_calibration,
|
|
57
59
|
score_recording_list,
|
|
58
60
|
)
|
|
59
|
-
from accusleepy.validation import validate_and_correct_labels
|
|
60
61
|
from accusleepy.signal_processing import resample_and_standardize
|
|
61
|
-
from accusleepy.validation import check_config_consistency
|
|
62
|
+
from accusleepy.validation import check_config_consistency, validate_and_correct_labels
|
|
62
63
|
|
|
63
64
|
logger = logging.getLogger(__name__)
|
|
64
65
|
|
|
@@ -93,7 +94,7 @@ class AccuSleepWindow(QMainWindow):
|
|
|
93
94
|
"""AccuSleePy primary window"""
|
|
94
95
|
|
|
95
96
|
def __init__(self):
|
|
96
|
-
super(
|
|
97
|
+
super().__init__()
|
|
97
98
|
|
|
98
99
|
# initialize the UI
|
|
99
100
|
self.ui = Ui_PrimaryWindow()
|
|
@@ -361,22 +362,18 @@ class AccuSleepWindow(QMainWindow):
|
|
|
361
362
|
except Exception:
|
|
362
363
|
logger.exception("Failed to load %s", filename)
|
|
363
364
|
self.show_message(
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
"user manual for instructions on creating this file."
|
|
367
|
-
)
|
|
365
|
+
"ERROR: could not load classification model. Check "
|
|
366
|
+
"user manual for instructions on creating this file."
|
|
368
367
|
)
|
|
369
368
|
return
|
|
370
369
|
|
|
371
370
|
# make sure only "default" model type is loaded
|
|
372
371
|
if model_type != DEFAULT_MODEL_TYPE:
|
|
373
372
|
self.show_message(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"for an example of how to classify brain states in real time."
|
|
379
|
-
)
|
|
373
|
+
"ERROR: only 'default'-style models can be used. "
|
|
374
|
+
"'Real-time' models are not supported. "
|
|
375
|
+
"See classification.example_real_time_scoring_function.py "
|
|
376
|
+
"for an example of how to classify brain states in real time."
|
|
380
377
|
)
|
|
381
378
|
return
|
|
382
379
|
|
|
@@ -431,10 +428,8 @@ class AccuSleepWindow(QMainWindow):
|
|
|
431
428
|
)
|
|
432
429
|
status_widget.setText("could not load recording")
|
|
433
430
|
self.show_message(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
"Check user manual for formatting instructions."
|
|
437
|
-
)
|
|
431
|
+
"ERROR: could not load recording. "
|
|
432
|
+
"Check user manual for formatting instructions."
|
|
438
433
|
)
|
|
439
434
|
return None, None, None, False
|
|
440
435
|
|
|
@@ -504,10 +499,8 @@ class AccuSleepWindow(QMainWindow):
|
|
|
504
499
|
logger.exception("Failed to load %s", label_file)
|
|
505
500
|
self.ui.manual_scoring_status.setText("could not load labels")
|
|
506
501
|
self.show_message(
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
"Check user manual for formatting instructions."
|
|
510
|
-
)
|
|
502
|
+
"ERROR: could not load labels. "
|
|
503
|
+
"Check user manual for formatting instructions."
|
|
511
504
|
)
|
|
512
505
|
return
|
|
513
506
|
else:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
# Icon sources:
|
|
3
|
-
# Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
4
|
-
# kendis lasman, https://www.flaticon.com/packs/ui-79
|
|
1
|
+
"""AccuSleePy manual scoring GUI.
|
|
5
2
|
|
|
3
|
+
Icon sources:
|
|
4
|
+
Arkinasi, https://www.flaticon.com/authors/arkinasi
|
|
5
|
+
kendis lasman, https://www.flaticon.com/packs/ui-79
|
|
6
|
+
"""
|
|
6
7
|
|
|
7
8
|
import copy
|
|
8
9
|
import os
|
|
@@ -33,7 +34,7 @@ from PySide6.QtWidgets import (
|
|
|
33
34
|
)
|
|
34
35
|
|
|
35
36
|
from accusleepy.constants import UNDEFINED_LABEL
|
|
36
|
-
from accusleepy.fileio import load_config, save_labels
|
|
37
|
+
from accusleepy.fileio import EMGFilter, load_config, save_labels
|
|
37
38
|
from accusleepy.gui.mplwidget import resample_x_ticks
|
|
38
39
|
from accusleepy.gui.viewer_window import Ui_ViewerWindow
|
|
39
40
|
from accusleepy.signal_processing import create_spectrogram, get_emg_power
|
|
@@ -85,8 +86,8 @@ BRIGHTER_SCALE_FACTOR = 0.96
|
|
|
85
86
|
DIMMER_SCALE_FACTOR = 1.07
|
|
86
87
|
# zoom factor for upper plots - larger values = bigger changes
|
|
87
88
|
ZOOM_FACTOR = 0.1
|
|
88
|
-
#
|
|
89
|
-
|
|
89
|
+
# rate limit for zoom events triggered by scrolling
|
|
90
|
+
MAX_SCROLL_EVENTS_PER_SEC = 24
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
@dataclass
|
|
@@ -123,7 +124,7 @@ class ManualScoringWindow(QDialog):
|
|
|
123
124
|
:param epoch_length: epoch length, in seconds
|
|
124
125
|
:param emg_filter: EMG filter parameters
|
|
125
126
|
"""
|
|
126
|
-
super(
|
|
127
|
+
super().__init__()
|
|
127
128
|
|
|
128
129
|
self.label_file = label_file
|
|
129
130
|
self.eeg = eeg
|
|
@@ -914,9 +915,7 @@ class ManualScoringWindow(QDialog):
|
|
|
914
915
|
)
|
|
915
916
|
self.ui.lowerfigure.canvas.axes[1].set_xticklabels(
|
|
916
917
|
[
|
|
917
|
-
"{:02d}:{:02d}:{:05.2f}"
|
|
918
|
-
int(x // 3600), int(x // 60) % 60, (x % 60)
|
|
919
|
-
)
|
|
918
|
+
f"{int(x // 3600):02d}:{int(x // 60) % 60:02d}:{x % 60:05.2f}"
|
|
920
919
|
for x in x_ticks * self.epoch_length
|
|
921
920
|
]
|
|
922
921
|
)
|
|
@@ -983,11 +982,15 @@ class ManualScoringWindow(QDialog):
|
|
|
983
982
|
return
|
|
984
983
|
|
|
985
984
|
self.now_zooming = True
|
|
985
|
+
start_time = time.time()
|
|
986
986
|
if event.button == "up":
|
|
987
987
|
self.zoom_x(direction=ZOOM_IN)
|
|
988
988
|
else:
|
|
989
989
|
self.zoom_x(direction=ZOOM_OUT)
|
|
990
|
-
time.
|
|
990
|
+
end_time = time.time()
|
|
991
|
+
time_elapsed = end_time - start_time
|
|
992
|
+
if time_elapsed < 1 / MAX_SCROLL_EVENTS_PER_SEC:
|
|
993
|
+
time.sleep(1 / MAX_SCROLL_EVENTS_PER_SEC - time_elapsed)
|
|
991
994
|
self.now_zooming = False
|
|
992
995
|
|
|
993
996
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
"""Matplotlib FigureCanvas widget for manual scoring."""
|
|
2
|
+
|
|
2
3
|
from collections.abc import Callable
|
|
3
4
|
|
|
4
5
|
import matplotlib.ticker as mticker
|
|
@@ -293,7 +294,7 @@ class MplWidget(QtWidgets.QWidget):
|
|
|
293
294
|
[1 - marker_dy, 1],
|
|
294
295
|
]
|
|
295
296
|
)
|
|
296
|
-
for x, y in zip(marker_x, marker_y):
|
|
297
|
+
for x, y in zip(marker_x, marker_y, strict=True):
|
|
297
298
|
self.top_marker.append(axes[0].plot(x, y - marker_y_offset_top, "r")[0])
|
|
298
299
|
|
|
299
300
|
# EMG subplot
|
|
@@ -317,7 +318,7 @@ class MplWidget(QtWidgets.QWidget):
|
|
|
317
318
|
linewidth=0.5,
|
|
318
319
|
)[0]
|
|
319
320
|
|
|
320
|
-
for x, y in zip(marker_x, marker_y):
|
|
321
|
+
for x, y in zip(marker_x, marker_y, strict=True):
|
|
321
322
|
self.bottom_marker.append(
|
|
322
323
|
axes[1].plot(x, -1 * (y - marker_y_offset_bottom), "r")[0]
|
|
323
324
|
)
|
|
@@ -355,9 +356,7 @@ class MplWidget(QtWidgets.QWidget):
|
|
|
355
356
|
|
|
356
357
|
def time_formatter(self, x, pos):
|
|
357
358
|
x = x * self.epoch_length
|
|
358
|
-
return "{:02d}:{:02d}:{:05.2f}"
|
|
359
|
-
int(x // 3600), int(x // 60) % 60, (x % 60)
|
|
360
|
-
)
|
|
359
|
+
return f"{int(x // 3600):02d}:{int(x // 60) % 60:02d}:{x % 60:05.2f}"
|
|
361
360
|
|
|
362
361
|
|
|
363
362
|
def resample_x_ticks(x_ticks: np.array) -> np.array:
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
1
|
################################################################################
|
|
4
2
|
## Form generated from reading UI file 'primary_window.ui'
|
|
5
3
|
##
|
|
6
|
-
## Created by: Qt User Interface Compiler version 6.
|
|
4
|
+
## Created by: Qt User Interface Compiler version 6.10.2
|
|
7
5
|
##
|
|
8
6
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
9
7
|
################################################################################
|
|
@@ -32,10 +30,11 @@ from PySide6.QtWidgets import (
|
|
|
32
30
|
QVBoxLayout,
|
|
33
31
|
QWidget,
|
|
34
32
|
)
|
|
33
|
+
|
|
35
34
|
import accusleepy.gui.resources_rc # noqa F401
|
|
36
35
|
|
|
37
36
|
|
|
38
|
-
class Ui_PrimaryWindow
|
|
37
|
+
class Ui_PrimaryWindow:
|
|
39
38
|
def setupUi(self, PrimaryWindow):
|
|
40
39
|
if not PrimaryWindow.objectName():
|
|
41
40
|
PrimaryWindow.setObjectName("PrimaryWindow")
|
|
@@ -3148,7 +3147,6 @@ class Ui_PrimaryWindow(object):
|
|
|
3148
3147
|
"This is the current set of brain states. Important notes:\n"
|
|
3149
3148
|
"- You must click 'Save settings' for changes to take effect.\n"
|
|
3150
3149
|
"- Changing these settings can prevent existing label files, calibration files, and trained models from working properly.\n"
|
|
3151
|
-
"- Reinstalling AccuSleePy will overwrite this configuration.\n"
|
|
3152
3150
|
"\n"
|
|
3153
3151
|
"Each brain state has several attributes:\n"
|
|
3154
3152
|
"- Digit: the indicator for this state in label files, and the keyboard shortcut for this state in manual scoring.\n"
|
|
@@ -3701,7 +3701,6 @@ color: rgb(244, 195, 68);</string>
|
|
|
3701
3701
|
<string>This is the current set of brain states. Important notes:
|
|
3702
3702
|
- You must click 'Save settings' for changes to take effect.
|
|
3703
3703
|
- Changing these settings can prevent existing label files, calibration files, and trained models from working properly.
|
|
3704
|
-
- Reinstalling AccuSleePy will overwrite this configuration.
|
|
3705
3704
|
|
|
3706
3705
|
Each brain state has several attributes:
|
|
3707
3706
|
- Digit: the indicator for this state in label files, and the keyboard shortcut for this state in manual scoring.
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
"""Recording list manager"""
|
|
2
2
|
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
3
5
|
from PySide6.QtCore import QObject
|
|
4
6
|
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
|
5
7
|
|
|
6
8
|
from accusleepy.fileio import Recording, load_recording_list, save_recording_list
|
|
7
9
|
|
|
8
10
|
|
|
11
|
+
@dataclass
|
|
12
|
+
class RecordingListItem(Recording):
|
|
13
|
+
"""A Recording with an associated QListWidget item for the GUI"""
|
|
14
|
+
|
|
15
|
+
widget: QListWidgetItem = None
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
class RecordingListManager(QObject):
|
|
10
19
|
"""Manages the list of recordings and the associated QListWidget"""
|
|
11
20
|
|
|
@@ -14,19 +23,19 @@ class RecordingListManager(QObject):
|
|
|
14
23
|
self._widget = list_widget
|
|
15
24
|
|
|
16
25
|
# Create initial empty recording (there is always at least one)
|
|
17
|
-
first_recording =
|
|
26
|
+
first_recording = RecordingListItem(
|
|
18
27
|
widget=QListWidgetItem("Recording 1", self._widget),
|
|
19
28
|
)
|
|
20
|
-
self._recordings: list[
|
|
29
|
+
self._recordings: list[RecordingListItem] = [first_recording]
|
|
21
30
|
self._widget.addItem(first_recording.widget)
|
|
22
31
|
self._widget.setCurrentRow(0)
|
|
23
32
|
|
|
24
33
|
@property
|
|
25
|
-
def current(self) ->
|
|
34
|
+
def current(self) -> RecordingListItem:
|
|
26
35
|
"""The currently selected recording"""
|
|
27
36
|
return self._recordings[self._widget.currentRow()]
|
|
28
37
|
|
|
29
|
-
def add(self, sampling_rate: int | float) ->
|
|
38
|
+
def add(self, sampling_rate: int | float) -> RecordingListItem:
|
|
30
39
|
"""Add a new recording to the list
|
|
31
40
|
|
|
32
41
|
:param sampling_rate: sampling rate for the new recording
|
|
@@ -35,7 +44,7 @@ class RecordingListManager(QObject):
|
|
|
35
44
|
new_name = max(r.name for r in self._recordings) + 1
|
|
36
45
|
|
|
37
46
|
# Create recording with widget
|
|
38
|
-
recording =
|
|
47
|
+
recording = RecordingListItem(
|
|
39
48
|
name=new_name,
|
|
40
49
|
sampling_rate=sampling_rate,
|
|
41
50
|
widget=QListWidgetItem(f"Recording {new_name}", self._widget),
|
|
@@ -63,7 +72,7 @@ class RecordingListManager(QObject):
|
|
|
63
72
|
else:
|
|
64
73
|
# Reset the single recording to defaults
|
|
65
74
|
recording_name = self._recordings[0].name
|
|
66
|
-
self._recordings[0] =
|
|
75
|
+
self._recordings[0] = RecordingListItem(widget=self._recordings[0].widget)
|
|
67
76
|
self._recordings[0].widget.setText(f"Recording {self._recordings[0].name}")
|
|
68
77
|
return f"cleared Recording {recording_name}"
|
|
69
78
|
|
|
@@ -85,14 +94,15 @@ class RecordingListManager(QObject):
|
|
|
85
94
|
try:
|
|
86
95
|
self._widget.clear()
|
|
87
96
|
|
|
88
|
-
# Load recordings
|
|
89
|
-
self._recordings =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
recording.widget = QListWidgetItem(
|
|
94
|
-
f"Recording {recording.name}", self._widget
|
|
97
|
+
# Load recordings and create widgets
|
|
98
|
+
self._recordings = [
|
|
99
|
+
RecordingListItem(
|
|
100
|
+
**r.__dict__,
|
|
101
|
+
widget=QListWidgetItem(f"Recording {r.name}", self._widget),
|
|
95
102
|
)
|
|
103
|
+
for r in load_recording_list(filename)
|
|
104
|
+
]
|
|
105
|
+
for recording in self._recordings:
|
|
96
106
|
self._widget.addItem(recording.widget)
|
|
97
107
|
finally:
|
|
98
108
|
self._widget.blockSignals(False)
|
|
@@ -106,5 +116,5 @@ class RecordingListManager(QObject):
|
|
|
106
116
|
def __len__(self):
|
|
107
117
|
return len(self._recordings)
|
|
108
118
|
|
|
109
|
-
def __getitem__(self, index: int) ->
|
|
119
|
+
def __getitem__(self, index: int) -> RecordingListItem:
|
|
110
120
|
return self._recordings[index]
|
|
@@ -197,7 +197,7 @@ class SettingsWidget(QObject):
|
|
|
197
197
|
# Brain states
|
|
198
198
|
states = {b.digit: b for b in self._brain_state_set.brain_states}
|
|
199
199
|
for digit in range(10):
|
|
200
|
-
if digit in states
|
|
200
|
+
if digit in states:
|
|
201
201
|
self._settings_widgets[digit].enabled_widget.setChecked(True)
|
|
202
202
|
self._settings_widgets[digit].name_widget.setText(states[digit].name)
|
|
203
203
|
self._settings_widgets[digit].is_scored_widget.setChecked(
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
1
|
################################################################################
|
|
4
2
|
## Form generated from reading UI file 'viewer_window.ui'
|
|
5
3
|
##
|
|
@@ -23,11 +21,11 @@ from PySide6.QtWidgets import (
|
|
|
23
21
|
QVBoxLayout,
|
|
24
22
|
)
|
|
25
23
|
|
|
26
|
-
from accusleepy.gui.mplwidget import MplWidget
|
|
27
24
|
import accusleepy.gui.resources_rc # noqa F401
|
|
25
|
+
from accusleepy.gui.mplwidget import MplWidget
|
|
28
26
|
|
|
29
27
|
|
|
30
|
-
class Ui_ViewerWindow
|
|
28
|
+
class Ui_ViewerWindow:
|
|
31
29
|
def setupUi(self, ViewerWindow):
|
|
32
30
|
if not ViewerWindow.objectName():
|
|
33
31
|
ViewerWindow.setObjectName("ViewerWindow")
|
|
@@ -14,10 +14,6 @@ import timeit
|
|
|
14
14
|
import warnings
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
|
-
from joblib import Parallel, cpu_count, delayed
|
|
18
|
-
|
|
19
|
-
# from scipy.signal import detrend # unused by AccuSleePy
|
|
20
|
-
# from scipy.signal.windows import dpss # lazily loaded later
|
|
21
17
|
|
|
22
18
|
|
|
23
19
|
# MULTITAPER SPECTROGRAM #
|
|
@@ -206,19 +202,9 @@ def spectrogram(
|
|
|
206
202
|
wt,
|
|
207
203
|
)
|
|
208
204
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
Parallel(n_jobs=n_jobs)(
|
|
213
|
-
delayed(calc_mts_segment)(data_segments[num_window, :], *mts_params)
|
|
214
|
-
for num_window in range(num_windows)
|
|
215
|
-
)
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
else: # if no multiprocessing, compute normally
|
|
219
|
-
mt_spectrogram = np.apply_along_axis(
|
|
220
|
-
calc_mts_segment, 1, data_segments, *mts_params
|
|
221
|
-
)
|
|
205
|
+
mt_spectrogram = np.apply_along_axis(
|
|
206
|
+
calc_mts_segment, 1, data_segments, *mts_params
|
|
207
|
+
)
|
|
222
208
|
|
|
223
209
|
# Compute one-sided PSD spectrum
|
|
224
210
|
mt_spectrogram = mt_spectrogram.T
|
|
@@ -251,37 +237,6 @@ def spectrogram(
|
|
|
251
237
|
if np.all(mt_spectrogram.flatten() == 0):
|
|
252
238
|
print("\n Data was all zeros, no output")
|
|
253
239
|
|
|
254
|
-
# # Plot multitaper spectrogram
|
|
255
|
-
# if plot_on:
|
|
256
|
-
# # convert from power to dB
|
|
257
|
-
# spect_data = nanpow2db(mt_spectrogram)
|
|
258
|
-
#
|
|
259
|
-
# # Set x and y axes
|
|
260
|
-
# dx = stimes[1] - stimes[0]
|
|
261
|
-
# dy = sfreqs[1] - sfreqs[0]
|
|
262
|
-
# extent = [stimes[0] - dx, stimes[-1] + dx, sfreqs[-1] + dy, sfreqs[0] - dy]
|
|
263
|
-
#
|
|
264
|
-
# # Plot spectrogram
|
|
265
|
-
# if ax is None:
|
|
266
|
-
# fig, ax = plt.subplots()
|
|
267
|
-
# else:
|
|
268
|
-
# fig = ax.get_figure()
|
|
269
|
-
# im = ax.imshow(spect_data, extent=extent, aspect="auto")
|
|
270
|
-
# fig.colorbar(im, ax=ax, label="PSD (dB)", shrink=0.8)
|
|
271
|
-
# ax.set_xlabel("Time (HH:MM:SS)")
|
|
272
|
-
# ax.set_ylabel("Frequency (Hz)")
|
|
273
|
-
# im.set_cmap(plt.cm.get_cmap("cet_rainbow4"))
|
|
274
|
-
# ax.invert_yaxis()
|
|
275
|
-
#
|
|
276
|
-
# # Scale colormap
|
|
277
|
-
# if clim_scale:
|
|
278
|
-
# clim = np.percentile(spect_data, [5, 98]) # from 5th percentile to 98th
|
|
279
|
-
# im.set_clim(clim) # actually change colorbar scale
|
|
280
|
-
#
|
|
281
|
-
# fig.show()
|
|
282
|
-
# if return_fig:
|
|
283
|
-
# return mt_spectrogram, stimes, sfreqs, (fig, ax)
|
|
284
|
-
|
|
285
240
|
return mt_spectrogram, stimes, sfreqs
|
|
286
241
|
|
|
287
242
|
|
|
@@ -376,7 +331,8 @@ def process_input(
|
|
|
376
331
|
+ str(frequency_range[0])
|
|
377
332
|
+ ", "
|
|
378
333
|
+ str(frequency_range[1])
|
|
379
|
-
+ "]"
|
|
334
|
+
+ "]",
|
|
335
|
+
stacklevel=2,
|
|
380
336
|
)
|
|
381
337
|
|
|
382
338
|
# Set number of tapers if none provided
|
|
@@ -387,7 +343,8 @@ def process_input(
|
|
|
387
343
|
if num_tapers != math.floor(2 * time_bandwidth) - 1:
|
|
388
344
|
warnings.warn(
|
|
389
345
|
"Number of tapers is optimal at floor(2*TW) - 1. consider using "
|
|
390
|
-
+ str(math.floor(2 * time_bandwidth) - 1)
|
|
346
|
+
+ str(math.floor(2 * time_bandwidth) - 1),
|
|
347
|
+
stacklevel=2,
|
|
391
348
|
)
|
|
392
349
|
|
|
393
350
|
# If no window params provided, set to defaults
|
|
@@ -400,7 +357,8 @@ def process_input(
|
|
|
400
357
|
warnings.warn(
|
|
401
358
|
"Window size is not divisible by sampling frequency. Adjusting window size to "
|
|
402
359
|
+ str(winsize_samples / fs)
|
|
403
|
-
+ " seconds"
|
|
360
|
+
+ " seconds",
|
|
361
|
+
stacklevel=2,
|
|
404
362
|
)
|
|
405
363
|
else:
|
|
406
364
|
winsize_samples = window_params[0] * fs
|
|
@@ -411,7 +369,8 @@ def process_input(
|
|
|
411
369
|
warnings.warn(
|
|
412
370
|
"Window step size is not divisible by sampling frequency. Adjusting window step size to "
|
|
413
371
|
+ str(winstep_samples / fs)
|
|
414
|
-
+ " seconds"
|
|
372
|
+
+ " seconds",
|
|
373
|
+
stacklevel=2,
|
|
415
374
|
)
|
|
416
375
|
else:
|
|
417
376
|
winstep_samples = window_params[1] * fs
|
|
@@ -553,7 +512,7 @@ def nanpow2db(y):
|
|
|
553
512
|
ydB (float or np array): inputs converted to dB with 0s and negatives resulting in nans
|
|
554
513
|
"""
|
|
555
514
|
|
|
556
|
-
if isinstance(y, int
|
|
515
|
+
if isinstance(y, int | float):
|
|
557
516
|
if y == 0:
|
|
558
517
|
return np.nan
|
|
559
518
|
else:
|
|
@@ -568,18 +527,6 @@ def nanpow2db(y):
|
|
|
568
527
|
return ydB
|
|
569
528
|
|
|
570
529
|
|
|
571
|
-
# Helper #
|
|
572
|
-
def is_outlier(data):
|
|
573
|
-
smad = 1.4826 * np.median(
|
|
574
|
-
abs(data - np.median(data))
|
|
575
|
-
) # scaled median absolute deviation
|
|
576
|
-
outlier_mask = (
|
|
577
|
-
abs(data - np.median(data)) > 3 * smad
|
|
578
|
-
) # outliers are more than 3 smads away from median
|
|
579
|
-
outlier_mask = outlier_mask | np.isnan(data) | np.isinf(data)
|
|
580
|
-
return outlier_mask
|
|
581
|
-
|
|
582
|
-
|
|
583
530
|
# CALCULATE MULTITAPER SPECTRUM ON SINGLE SEGMENT
|
|
584
531
|
def calc_mts_segment(
|
|
585
532
|
data_segment,
|
|
@@ -638,7 +585,7 @@ def calc_mts_segment(
|
|
|
638
585
|
spower_iter = np.mean(spower[:, 0:2], 1)
|
|
639
586
|
spower_iter = spower_iter[:, np.newaxis]
|
|
640
587
|
a = (1 - dpss_eigen) * tpower
|
|
641
|
-
for
|
|
588
|
+
for _i in range(3): # 3 iterations only
|
|
642
589
|
# Calc the MSE weights
|
|
643
590
|
b = np.dot(spower_iter, np.ones((1, num_tapers))) / (
|
|
644
591
|
(np.dot(spower_iter, np.transpose(dpss_eigen)))
|
|
@@ -8,8 +8,8 @@ import datetime
|
|
|
8
8
|
import logging
|
|
9
9
|
import os
|
|
10
10
|
import shutil
|
|
11
|
+
from collections.abc import Callable
|
|
11
12
|
from dataclasses import dataclass, field
|
|
12
|
-
from typing import Callable
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
import pandas as pd
|
|
@@ -21,9 +21,9 @@ from accusleepy.constants import (
|
|
|
21
21
|
CALIBRATION_ANNOTATION_FILENAME,
|
|
22
22
|
DEFAULT_MODEL_TYPE,
|
|
23
23
|
MIN_EPOCHS_PER_STATE,
|
|
24
|
-
UNDEFINED_LABEL,
|
|
25
24
|
MIXTURE_MEAN_COL,
|
|
26
25
|
MIXTURE_SD_COL,
|
|
26
|
+
UNDEFINED_LABEL,
|
|
27
27
|
)
|
|
28
28
|
from accusleepy.fileio import (
|
|
29
29
|
EMGFilter,
|
|
@@ -36,10 +36,10 @@ from accusleepy.fileio import (
|
|
|
36
36
|
)
|
|
37
37
|
from accusleepy.models import SSANN
|
|
38
38
|
from accusleepy.signal_processing import (
|
|
39
|
-
create_training_images,
|
|
40
|
-
resample_and_standardize,
|
|
41
39
|
create_eeg_emg_image,
|
|
40
|
+
create_training_images,
|
|
42
41
|
get_mixture_values,
|
|
42
|
+
resample_and_standardize,
|
|
43
43
|
)
|
|
44
44
|
from accusleepy.validation import check_label_validity
|
|
45
45
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""EEG/EMG signal processing, mixture z-scoring, and training image generation."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
import os
|
|
3
5
|
import warnings
|
|
@@ -19,10 +21,10 @@ from accusleepy.constants import (
|
|
|
19
21
|
LABEL_COL,
|
|
20
22
|
MIN_EPOCHS_PER_STATE,
|
|
21
23
|
MIN_WINDOW_LEN,
|
|
22
|
-
UPPER_FREQ,
|
|
23
24
|
SPECTROGRAM_UPPER_FREQ,
|
|
25
|
+
UPPER_FREQ,
|
|
24
26
|
)
|
|
25
|
-
from accusleepy.fileio import Recording, load_labels, load_recording
|
|
27
|
+
from accusleepy.fileio import EMGFilter, Recording, load_labels, load_recording
|
|
26
28
|
from accusleepy.multitaper import spectrogram
|
|
27
29
|
|
|
28
30
|
# note: scipy is lazily imported
|
|
@@ -149,7 +151,7 @@ def create_spectrogram(
|
|
|
149
151
|
# pad the EEG signal so that the first spectrogram window is centered
|
|
150
152
|
# on the first epoch
|
|
151
153
|
# it's possible there's some jank here, if this isn't close to an integer
|
|
152
|
-
pad_length = round(
|
|
154
|
+
pad_length = round(sampling_rate * (window_length_sec - epoch_length) / 2)
|
|
153
155
|
padded_eeg = np.concatenate(
|
|
154
156
|
[eeg[:pad_length][::-1], eeg, eeg[(len(eeg) - pad_length) :][::-1]]
|
|
155
157
|
)
|
|
@@ -326,7 +328,10 @@ def mixture_z_score_img(
|
|
|
326
328
|
if labels is None and (mixture_means is None or mixture_sds is None):
|
|
327
329
|
raise ValueError("must provide either labels or mixture means+SDs")
|
|
328
330
|
if labels is not None and ((mixture_means is not None) ^ (mixture_sds is not None)):
|
|
329
|
-
warnings.warn(
|
|
331
|
+
warnings.warn(
|
|
332
|
+
"labels were given, mixture means / SDs will be ignored",
|
|
333
|
+
stacklevel=2,
|
|
334
|
+
)
|
|
330
335
|
|
|
331
336
|
if labels is not None:
|
|
332
337
|
mixture_means, mixture_sds = get_mixture_values(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Temperature scaling for model confidence calibration."""
|
|
2
|
+
|
|
1
3
|
import logging
|
|
2
4
|
|
|
3
5
|
import numpy as np
|
|
@@ -18,7 +20,7 @@ class ModelWithTemperature(nn.Module):
|
|
|
18
20
|
"""
|
|
19
21
|
|
|
20
22
|
def __init__(self, model):
|
|
21
|
-
super(
|
|
23
|
+
super().__init__()
|
|
22
24
|
self.model = model
|
|
23
25
|
# https://github.com/gpleiss/temperature_scaling/issues/20
|
|
24
26
|
# for another approach, see https://github.com/gpleiss/temperature_scaling/issues/36
|
|
@@ -140,7 +142,7 @@ class _ECELoss(nn.Module):
|
|
|
140
142
|
"""
|
|
141
143
|
n_bins (int): number of confidence interval bins
|
|
142
144
|
"""
|
|
143
|
-
super(
|
|
145
|
+
super().__init__()
|
|
144
146
|
bin_boundaries = torch.linspace(0, 1, n_bins + 1)
|
|
145
147
|
self.bin_lowers = bin_boundaries[:-1]
|
|
146
148
|
self.bin_uppers = bin_boundaries[1:]
|
|
@@ -151,7 +153,7 @@ class _ECELoss(nn.Module):
|
|
|
151
153
|
accuracies = predictions.eq(labels)
|
|
152
154
|
|
|
153
155
|
ece = torch.zeros(1, device=logits.device)
|
|
154
|
-
for bin_lower, bin_upper in zip(self.bin_lowers, self.bin_uppers):
|
|
156
|
+
for bin_lower, bin_upper in zip(self.bin_lowers, self.bin_uppers, strict=True):
|
|
155
157
|
# Calculated |confidence - accuracy| in each bin
|
|
156
158
|
in_bin = confidences.gt(bin_lower.item()) * confidences.le(bin_upper.item())
|
|
157
159
|
prop_in_bin = in_bin.float().mean()
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"""Validation utilities for brain state labels and recordings."""
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
|
|
3
5
|
from accusleepy.brain_state_set import BrainStateSet
|
|
@@ -39,9 +41,10 @@ def check_label_validity(
|
|
|
39
41
|
):
|
|
40
42
|
return "label file contains invalid entries"
|
|
41
43
|
|
|
42
|
-
if confidence_scores is not None
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
if confidence_scores is not None and (
|
|
45
|
+
np.min(confidence_scores) < 0 or np.max(confidence_scores) > 1
|
|
46
|
+
):
|
|
47
|
+
return "label file contains invalid confidence scores"
|
|
45
48
|
|
|
46
49
|
return None
|
|
47
50
|
|
|
@@ -141,7 +144,9 @@ def check_config_consistency(
|
|
|
141
144
|
# generate message comparing the brain state configs
|
|
142
145
|
config_comparisons = list()
|
|
143
146
|
for config, config_name in zip(
|
|
144
|
-
[current_scored_states, model_scored_states],
|
|
147
|
+
[current_scored_states, model_scored_states],
|
|
148
|
+
["current", "model's"],
|
|
149
|
+
strict=True,
|
|
145
150
|
):
|
|
146
151
|
config_comparisons.append(
|
|
147
152
|
f"Scored brain states in {config_name} configuration: "
|
|
@@ -152,6 +157,7 @@ def check_config_consistency(
|
|
|
152
157
|
for x, y in zip(
|
|
153
158
|
config["digit"],
|
|
154
159
|
config["name"],
|
|
160
|
+
strict=True,
|
|
155
161
|
)
|
|
156
162
|
]
|
|
157
163
|
)
|
|
@@ -162,32 +168,26 @@ def check_config_consistency(
|
|
|
162
168
|
len_diff = len(current_scored_states["name"]) - len(model_scored_states["name"])
|
|
163
169
|
if len_diff != 0:
|
|
164
170
|
output.append(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"scored brain states than the model's configuration."
|
|
169
|
-
)
|
|
171
|
+
"WARNING: current brain state configuration has "
|
|
172
|
+
f"{'fewer' if len_diff < 0 else 'more'} "
|
|
173
|
+
"scored brain states than the model's configuration."
|
|
170
174
|
)
|
|
171
175
|
output = output + config_comparisons
|
|
172
176
|
else:
|
|
173
177
|
# the length is the same, but names might be different
|
|
174
178
|
if current_scored_states["name"] != model_scored_states["name"]:
|
|
175
179
|
output.append(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"the model's configuration."
|
|
180
|
-
)
|
|
180
|
+
"WARNING: current brain state configuration appears "
|
|
181
|
+
"to contain different brain states than "
|
|
182
|
+
"the model's configuration."
|
|
181
183
|
)
|
|
182
184
|
output = output + config_comparisons
|
|
183
185
|
|
|
184
186
|
if current_epoch_length != model_epoch_length:
|
|
185
187
|
output.append(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"does not match the current epoch length setting."
|
|
190
|
-
)
|
|
188
|
+
"Warning: the epoch length used when training this model "
|
|
189
|
+
f"({model_epoch_length} seconds) "
|
|
190
|
+
"does not match the current epoch length setting."
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
return output
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "accusleepy"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.0"
|
|
4
4
|
description = "Python implementation of AccuSleep"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Zeke Barger",email = "zekebarger@gmail.com"}
|
|
@@ -15,15 +15,15 @@ dependencies = [
|
|
|
15
15
|
"torchvision (>=0.23.0,<1.0.0)",
|
|
16
16
|
"scipy (>=1.15.2,<2.0.0)",
|
|
17
17
|
"matplotlib (>=3.10.1,<4.0.0)",
|
|
18
|
-
"joblib (>=1.4.2,<2.0.0)",
|
|
19
18
|
"pillow (>=11.1.0,<12.0.0)",
|
|
20
|
-
"fastparquet (>=2024.11.0,<2025.0.0)",
|
|
21
|
-
"pre-commit (>=4.2.0,<5.0.0)",
|
|
22
19
|
"tqdm (>=4.67.1,<5.0.0)",
|
|
23
|
-
"pyside6 (>=6.
|
|
20
|
+
"pyside6 (>=6.10.1,<7.0.0)",
|
|
21
|
+
"pyarrow (>=23.0.0,<24.0.0)",
|
|
22
|
+
"platformdirs (>=4.0.0,<5.0.0)",
|
|
24
23
|
]
|
|
25
24
|
|
|
26
25
|
[tool.poetry.group.dev.dependencies]
|
|
26
|
+
pre-commit = ">=4.2.0,<5.0.0"
|
|
27
27
|
pytest = ">=8.3.5,<9.0.0"
|
|
28
28
|
pytest-qt = ">=4.4.0,<5.0.0"
|
|
29
29
|
ruff = ">=0.11.2,<0.12.0"
|
|
@@ -35,6 +35,9 @@ build-backend = "poetry.core.masonry.api"
|
|
|
35
35
|
[tool.ruff]
|
|
36
36
|
extend-exclude = ["*.ipynb"]
|
|
37
37
|
|
|
38
|
+
[tool.ruff.lint]
|
|
39
|
+
select = ["E4", "E7", "E9", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
40
|
+
|
|
38
41
|
[tool.pytest.ini_options]
|
|
39
42
|
markers = [
|
|
40
43
|
"gui: marks tests as GUI tests (may require display or xvfb)",
|
|
File without changes
|
|
File without changes
|
/accusleepy-0.10.0/accusleepy/config.json → /accusleepy-0.11.0/accusleepy/default_config.json
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
|