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.
@@ -139,18 +139,10 @@ class ManualScoringWindow(QDialog):
139
139
  self.ui.setupUi(self)
140
140
  self.setWindowTitle("AccuSleePy manual scoring window")
141
141
 
142
- # load set of valid brain states
143
- (
144
- self.brain_state_set,
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 Exception(f"style must be '{DISPLAY_FORMAT}' or '{DIGIT_FORMAT}'")
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:
@@ -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.0
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 store any changes.\n"
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: how the brain state is represented in label files, and keyboard shortcut for this state in manual scoring.\n"
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 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 all scored states must sum to 1.",
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 &quot;Sleep scoring&quot; 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 &quot;Sleep scoring&quot; 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.\n"
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
  )
@@ -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 store any changes.
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: how the brain state is represented in label files, and keyboard shortcut for this state in manual scoring.
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 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 all scored states must sum to 1.</string>
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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These are the default values/settings that are shown in the primary window and manual scoring window when they start up.&lt;/p&gt;&lt;p&gt;Changes here will not affect the **current** state of the controls in the &amp;quot;Sleep scoring&amp;quot; tab.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
4115
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;These are the default values/settings that are shown in the primary window and manual scoring window when they start up.&lt;/p&gt;&lt;p&gt;Changes here will not affect the **current** state of the controls in the &amp;quot;Sleep scoring&amp;quot; tab.&lt;/p&gt;&lt;p&gt;You must click 'Save settings' for changes to take effect.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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>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.
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>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;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.&lt;/p&gt;&lt;p&gt;You must click 'Save settings' for changes to take effect.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</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 console will display progress updates.
145
+ The terminal will display progress updates.
146
146
 
147
147
  ## 4C. Automatic scoring
148
148
 
accusleepy/models.py CHANGED
@@ -40,7 +40,7 @@ class SSANN(nn.Module):
40
40
 
41
41
 
42
42
  def save_model(
43
- model: SSANN,
43
+ model: SSANN | ModelWithTemperature,
44
44
  filename: str,
45
45
  epoch_length: int | float,
46
46
  epochs_per_img: int,