accusleepy 0.8.0__py3-none-any.whl → 0.9.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- accusleepy/bouts.py +46 -38
- accusleepy/brain_state_set.py +6 -4
- accusleepy/classification.py +14 -50
- accusleepy/constants.py +3 -0
- accusleepy/fileio.py +58 -27
- accusleepy/gui/dialogs.py +40 -0
- accusleepy/gui/images/primary_window.png +0 -0
- accusleepy/gui/main.py +212 -1026
- accusleepy/gui/manual_scoring.py +5 -13
- accusleepy/gui/primary_window.py +7 -9
- accusleepy/gui/primary_window.ui +6 -8
- accusleepy/gui/recording_manager.py +110 -0
- accusleepy/gui/settings_widget.py +409 -0
- accusleepy/gui/text/main_guide.md +1 -1
- accusleepy/models.py +1 -1
- accusleepy/services.py +581 -0
- accusleepy/signal_processing.py +110 -38
- accusleepy/temperature_scaling.py +14 -8
- accusleepy/validation.py +67 -2
- {accusleepy-0.8.0.dist-info → accusleepy-0.9.2.dist-info}/METADATA +3 -5
- {accusleepy-0.8.0.dist-info → accusleepy-0.9.2.dist-info}/RECORD +22 -18
- {accusleepy-0.8.0.dist-info → accusleepy-0.9.2.dist-info}/WHEEL +1 -1
accusleepy/gui/manual_scoring.py
CHANGED
|
@@ -139,18 +139,10 @@ class ManualScoringWindow(QDialog):
|
|
|
139
139
|
self.ui.setupUi(self)
|
|
140
140
|
self.setWindowTitle("AccuSleePy manual scoring window")
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
_,
|
|
147
|
-
_,
|
|
148
|
-
_,
|
|
149
|
-
_,
|
|
150
|
-
_,
|
|
151
|
-
self.epochs_to_show,
|
|
152
|
-
self.autoscroll_state,
|
|
153
|
-
) = load_config()
|
|
142
|
+
config = load_config()
|
|
143
|
+
self.brain_state_set = config.brain_state_set
|
|
144
|
+
self.epochs_to_show = config.epochs_to_show
|
|
145
|
+
self.autoscroll_state = config.autoscroll_state
|
|
154
146
|
|
|
155
147
|
# find the set of y-axis locations of valid brain state labels
|
|
156
148
|
self.label_display_options = convert_labels(
|
|
@@ -1018,7 +1010,7 @@ def convert_labels(labels: np.array, style: str) -> np.array:
|
|
|
1018
1010
|
labels = [i if i != 0 else UNDEFINED_LABEL for i in labels]
|
|
1019
1011
|
return np.array([i if i != 10 else 0 for i in labels])
|
|
1020
1012
|
else:
|
|
1021
|
-
raise
|
|
1013
|
+
raise ValueError(f"style must be '{DISPLAY_FORMAT}' or '{DIGIT_FORMAT}'")
|
|
1022
1014
|
|
|
1023
1015
|
|
|
1024
1016
|
def create_label_img(labels: np.array, label_display_options: np.array) -> np.array:
|
accusleepy/gui/primary_window.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
################################################################################
|
|
4
4
|
## Form generated from reading UI file 'primary_window.ui'
|
|
5
5
|
##
|
|
6
|
-
## Created by: Qt User Interface Compiler version 6.9.
|
|
6
|
+
## Created by: Qt User Interface Compiler version 6.9.2
|
|
7
7
|
##
|
|
8
8
|
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
|
9
9
|
################################################################################
|
|
@@ -3153,16 +3153,16 @@ class Ui_PrimaryWindow(object):
|
|
|
3153
3153
|
QCoreApplication.translate(
|
|
3154
3154
|
"PrimaryWindow",
|
|
3155
3155
|
"This is the current set of brain states. Important notes:\n"
|
|
3156
|
-
"- You must click 'Save settings' to
|
|
3156
|
+
"- You must click 'Save settings' for changes to take effect.\n"
|
|
3157
3157
|
"- Changing these settings can prevent existing label files, calibration files, and trained models from working properly.\n"
|
|
3158
3158
|
"- Reinstalling AccuSleePy will overwrite this configuration.\n"
|
|
3159
3159
|
"\n"
|
|
3160
3160
|
"Each brain state has several attributes:\n"
|
|
3161
|
-
"- Digit:
|
|
3161
|
+
"- Digit: the indicator for this state in label files, and the keyboard shortcut for this state in manual scoring.\n"
|
|
3162
3162
|
"- Enabled: whether a brain state for this digit exists.\n"
|
|
3163
3163
|
"- Name: unique name of the brain state (e.g., REM).\n"
|
|
3164
|
-
"- Scored: whether a classification model should output this brain state. If a state
|
|
3165
|
-
"- Frequency: approximate relative frequency of this brain state. Does not need to be very accurate. Frequencies for
|
|
3164
|
+
"- Scored: whether a classification model should output this brain state. If a state corresponds to missing/corrupted data, you probably want to uncheck this box.\n"
|
|
3165
|
+
"- Frequency: approximate relative frequency of this brain state. Does not need to be very accurate. Frequencies for scored states must sum to 1.",
|
|
3166
3166
|
None,
|
|
3167
3167
|
)
|
|
3168
3168
|
)
|
|
@@ -3255,7 +3255,7 @@ class Ui_PrimaryWindow(object):
|
|
|
3255
3255
|
self.ui_default_description_label.setText(
|
|
3256
3256
|
QCoreApplication.translate(
|
|
3257
3257
|
"PrimaryWindow",
|
|
3258
|
-
"<html><head/><body><p>These are the default values/settings that are shown in the primary window and manual scoring window when they start up.</p><p>Changes here will not affect the **current** state of the controls in the "Sleep scoring" tab.</p></body></html>",
|
|
3258
|
+
"<html><head/><body><p>These are the default values/settings that are shown in the primary window and manual scoring window when they start up.</p><p>Changes here will not affect the **current** state of the controls in the "Sleep scoring" tab.</p><p>You must click 'Save settings' for changes to take effect.</p></body></html>",
|
|
3259
3259
|
None,
|
|
3260
3260
|
)
|
|
3261
3261
|
)
|
|
@@ -3280,9 +3280,7 @@ class Ui_PrimaryWindow(object):
|
|
|
3280
3280
|
self.emg_filter_description_label.setText(
|
|
3281
3281
|
QCoreApplication.translate(
|
|
3282
3282
|
"PrimaryWindow",
|
|
3283
|
-
"EMG power is calculated as the root-mean-square of the EMG signal in each epoch after a bandpass filter is applied. You can change the proprties of that filter here. If the manual scoring window displays the EMG signal but not the EMG power, you can try lowering the filter order to 4
|
|
3284
|
-
"\n"
|
|
3285
|
-
"Changing these settings can prevent existing classification models and calibration files from working as expected. You will probably want to recreate them using your new settings.",
|
|
3283
|
+
"<html><head/><body><p>EMG power is calculated as the root-mean-square of the EMG signal in each epoch after a bandpass filter is applied. You can change the proprties of that filter here. If the manual scoring window displays the EMG signal but not the EMG power, you can try lowering the filter order to 4.</p><p>Changing these settings can prevent existing classification models and calibration files from working as expected. You will probably want to recreate them using your new settings.</p><p>You must click 'Save settings' for changes to take effect.</p></body></html>",
|
|
3286
3284
|
None,
|
|
3287
3285
|
)
|
|
3288
3286
|
)
|
accusleepy/gui/primary_window.ui
CHANGED
|
@@ -3750,16 +3750,16 @@ color: rgb(244, 195, 68);</string>
|
|
|
3750
3750
|
</property>
|
|
3751
3751
|
<property name="text">
|
|
3752
3752
|
<string>This is the current set of brain states. Important notes:
|
|
3753
|
-
- You must click 'Save settings' to
|
|
3753
|
+
- You must click 'Save settings' for changes to take effect.
|
|
3754
3754
|
- Changing these settings can prevent existing label files, calibration files, and trained models from working properly.
|
|
3755
3755
|
- Reinstalling AccuSleePy will overwrite this configuration.
|
|
3756
3756
|
|
|
3757
3757
|
Each brain state has several attributes:
|
|
3758
|
-
- Digit:
|
|
3758
|
+
- Digit: the indicator for this state in label files, and the keyboard shortcut for this state in manual scoring.
|
|
3759
3759
|
- Enabled: whether a brain state for this digit exists.
|
|
3760
3760
|
- Name: unique name of the brain state (e.g., REM).
|
|
3761
|
-
- Scored: whether a classification model should output this brain state. If a state
|
|
3762
|
-
- Frequency: approximate relative frequency of this brain state. Does not need to be very accurate. Frequencies for
|
|
3761
|
+
- Scored: whether a classification model should output this brain state. If a state corresponds to missing/corrupted data, you probably want to uncheck this box.
|
|
3762
|
+
- Frequency: approximate relative frequency of this brain state. Does not need to be very accurate. Frequencies for scored states must sum to 1.</string>
|
|
3763
3763
|
</property>
|
|
3764
3764
|
<property name="textFormat">
|
|
3765
3765
|
<enum>Qt::TextFormat::MarkdownText</enum>
|
|
@@ -4112,7 +4112,7 @@ Each brain state has several attributes:
|
|
|
4112
4112
|
<string notr="true">background-color: white;</string>
|
|
4113
4113
|
</property>
|
|
4114
4114
|
<property name="text">
|
|
4115
|
-
<string><html><head/><body><p>These are the default values/settings that are shown in the primary window and manual scoring window when they start up.</p><p>Changes here will not affect the **current** state of the controls in the &quot;Sleep scoring&quot; tab.</p></body></html></string>
|
|
4115
|
+
<string><html><head/><body><p>These are the default values/settings that are shown in the primary window and manual scoring window when they start up.</p><p>Changes here will not affect the **current** state of the controls in the &quot;Sleep scoring&quot; tab.</p><p>You must click 'Save settings' for changes to take effect.</p></body></html></string>
|
|
4116
4116
|
</property>
|
|
4117
4117
|
<property name="textFormat">
|
|
4118
4118
|
<enum>Qt::TextFormat::MarkdownText</enum>
|
|
@@ -4364,9 +4364,7 @@ Each brain state has several attributes:
|
|
|
4364
4364
|
<string notr="true">background-color: white;</string>
|
|
4365
4365
|
</property>
|
|
4366
4366
|
<property name="text">
|
|
4367
|
-
<string
|
|
4368
|
-
|
|
4369
|
-
Changing these settings can prevent existing classification models and calibration files from working as expected. You will probably want to recreate them using your new settings.</string>
|
|
4367
|
+
<string><html><head/><body><p>EMG power is calculated as the root-mean-square of the EMG signal in each epoch after a bandpass filter is applied. You can change the proprties of that filter here. If the manual scoring window displays the EMG signal but not the EMG power, you can try lowering the filter order to 4.</p><p>Changing these settings can prevent existing classification models and calibration files from working as expected. You will probably want to recreate them using your new settings.</p><p>You must click 'Save settings' for changes to take effect.</p></body></html></string>
|
|
4370
4368
|
</property>
|
|
4371
4369
|
<property name="alignment">
|
|
4372
4370
|
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignTop</set>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Recording list manager"""
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import QObject
|
|
4
|
+
from PySide6.QtWidgets import QListWidget, QListWidgetItem
|
|
5
|
+
|
|
6
|
+
from accusleepy.fileio import Recording, load_recording_list, save_recording_list
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RecordingListManager(QObject):
|
|
10
|
+
"""Manages the list of recordings and the associated QListWidget"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, list_widget: QListWidget, parent: QObject | None = None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
self._widget = list_widget
|
|
15
|
+
|
|
16
|
+
# Create initial empty recording (there is always at least one)
|
|
17
|
+
first_recording = Recording(
|
|
18
|
+
widget=QListWidgetItem("Recording 1", self._widget),
|
|
19
|
+
)
|
|
20
|
+
self._recordings: list[Recording] = [first_recording]
|
|
21
|
+
self._widget.addItem(first_recording.widget)
|
|
22
|
+
self._widget.setCurrentRow(0)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def current(self) -> Recording:
|
|
26
|
+
"""The currently selected recording"""
|
|
27
|
+
return self._recordings[self._widget.currentRow()]
|
|
28
|
+
|
|
29
|
+
def add(self, sampling_rate: int | float) -> Recording:
|
|
30
|
+
"""Add a new recording to the list
|
|
31
|
+
|
|
32
|
+
:param sampling_rate: sampling rate for the new recording
|
|
33
|
+
:return: the newly created Recording
|
|
34
|
+
"""
|
|
35
|
+
new_name = max(r.name for r in self._recordings) + 1
|
|
36
|
+
|
|
37
|
+
# Create recording with widget
|
|
38
|
+
recording = Recording(
|
|
39
|
+
name=new_name,
|
|
40
|
+
sampling_rate=sampling_rate,
|
|
41
|
+
widget=QListWidgetItem(f"Recording {new_name}", self._widget),
|
|
42
|
+
)
|
|
43
|
+
self._recordings.append(recording)
|
|
44
|
+
|
|
45
|
+
# Update widget
|
|
46
|
+
self._widget.addItem(recording.widget)
|
|
47
|
+
self._widget.setCurrentRow(len(self._recordings) - 1)
|
|
48
|
+
|
|
49
|
+
return recording
|
|
50
|
+
|
|
51
|
+
def remove_current(self) -> str:
|
|
52
|
+
"""Remove the currently selected recording
|
|
53
|
+
|
|
54
|
+
:return: message describing what was removed/reset
|
|
55
|
+
"""
|
|
56
|
+
if len(self._recordings) > 1:
|
|
57
|
+
# Remove from list and widget
|
|
58
|
+
index = self._widget.currentRow()
|
|
59
|
+
recording_name = self._recordings[index].name
|
|
60
|
+
self._widget.takeItem(index)
|
|
61
|
+
del self._recordings[index]
|
|
62
|
+
return f"deleted Recording {recording_name}"
|
|
63
|
+
else:
|
|
64
|
+
# Reset the single recording to defaults
|
|
65
|
+
recording_name = self._recordings[0].name
|
|
66
|
+
self._recordings[0] = Recording(widget=self._recordings[0].widget)
|
|
67
|
+
self._recordings[0].widget.setText(f"Recording {self._recordings[0].name}")
|
|
68
|
+
return f"cleared Recording {recording_name}"
|
|
69
|
+
|
|
70
|
+
def export_to_file(self, filename: str) -> None:
|
|
71
|
+
"""Save the recording list to a file
|
|
72
|
+
|
|
73
|
+
:param filename: path to which the list will be exported
|
|
74
|
+
"""
|
|
75
|
+
save_recording_list(filename=filename, recordings=self._recordings)
|
|
76
|
+
|
|
77
|
+
def import_from_file(self, filename: str) -> None:
|
|
78
|
+
"""Load a recording list from a file, replacing current list
|
|
79
|
+
|
|
80
|
+
:param filename: path to load from
|
|
81
|
+
"""
|
|
82
|
+
# Block signals while rebuilding the list to avoid triggering
|
|
83
|
+
# selection callbacks with invalid state
|
|
84
|
+
self._widget.blockSignals(True)
|
|
85
|
+
try:
|
|
86
|
+
self._widget.clear()
|
|
87
|
+
|
|
88
|
+
# Load recordings
|
|
89
|
+
self._recordings = load_recording_list(filename)
|
|
90
|
+
|
|
91
|
+
# Create widgets for each recording
|
|
92
|
+
for recording in self._recordings:
|
|
93
|
+
recording.widget = QListWidgetItem(
|
|
94
|
+
f"Recording {recording.name}", self._widget
|
|
95
|
+
)
|
|
96
|
+
self._widget.addItem(recording.widget)
|
|
97
|
+
finally:
|
|
98
|
+
self._widget.blockSignals(False)
|
|
99
|
+
|
|
100
|
+
# Select first recording (this will trigger the selection callback)
|
|
101
|
+
self._widget.setCurrentRow(0)
|
|
102
|
+
|
|
103
|
+
def __iter__(self):
|
|
104
|
+
return iter(self._recordings)
|
|
105
|
+
|
|
106
|
+
def __len__(self):
|
|
107
|
+
return len(self._recordings)
|
|
108
|
+
|
|
109
|
+
def __getitem__(self, index: int) -> Recording:
|
|
110
|
+
return self._recordings[index]
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Settings tab manager"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from functools import partial
|
|
5
|
+
|
|
6
|
+
from PySide6.QtCore import QObject
|
|
7
|
+
from PySide6.QtWidgets import QCheckBox, QDoubleSpinBox, QLineEdit
|
|
8
|
+
|
|
9
|
+
from accusleepy.brain_state_set import BrainState, BrainStateSet
|
|
10
|
+
from accusleepy.constants import (
|
|
11
|
+
DEFAULT_BATCH_SIZE,
|
|
12
|
+
DEFAULT_EMG_BP_LOWER,
|
|
13
|
+
DEFAULT_EMG_BP_UPPER,
|
|
14
|
+
DEFAULT_EMG_FILTER_ORDER,
|
|
15
|
+
DEFAULT_LEARNING_RATE,
|
|
16
|
+
DEFAULT_MOMENTUM,
|
|
17
|
+
DEFAULT_TRAINING_EPOCHS,
|
|
18
|
+
UNDEFINED_LABEL,
|
|
19
|
+
)
|
|
20
|
+
from accusleepy.fileio import (
|
|
21
|
+
AccuSleePyConfig,
|
|
22
|
+
EMGFilter,
|
|
23
|
+
Hyperparameters,
|
|
24
|
+
save_config,
|
|
25
|
+
)
|
|
26
|
+
from accusleepy.gui.primary_window import Ui_PrimaryWindow
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class StateSettings:
|
|
31
|
+
"""Widgets for config settings for a brain state"""
|
|
32
|
+
|
|
33
|
+
digit: int
|
|
34
|
+
enabled_widget: QCheckBox
|
|
35
|
+
name_widget: QLineEdit
|
|
36
|
+
is_scored_widget: QCheckBox
|
|
37
|
+
frequency_widget: QDoubleSpinBox
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SettingsWidget(QObject):
|
|
41
|
+
"""Manages settings tab UI and configuration data"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
ui: Ui_PrimaryWindow,
|
|
46
|
+
config: AccuSleePyConfig,
|
|
47
|
+
parent: QObject | None = None,
|
|
48
|
+
):
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
self._ui = ui
|
|
51
|
+
|
|
52
|
+
# Store configuration values (only settings managed by the Settings tab)
|
|
53
|
+
self._brain_state_set = config.brain_state_set
|
|
54
|
+
self._emg_filter = config.emg_filter
|
|
55
|
+
self._hyperparameters = config.hyperparameters
|
|
56
|
+
self._default_epochs_to_show = config.epochs_to_show
|
|
57
|
+
self._default_autoscroll_state = config.autoscroll_state
|
|
58
|
+
|
|
59
|
+
# Store default values for main tab settings (used to populate Settings tab UI)
|
|
60
|
+
self._default_epoch_length = config.default_epoch_length
|
|
61
|
+
self._default_overwrite_setting = config.overwrite_setting
|
|
62
|
+
self._default_confidence_setting = config.save_confidence_setting
|
|
63
|
+
self._default_min_bout_length = config.min_bout_length
|
|
64
|
+
|
|
65
|
+
# Initialize settings widgets
|
|
66
|
+
self._settings_widgets: dict[int, StateSettings] = {}
|
|
67
|
+
self._initialize_settings_tab()
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def brain_state_set(self) -> BrainStateSet:
|
|
71
|
+
"""The current brain state set configuration (from saved config)"""
|
|
72
|
+
return self._brain_state_set
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def emg_filter(self) -> EMGFilter:
|
|
76
|
+
"""EMG filter parameters (from saved config)"""
|
|
77
|
+
return self._emg_filter
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def hyperparameters(self) -> Hyperparameters:
|
|
81
|
+
"""Model training hyperparameters"""
|
|
82
|
+
return self._hyperparameters
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def default_epochs_to_show(self) -> int:
|
|
86
|
+
"""Default number of epochs to show in manual scoring"""
|
|
87
|
+
return self._default_epochs_to_show
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def default_autoscroll_state(self) -> bool:
|
|
91
|
+
"""Default autoscroll state for manual scoring"""
|
|
92
|
+
return self._default_autoscroll_state
|
|
93
|
+
|
|
94
|
+
def _initialize_settings_tab(self) -> None:
|
|
95
|
+
"""Populate settings tab and assign its callbacks"""
|
|
96
|
+
# Store dictionary that maps digits to rows of widgets
|
|
97
|
+
self._settings_widgets = {
|
|
98
|
+
1: StateSettings(
|
|
99
|
+
digit=1,
|
|
100
|
+
enabled_widget=self._ui.enable_state_1,
|
|
101
|
+
name_widget=self._ui.state_name_1,
|
|
102
|
+
is_scored_widget=self._ui.state_scored_1,
|
|
103
|
+
frequency_widget=self._ui.state_frequency_1,
|
|
104
|
+
),
|
|
105
|
+
2: StateSettings(
|
|
106
|
+
digit=2,
|
|
107
|
+
enabled_widget=self._ui.enable_state_2,
|
|
108
|
+
name_widget=self._ui.state_name_2,
|
|
109
|
+
is_scored_widget=self._ui.state_scored_2,
|
|
110
|
+
frequency_widget=self._ui.state_frequency_2,
|
|
111
|
+
),
|
|
112
|
+
3: StateSettings(
|
|
113
|
+
digit=3,
|
|
114
|
+
enabled_widget=self._ui.enable_state_3,
|
|
115
|
+
name_widget=self._ui.state_name_3,
|
|
116
|
+
is_scored_widget=self._ui.state_scored_3,
|
|
117
|
+
frequency_widget=self._ui.state_frequency_3,
|
|
118
|
+
),
|
|
119
|
+
4: StateSettings(
|
|
120
|
+
digit=4,
|
|
121
|
+
enabled_widget=self._ui.enable_state_4,
|
|
122
|
+
name_widget=self._ui.state_name_4,
|
|
123
|
+
is_scored_widget=self._ui.state_scored_4,
|
|
124
|
+
frequency_widget=self._ui.state_frequency_4,
|
|
125
|
+
),
|
|
126
|
+
5: StateSettings(
|
|
127
|
+
digit=5,
|
|
128
|
+
enabled_widget=self._ui.enable_state_5,
|
|
129
|
+
name_widget=self._ui.state_name_5,
|
|
130
|
+
is_scored_widget=self._ui.state_scored_5,
|
|
131
|
+
frequency_widget=self._ui.state_frequency_5,
|
|
132
|
+
),
|
|
133
|
+
6: StateSettings(
|
|
134
|
+
digit=6,
|
|
135
|
+
enabled_widget=self._ui.enable_state_6,
|
|
136
|
+
name_widget=self._ui.state_name_6,
|
|
137
|
+
is_scored_widget=self._ui.state_scored_6,
|
|
138
|
+
frequency_widget=self._ui.state_frequency_6,
|
|
139
|
+
),
|
|
140
|
+
7: StateSettings(
|
|
141
|
+
digit=7,
|
|
142
|
+
enabled_widget=self._ui.enable_state_7,
|
|
143
|
+
name_widget=self._ui.state_name_7,
|
|
144
|
+
is_scored_widget=self._ui.state_scored_7,
|
|
145
|
+
frequency_widget=self._ui.state_frequency_7,
|
|
146
|
+
),
|
|
147
|
+
8: StateSettings(
|
|
148
|
+
digit=8,
|
|
149
|
+
enabled_widget=self._ui.enable_state_8,
|
|
150
|
+
name_widget=self._ui.state_name_8,
|
|
151
|
+
is_scored_widget=self._ui.state_scored_8,
|
|
152
|
+
frequency_widget=self._ui.state_frequency_8,
|
|
153
|
+
),
|
|
154
|
+
9: StateSettings(
|
|
155
|
+
digit=9,
|
|
156
|
+
enabled_widget=self._ui.enable_state_9,
|
|
157
|
+
name_widget=self._ui.state_name_9,
|
|
158
|
+
is_scored_widget=self._ui.state_scored_9,
|
|
159
|
+
frequency_widget=self._ui.state_frequency_9,
|
|
160
|
+
),
|
|
161
|
+
0: StateSettings(
|
|
162
|
+
digit=0,
|
|
163
|
+
enabled_widget=self._ui.enable_state_0,
|
|
164
|
+
name_widget=self._ui.state_name_0,
|
|
165
|
+
is_scored_widget=self._ui.state_scored_0,
|
|
166
|
+
frequency_widget=self._ui.state_frequency_0,
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Update widget state to display current config
|
|
171
|
+
# UI defaults for main tab settings (shown in Settings tab for configuration)
|
|
172
|
+
self._ui.default_epoch_input.setValue(self._default_epoch_length)
|
|
173
|
+
self._ui.overwrite_default_checkbox.setChecked(self._default_overwrite_setting)
|
|
174
|
+
self._ui.confidence_setting_checkbox.setChecked(
|
|
175
|
+
self._default_confidence_setting
|
|
176
|
+
)
|
|
177
|
+
self._ui.default_min_bout_length_spinbox.setValue(self._default_min_bout_length)
|
|
178
|
+
self._ui.epochs_to_show_spinbox.setValue(self._default_epochs_to_show)
|
|
179
|
+
self._ui.autoscroll_checkbox.setChecked(self._default_autoscroll_state)
|
|
180
|
+
# EMG filter
|
|
181
|
+
self._ui.emg_order_spinbox.setValue(self._emg_filter.order)
|
|
182
|
+
self._ui.bp_lower_spinbox.setValue(self._emg_filter.bp_lower)
|
|
183
|
+
self._ui.bp_upper_spinbox.setValue(self._emg_filter.bp_upper)
|
|
184
|
+
# Model training hyperparameters
|
|
185
|
+
self._ui.batch_size_spinbox.setValue(self._hyperparameters.batch_size)
|
|
186
|
+
self._ui.learning_rate_spinbox.setValue(self._hyperparameters.learning_rate)
|
|
187
|
+
self._ui.momentum_spinbox.setValue(self._hyperparameters.momentum)
|
|
188
|
+
self._ui.training_epochs_spinbox.setValue(self._hyperparameters.training_epochs)
|
|
189
|
+
# Brain states
|
|
190
|
+
states = {b.digit: b for b in self._brain_state_set.brain_states}
|
|
191
|
+
for digit in range(10):
|
|
192
|
+
if digit in states.keys():
|
|
193
|
+
self._settings_widgets[digit].enabled_widget.setChecked(True)
|
|
194
|
+
self._settings_widgets[digit].name_widget.setText(states[digit].name)
|
|
195
|
+
self._settings_widgets[digit].is_scored_widget.setChecked(
|
|
196
|
+
states[digit].is_scored
|
|
197
|
+
)
|
|
198
|
+
self._settings_widgets[digit].frequency_widget.setValue(
|
|
199
|
+
states[digit].frequency
|
|
200
|
+
)
|
|
201
|
+
else:
|
|
202
|
+
self._settings_widgets[digit].enabled_widget.setChecked(False)
|
|
203
|
+
self._settings_widgets[digit].name_widget.setEnabled(False)
|
|
204
|
+
self._settings_widgets[digit].is_scored_widget.setEnabled(False)
|
|
205
|
+
self._settings_widgets[digit].frequency_widget.setEnabled(False)
|
|
206
|
+
|
|
207
|
+
# Set callbacks
|
|
208
|
+
self._ui.default_epoch_input.valueChanged.connect(self.reset_status_message)
|
|
209
|
+
self._ui.overwrite_default_checkbox.stateChanged.connect(
|
|
210
|
+
self.reset_status_message
|
|
211
|
+
)
|
|
212
|
+
self._ui.confidence_setting_checkbox.stateChanged.connect(
|
|
213
|
+
self.reset_status_message
|
|
214
|
+
)
|
|
215
|
+
self._ui.default_min_bout_length_spinbox.valueChanged.connect(
|
|
216
|
+
self.reset_status_message
|
|
217
|
+
)
|
|
218
|
+
self._ui.epochs_to_show_spinbox.valueChanged.connect(self.reset_status_message)
|
|
219
|
+
self._ui.autoscroll_checkbox.stateChanged.connect(self.reset_status_message)
|
|
220
|
+
|
|
221
|
+
self._ui.emg_order_spinbox.valueChanged.connect(self.reset_status_message)
|
|
222
|
+
self._ui.bp_lower_spinbox.valueChanged.connect(
|
|
223
|
+
self._emg_filter_bp_lower_changed
|
|
224
|
+
)
|
|
225
|
+
self._ui.bp_upper_spinbox.valueChanged.connect(
|
|
226
|
+
self._emg_filter_bp_upper_changed
|
|
227
|
+
)
|
|
228
|
+
self._ui.batch_size_spinbox.valueChanged.connect(self._hyperparameters_changed)
|
|
229
|
+
self._ui.learning_rate_spinbox.valueChanged.connect(
|
|
230
|
+
self._hyperparameters_changed
|
|
231
|
+
)
|
|
232
|
+
self._ui.momentum_spinbox.valueChanged.connect(self._hyperparameters_changed)
|
|
233
|
+
self._ui.training_epochs_spinbox.valueChanged.connect(
|
|
234
|
+
self._hyperparameters_changed
|
|
235
|
+
)
|
|
236
|
+
for digit in range(10):
|
|
237
|
+
state = self._settings_widgets[digit]
|
|
238
|
+
state.enabled_widget.stateChanged.connect(
|
|
239
|
+
partial(self._set_brain_state_enabled, digit)
|
|
240
|
+
)
|
|
241
|
+
state.name_widget.editingFinished.connect(self.check_validity)
|
|
242
|
+
state.is_scored_widget.stateChanged.connect(
|
|
243
|
+
partial(self._is_scored_changed, digit)
|
|
244
|
+
)
|
|
245
|
+
state.frequency_widget.valueChanged.connect(self.check_validity)
|
|
246
|
+
|
|
247
|
+
def _set_brain_state_enabled(self, digit: int, _state: int) -> None:
|
|
248
|
+
"""Called when user clicks "enabled" checkbox
|
|
249
|
+
|
|
250
|
+
:param digit: brain state digit
|
|
251
|
+
:param _state: unused but mandatory
|
|
252
|
+
"""
|
|
253
|
+
state = self._settings_widgets[digit]
|
|
254
|
+
is_checked = state.enabled_widget.isChecked()
|
|
255
|
+
for widget in [
|
|
256
|
+
state.name_widget,
|
|
257
|
+
state.is_scored_widget,
|
|
258
|
+
]:
|
|
259
|
+
widget.setEnabled(is_checked)
|
|
260
|
+
state.frequency_widget.setEnabled(
|
|
261
|
+
is_checked and state.is_scored_widget.isChecked()
|
|
262
|
+
)
|
|
263
|
+
if not is_checked:
|
|
264
|
+
state.name_widget.setText("")
|
|
265
|
+
state.frequency_widget.setValue(0)
|
|
266
|
+
self.check_validity()
|
|
267
|
+
|
|
268
|
+
def _is_scored_changed(self, digit: int, _state: int) -> None:
|
|
269
|
+
"""Called when user sets whether a state is scored
|
|
270
|
+
|
|
271
|
+
:param digit: brain state digit
|
|
272
|
+
:param _state: unused but mandatory
|
|
273
|
+
"""
|
|
274
|
+
state = self._settings_widgets[digit]
|
|
275
|
+
is_checked = state.is_scored_widget.isChecked()
|
|
276
|
+
state.frequency_widget.setEnabled(is_checked)
|
|
277
|
+
if not is_checked:
|
|
278
|
+
state.frequency_widget.setValue(0)
|
|
279
|
+
self.check_validity()
|
|
280
|
+
|
|
281
|
+
def _emg_filter_bp_lower_changed(self, _new_value: int | float) -> None:
|
|
282
|
+
"""Called when user modifies EMG filter lower cutoff
|
|
283
|
+
|
|
284
|
+
Value is read from UI when config is saved. Triggers validation.
|
|
285
|
+
"""
|
|
286
|
+
self.check_validity()
|
|
287
|
+
|
|
288
|
+
def _emg_filter_bp_upper_changed(self, _new_value: int | float) -> None:
|
|
289
|
+
"""Called when user modifies EMG filter upper cutoff
|
|
290
|
+
|
|
291
|
+
Value is read from UI when config is saved. Triggers validation.
|
|
292
|
+
"""
|
|
293
|
+
self.check_validity()
|
|
294
|
+
|
|
295
|
+
def _hyperparameters_changed(self, _new_value) -> None:
|
|
296
|
+
"""Called when user modifies model training hyperparameters"""
|
|
297
|
+
# These are the only changes to the settings tab that take
|
|
298
|
+
# effect immediately
|
|
299
|
+
self._hyperparameters = Hyperparameters(
|
|
300
|
+
batch_size=self._ui.batch_size_spinbox.value(),
|
|
301
|
+
learning_rate=self._ui.learning_rate_spinbox.value(),
|
|
302
|
+
momentum=self._ui.momentum_spinbox.value(),
|
|
303
|
+
training_epochs=self._ui.training_epochs_spinbox.value(),
|
|
304
|
+
)
|
|
305
|
+
self._ui.save_config_status.setText("")
|
|
306
|
+
|
|
307
|
+
def reset_status_message(self, _new_value=None) -> None:
|
|
308
|
+
"""Clear the message next to the 'save' button"""
|
|
309
|
+
self._ui.save_config_status.setText("")
|
|
310
|
+
|
|
311
|
+
def check_validity(self) -> str | None:
|
|
312
|
+
"""Check if brain state configuration on screen is valid
|
|
313
|
+
|
|
314
|
+
:return: error message if invalid, None if valid
|
|
315
|
+
"""
|
|
316
|
+
message = None
|
|
317
|
+
|
|
318
|
+
# Strip whitespace from brain state names and update display
|
|
319
|
+
for digit in range(10):
|
|
320
|
+
state = self._settings_widgets[digit]
|
|
321
|
+
current_name = state.name_widget.text()
|
|
322
|
+
formatted_name = current_name.strip()
|
|
323
|
+
if current_name != formatted_name:
|
|
324
|
+
state.name_widget.setText(formatted_name)
|
|
325
|
+
|
|
326
|
+
# Check if names are unique and frequencies add up to 1
|
|
327
|
+
names = []
|
|
328
|
+
frequencies = []
|
|
329
|
+
for digit in range(10):
|
|
330
|
+
state = self._settings_widgets[digit]
|
|
331
|
+
if state.enabled_widget.isChecked():
|
|
332
|
+
names.append(state.name_widget.text())
|
|
333
|
+
frequencies.append(state.frequency_widget.value())
|
|
334
|
+
if len(names) != len(set(names)):
|
|
335
|
+
message = "Error: names must be unique"
|
|
336
|
+
if sum(frequencies) != 1:
|
|
337
|
+
message = "Error: sum(frequencies) != 1"
|
|
338
|
+
|
|
339
|
+
# Check validity of EMG filter settings (read from UI, not saved state)
|
|
340
|
+
bp_lower = self._ui.bp_lower_spinbox.value()
|
|
341
|
+
bp_upper = self._ui.bp_upper_spinbox.value()
|
|
342
|
+
if bp_lower >= bp_upper:
|
|
343
|
+
message = "Error: EMG filter cutoff frequencies are invalid"
|
|
344
|
+
|
|
345
|
+
if message is not None:
|
|
346
|
+
self._ui.save_config_status.setText(message)
|
|
347
|
+
self._ui.save_config_button.setEnabled(False)
|
|
348
|
+
return message
|
|
349
|
+
|
|
350
|
+
self._ui.save_config_button.setEnabled(True)
|
|
351
|
+
self._ui.save_config_status.setText("")
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
def save_config(self) -> None:
|
|
355
|
+
"""Save configuration to file"""
|
|
356
|
+
# Check that configuration is valid
|
|
357
|
+
error_message = self.check_validity()
|
|
358
|
+
if error_message is not None:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
# Build a BrainStateSet object from the current configuration
|
|
362
|
+
brain_states = []
|
|
363
|
+
for digit in range(10):
|
|
364
|
+
state = self._settings_widgets[digit]
|
|
365
|
+
if state.enabled_widget.isChecked():
|
|
366
|
+
brain_states.append(
|
|
367
|
+
BrainState(
|
|
368
|
+
name=state.name_widget.text(),
|
|
369
|
+
digit=digit,
|
|
370
|
+
is_scored=state.is_scored_widget.isChecked(),
|
|
371
|
+
frequency=state.frequency_widget.value(),
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Update brain state set and EMG filter from UI
|
|
376
|
+
# Note that this only occurs when a valid configuration is saved
|
|
377
|
+
self._brain_state_set = BrainStateSet(brain_states, UNDEFINED_LABEL)
|
|
378
|
+
self._emg_filter = EMGFilter(
|
|
379
|
+
order=self._ui.emg_order_spinbox.value(),
|
|
380
|
+
bp_lower=self._ui.bp_lower_spinbox.value(),
|
|
381
|
+
bp_upper=self._ui.bp_upper_spinbox.value(),
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
# Save to file
|
|
385
|
+
save_config(
|
|
386
|
+
brain_state_set=self._brain_state_set,
|
|
387
|
+
default_epoch_length=self._ui.default_epoch_input.value(),
|
|
388
|
+
overwrite_setting=self._ui.overwrite_default_checkbox.isChecked(),
|
|
389
|
+
save_confidence_setting=self._ui.confidence_setting_checkbox.isChecked(),
|
|
390
|
+
min_bout_length=self._ui.default_min_bout_length_spinbox.value(),
|
|
391
|
+
emg_filter=self._emg_filter,
|
|
392
|
+
hyperparameters=self._hyperparameters,
|
|
393
|
+
epochs_to_show=self._ui.epochs_to_show_spinbox.value(),
|
|
394
|
+
autoscroll_state=self._ui.autoscroll_checkbox.isChecked(),
|
|
395
|
+
)
|
|
396
|
+
self._ui.save_config_status.setText("configuration saved")
|
|
397
|
+
|
|
398
|
+
def reset_emg_filter(self) -> None:
|
|
399
|
+
"""Reset EMG filter settings to defaults"""
|
|
400
|
+
self._ui.emg_order_spinbox.setValue(DEFAULT_EMG_FILTER_ORDER)
|
|
401
|
+
self._ui.bp_lower_spinbox.setValue(DEFAULT_EMG_BP_LOWER)
|
|
402
|
+
self._ui.bp_upper_spinbox.setValue(DEFAULT_EMG_BP_UPPER)
|
|
403
|
+
|
|
404
|
+
def reset_hyperparams(self) -> None:
|
|
405
|
+
"""Reset hyperparameters to defaults"""
|
|
406
|
+
self._ui.batch_size_spinbox.setValue(DEFAULT_BATCH_SIZE)
|
|
407
|
+
self._ui.learning_rate_spinbox.setValue(DEFAULT_LEARNING_RATE)
|
|
408
|
+
self._ui.momentum_spinbox.setValue(DEFAULT_MOMENTUM)
|
|
409
|
+
self._ui.training_epochs_spinbox.setValue(DEFAULT_TRAINING_EPOCHS)
|
|
@@ -142,7 +142,7 @@ To train a new model on your own data:
|
|
|
142
142
|
of the training data to set aside for calibration.
|
|
143
143
|
7. Click "Train classification model" and enter a
|
|
144
144
|
filename for the trained model. Training can take some time.
|
|
145
|
-
The
|
|
145
|
+
The terminal will display progress updates.
|
|
146
146
|
|
|
147
147
|
## 4C. Automatic scoring
|
|
148
148
|
|