accusleepy 0.8.1__tar.gz → 0.9.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.
- {accusleepy-0.8.1 → accusleepy-0.9.3}/PKG-INFO +2 -3
- {accusleepy-0.8.1 → accusleepy-0.9.3}/README.md +1 -1
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/bouts.py +3 -3
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/brain_state_set.py +6 -4
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/classification.py +14 -50
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/constants.py +3 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/fileio.py +18 -5
- accusleepy-0.9.3/accusleepy/gui/dialogs.py +40 -0
- accusleepy-0.9.3/accusleepy/gui/images/primary_window.png +0 -0
- accusleepy-0.9.3/accusleepy/gui/main.py +661 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/manual_scoring.py +1 -1
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/primary_window.py +7 -9
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/primary_window.ui +6 -8
- accusleepy-0.9.3/accusleepy/gui/recording_manager.py +110 -0
- accusleepy-0.9.3/accusleepy/gui/settings_widget.py +409 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/text/main_guide.md +1 -1
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/models.py +1 -1
- accusleepy-0.9.3/accusleepy/services.py +581 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/signal_processing.py +110 -38
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/temperature_scaling.py +14 -8
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/validation.py +67 -2
- {accusleepy-0.8.1 → accusleepy-0.9.3}/pyproject.toml +8 -2
- accusleepy-0.8.1/accusleepy/gui/images/primary_window.png +0 -0
- accusleepy-0.8.1/accusleepy/gui/main.py +0 -1474
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/__init__.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/__main__.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/config.json +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/__init__.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/brightness_down.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/brightness_up.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/double_down_arrow.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/double_up_arrow.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/down_arrow.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/home.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/question.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/save.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/up_arrow.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/zoom_in.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/icons/zoom_out.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/images/viewer_window.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/images/viewer_window_annotated.png +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/mplwidget.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/resources.qrc +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/resources_rc.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/text/dev_guide.md +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/text/manual_scoring_guide.md +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/viewer_window.py +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/gui/viewer_window.ui +0 -0
- {accusleepy-0.8.1 → accusleepy-0.9.3}/accusleepy/multitaper.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accusleepy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.3
|
|
4
4
|
Summary: Python implementation of AccuSleep
|
|
5
5
|
License: GPL-3.0-only
|
|
6
6
|
Author: Zeke Barger
|
|
@@ -20,7 +20,6 @@ Requires-Dist: pillow (>=11.1.0,<12.0.0)
|
|
|
20
20
|
Requires-Dist: pre-commit (>=4.2.0,<5.0.0)
|
|
21
21
|
Requires-Dist: pyside6 (>=6.9.0,<6.9.3)
|
|
22
22
|
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
23
|
-
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
24
23
|
Requires-Dist: torch (>=2.8.0,<3.0.0)
|
|
25
24
|
Requires-Dist: torchvision (>=0.23.0,<1.0.0)
|
|
26
25
|
Requires-Dist: tqdm (>=4.67.1,<5.0.0)
|
|
@@ -80,7 +79,7 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
80
79
|
|
|
81
80
|
## Changelog
|
|
82
81
|
|
|
83
|
-
- 0.8.1:
|
|
82
|
+
- 0.8.1-0.9.3: Improved error handling and code quality
|
|
84
83
|
- 0.8.0: More configurable settings, visual improvements
|
|
85
84
|
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
86
85
|
- 0.7.0: More settings can be configured in the UI
|
|
@@ -52,7 +52,7 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
|
|
|
52
52
|
|
|
53
53
|
## Changelog
|
|
54
54
|
|
|
55
|
-
- 0.8.1:
|
|
55
|
+
- 0.8.1-0.9.3: Improved error handling and code quality
|
|
56
56
|
- 0.8.0: More configurable settings, visual improvements
|
|
57
57
|
- 0.7.1-0.7.3: Bugfixes, code cleanup
|
|
58
58
|
- 0.7.0: More settings can be configured in the UI
|
|
@@ -42,7 +42,7 @@ def find_last_adjacent_bout(sorted_bouts: list[Bout], bout_index: int) -> int:
|
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
def find_short_bouts(
|
|
45
|
-
labels: np.
|
|
45
|
+
labels: np.ndarray, min_epochs: int, brain_states: set[int]
|
|
46
46
|
) -> list[Bout]:
|
|
47
47
|
"""Locate all brain state bouts below a minimum length
|
|
48
48
|
|
|
@@ -80,8 +80,8 @@ def find_short_bouts(
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
def enforce_min_bout_length(
|
|
83
|
-
labels: np.
|
|
84
|
-
) -> np.
|
|
83
|
+
labels: np.ndarray, epoch_length: int | float, min_bout_length: int | float
|
|
84
|
+
) -> np.ndarray:
|
|
85
85
|
"""Ensure brain state bouts meet the min length requirement
|
|
86
86
|
|
|
87
87
|
As a post-processing step for sleep scoring, we can require that any
|
|
@@ -41,7 +41,7 @@ class BrainStateSet:
|
|
|
41
41
|
i = 0
|
|
42
42
|
for brain_state in self.brain_states:
|
|
43
43
|
if brain_state.digit == undefined_label:
|
|
44
|
-
raise
|
|
44
|
+
raise ValueError(
|
|
45
45
|
f"Digit for {brain_state.name} matches 'undefined' label"
|
|
46
46
|
)
|
|
47
47
|
if brain_state.is_scored:
|
|
@@ -56,9 +56,11 @@ class BrainStateSet:
|
|
|
56
56
|
|
|
57
57
|
self.mixture_weights = np.array(self.mixture_weights)
|
|
58
58
|
if np.sum(self.mixture_weights) != 1:
|
|
59
|
-
raise
|
|
59
|
+
raise ValueError(
|
|
60
|
+
"Typical frequencies for scored brain states must sum to 1"
|
|
61
|
+
)
|
|
60
62
|
|
|
61
|
-
def convert_digit_to_class(self, digits: np.
|
|
63
|
+
def convert_digit_to_class(self, digits: np.ndarray) -> np.ndarray:
|
|
62
64
|
"""Convert array of digits to their corresponding classes
|
|
63
65
|
|
|
64
66
|
:param digits: array of digits
|
|
@@ -66,7 +68,7 @@ class BrainStateSet:
|
|
|
66
68
|
"""
|
|
67
69
|
return np.array([self.digit_to_class[i] for i in digits])
|
|
68
70
|
|
|
69
|
-
def convert_class_to_digit(self, classes: np.
|
|
71
|
+
def convert_class_to_digit(self, classes: np.ndarray) -> np.ndarray:
|
|
70
72
|
"""Convert array of classes to their corresponding digits
|
|
71
73
|
|
|
72
74
|
:param classes: array of classes
|
|
@@ -16,7 +16,6 @@ from accusleepy.models import SSANN
|
|
|
16
16
|
from accusleepy.signal_processing import (
|
|
17
17
|
create_eeg_emg_image,
|
|
18
18
|
format_img,
|
|
19
|
-
get_mixture_values,
|
|
20
19
|
mixture_z_score_img,
|
|
21
20
|
)
|
|
22
21
|
|
|
@@ -84,7 +83,7 @@ def create_dataloader(
|
|
|
84
83
|
def train_ssann(
|
|
85
84
|
annotations_file: str,
|
|
86
85
|
img_dir: str,
|
|
87
|
-
|
|
86
|
+
training_class_balance: np.ndarray,
|
|
88
87
|
n_classes: int,
|
|
89
88
|
hyperparameters: Hyperparameters,
|
|
90
89
|
) -> SSANN:
|
|
@@ -92,7 +91,7 @@ def train_ssann(
|
|
|
92
91
|
|
|
93
92
|
:param annotations_file: file with information on each training image
|
|
94
93
|
:param img_dir: training image location
|
|
95
|
-
:param
|
|
94
|
+
:param training_class_balance: proportion of each class in the training set
|
|
96
95
|
:param n_classes: number of classes the model will learn
|
|
97
96
|
:param hyperparameters: model training hyperparameters
|
|
98
97
|
:return: trained Sleep Scoring Artificial Neural Network model
|
|
@@ -109,7 +108,7 @@ def train_ssann(
|
|
|
109
108
|
model.train()
|
|
110
109
|
|
|
111
110
|
# correct for class imbalance
|
|
112
|
-
weight = torch.tensor((
|
|
111
|
+
weight = torch.tensor((training_class_balance**-1).astype("float32")).to(device)
|
|
113
112
|
|
|
114
113
|
criterion = nn.CrossEntropyLoss(weight=weight)
|
|
115
114
|
optimizer = optim.SGD(
|
|
@@ -133,16 +132,16 @@ def train_ssann(
|
|
|
133
132
|
|
|
134
133
|
def score_recording(
|
|
135
134
|
model: SSANN,
|
|
136
|
-
eeg: np.
|
|
137
|
-
emg: np.
|
|
138
|
-
mixture_means: np.
|
|
139
|
-
mixture_sds: np.
|
|
135
|
+
eeg: np.ndarray,
|
|
136
|
+
emg: np.ndarray,
|
|
137
|
+
mixture_means: np.ndarray,
|
|
138
|
+
mixture_sds: np.ndarray,
|
|
140
139
|
sampling_rate: int | float,
|
|
141
140
|
epoch_length: int | float,
|
|
142
141
|
epochs_per_img: int,
|
|
143
142
|
brain_state_set: BrainStateSet,
|
|
144
143
|
emg_filter: EMGFilter,
|
|
145
|
-
) -> np.
|
|
144
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
146
145
|
"""Use classification model to get brain state labels for a recording
|
|
147
146
|
|
|
148
147
|
This assumes signals have been preprocessed to contain an integer
|
|
@@ -167,7 +166,7 @@ def score_recording(
|
|
|
167
166
|
|
|
168
167
|
# create and scale eeg+emg spectrogram
|
|
169
168
|
img = create_eeg_emg_image(eeg, emg, sampling_rate, epoch_length, emg_filter)
|
|
170
|
-
img = mixture_z_score_img(
|
|
169
|
+
img, _ = mixture_z_score_img(
|
|
171
170
|
img,
|
|
172
171
|
mixture_means=mixture_means,
|
|
173
172
|
mixture_sds=mixture_sds,
|
|
@@ -196,10 +195,10 @@ def score_recording(
|
|
|
196
195
|
|
|
197
196
|
def example_real_time_scoring_function(
|
|
198
197
|
model: SSANN,
|
|
199
|
-
eeg: np.
|
|
200
|
-
emg: np.
|
|
201
|
-
mixture_means: np.
|
|
202
|
-
mixture_sds: np.
|
|
198
|
+
eeg: np.ndarray,
|
|
199
|
+
emg: np.ndarray,
|
|
200
|
+
mixture_means: np.ndarray,
|
|
201
|
+
mixture_sds: np.ndarray,
|
|
203
202
|
sampling_rate: int | float,
|
|
204
203
|
epoch_length: int | float,
|
|
205
204
|
epochs_per_img: int,
|
|
@@ -244,7 +243,7 @@ def example_real_time_scoring_function(
|
|
|
244
243
|
|
|
245
244
|
# create and scale eeg+emg spectrogram
|
|
246
245
|
img = create_eeg_emg_image(eeg, emg, sampling_rate, epoch_length, emg_filter)
|
|
247
|
-
img = mixture_z_score_img(
|
|
246
|
+
img, _ = mixture_z_score_img(
|
|
248
247
|
img,
|
|
249
248
|
mixture_means=mixture_means,
|
|
250
249
|
mixture_sds=mixture_sds,
|
|
@@ -264,38 +263,3 @@ def example_real_time_scoring_function(
|
|
|
264
263
|
|
|
265
264
|
label = int(brain_state_set.convert_class_to_digit(predicted.cpu().numpy())[0])
|
|
266
265
|
return label
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def create_calibration_file(
|
|
270
|
-
filename: str,
|
|
271
|
-
eeg: np.array,
|
|
272
|
-
emg: np.array,
|
|
273
|
-
labels: np.array,
|
|
274
|
-
sampling_rate: int | float,
|
|
275
|
-
epoch_length: int | float,
|
|
276
|
-
brain_state_set: BrainStateSet,
|
|
277
|
-
emg_filter: EMGFilter,
|
|
278
|
-
) -> None:
|
|
279
|
-
"""Create file of calibration data for a subject
|
|
280
|
-
|
|
281
|
-
This assumes signals have been preprocessed to contain an integer
|
|
282
|
-
number of epochs.
|
|
283
|
-
|
|
284
|
-
:param filename: filename for the calibration file
|
|
285
|
-
:param eeg: EEG signal
|
|
286
|
-
:param emg: EMG signal
|
|
287
|
-
:param labels: brain state labels, as digits
|
|
288
|
-
:param sampling_rate: sampling rate, in Hz
|
|
289
|
-
:param epoch_length: epoch length, in seconds
|
|
290
|
-
:param brain_state_set: set of brain state options
|
|
291
|
-
:param emg_filter: EMG filter parameters
|
|
292
|
-
"""
|
|
293
|
-
img = create_eeg_emg_image(eeg, emg, sampling_rate, epoch_length, emg_filter)
|
|
294
|
-
mixture_means, mixture_sds = get_mixture_values(
|
|
295
|
-
img=img,
|
|
296
|
-
labels=brain_state_set.convert_digit_to_class(labels),
|
|
297
|
-
brain_state_set=brain_state_set,
|
|
298
|
-
)
|
|
299
|
-
pd.DataFrame(
|
|
300
|
-
{c.MIXTURE_MEAN_COL: mixture_means, c.MIXTURE_SD_COL: mixture_sds}
|
|
301
|
-
).to_csv(filename, index=False)
|
|
@@ -18,6 +18,9 @@ MESSAGE_BOX_MAX_DEPTH = 200
|
|
|
18
18
|
ABS_MAX_Z_SCORE = 3.5
|
|
19
19
|
# upper frequency limit when generating EEG spectrograms
|
|
20
20
|
SPECTROGRAM_UPPER_FREQ = 64
|
|
21
|
+
# minimum number of epochs per brain state needed to create
|
|
22
|
+
# a calibration file or use a recording for model training
|
|
23
|
+
MIN_EPOCHS_PER_STATE = 3
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
# very unlikely you will want to change values from here onwards
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
4
5
|
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
@@ -56,7 +57,7 @@ class Recording:
|
|
|
56
57
|
widget: QListWidgetItem = None # list item widget shown in the GUI
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def load_calibration_file(filename: str) ->
|
|
60
|
+
def load_calibration_file(filename: str) -> tuple[np.ndarray, np.ndarray]:
|
|
60
61
|
"""Load a calibration file
|
|
61
62
|
|
|
62
63
|
:param filename: filename
|
|
@@ -80,11 +81,11 @@ def load_csv_or_parquet(filename: str) -> pd.DataFrame:
|
|
|
80
81
|
elif extension == ".parquet":
|
|
81
82
|
df = pd.read_parquet(filename)
|
|
82
83
|
else:
|
|
83
|
-
raise
|
|
84
|
+
raise ValueError("file must be csv or parquet")
|
|
84
85
|
return df
|
|
85
86
|
|
|
86
87
|
|
|
87
|
-
def load_recording(filename: str) ->
|
|
88
|
+
def load_recording(filename: str) -> tuple[np.ndarray, np.ndarray]:
|
|
88
89
|
"""Load recording of EEG and EMG time series data
|
|
89
90
|
|
|
90
91
|
:param filename: filename
|
|
@@ -96,7 +97,7 @@ def load_recording(filename: str) -> (np.array, np.array):
|
|
|
96
97
|
return eeg, emg
|
|
97
98
|
|
|
98
99
|
|
|
99
|
-
def load_labels(filename: str) ->
|
|
100
|
+
def load_labels(filename: str) -> tuple[np.ndarray, np.ndarray | None]:
|
|
100
101
|
"""Load file of brain state labels and confidence scores
|
|
101
102
|
|
|
102
103
|
:param filename: filename
|
|
@@ -110,7 +111,7 @@ def load_labels(filename: str) -> (np.array, np.array):
|
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
def save_labels(
|
|
113
|
-
labels: np.
|
|
114
|
+
labels: np.ndarray, filename: str, confidence_scores: np.ndarray | None = None
|
|
114
115
|
) -> None:
|
|
115
116
|
"""Save brain state labels to file
|
|
116
117
|
|
|
@@ -223,6 +224,7 @@ def save_config(
|
|
|
223
224
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), c.CONFIG_FILE), "w"
|
|
224
225
|
) as f:
|
|
225
226
|
json.dump(output_dict, f, indent=4)
|
|
227
|
+
f.write("\n")
|
|
226
228
|
|
|
227
229
|
|
|
228
230
|
def load_recording_list(filename: str) -> list[Recording]:
|
|
@@ -258,3 +260,14 @@ def save_recording_list(filename: str, recordings: list[Recording]) -> None:
|
|
|
258
260
|
}
|
|
259
261
|
with open(filename, "w") as f:
|
|
260
262
|
json.dump(recording_dict, f, indent=4)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_version() -> str:
|
|
266
|
+
"""Get AccuSleePy package version
|
|
267
|
+
|
|
268
|
+
:return: AccuSleePy package version
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
return version("accusleepy")
|
|
272
|
+
except PackageNotFoundError:
|
|
273
|
+
return ""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""File dialog helpers"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from PySide6.QtWidgets import QFileDialog, QWidget
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def select_existing_file(parent: QWidget, title: str, file_filter: str) -> str | None:
|
|
9
|
+
"""Show dialog to select an existing file.
|
|
10
|
+
|
|
11
|
+
:param parent: parent widget
|
|
12
|
+
:param title: dialog window title
|
|
13
|
+
:param file_filter: file type filter (e.g., "*.csv")
|
|
14
|
+
:return: normalized path or None if cancelled
|
|
15
|
+
"""
|
|
16
|
+
dialog = QFileDialog(parent)
|
|
17
|
+
dialog.setWindowTitle(title)
|
|
18
|
+
dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
|
|
19
|
+
dialog.setViewMode(QFileDialog.ViewMode.Detail)
|
|
20
|
+
dialog.setNameFilter(file_filter)
|
|
21
|
+
|
|
22
|
+
if dialog.exec():
|
|
23
|
+
return os.path.normpath(dialog.selectedFiles()[0])
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def select_save_location(parent: QWidget, caption: str, file_filter: str) -> str | None:
|
|
28
|
+
"""Show dialog to choose save location.
|
|
29
|
+
|
|
30
|
+
:param parent: parent widget
|
|
31
|
+
:param caption: dialog window caption
|
|
32
|
+
:param file_filter: file type filter (e.g., "*.csv")
|
|
33
|
+
:return: normalized path or None if cancelled
|
|
34
|
+
"""
|
|
35
|
+
filename, _ = QFileDialog.getSaveFileName(
|
|
36
|
+
parent, caption=caption, filter=file_filter
|
|
37
|
+
)
|
|
38
|
+
if filename:
|
|
39
|
+
return os.path.normpath(filename)
|
|
40
|
+
return None
|
|
Binary file
|