accusleepy 0.7.0__py3-none-any.whl → 0.7.1__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/constants.py CHANGED
@@ -1,3 +1,5 @@
1
+ import numpy as np
2
+
1
3
  # probably don't change these unless you really need to
2
4
  UNDEFINED_LABEL = -1 # can't be the same as a brain state's digit, must be an integer
3
5
  # calibration file columns
@@ -9,9 +11,16 @@ EMG_COL = "emg"
9
11
  # label file columns
10
12
  BRAIN_STATE_COL = "brain_state"
11
13
  CONFIDENCE_SCORE_COL = "confidence_score"
14
+ # max number of messages to store in main window message box
15
+ MESSAGE_BOX_MAX_DEPTH = 200
16
+ # clip mixture z-scores above and below this level
17
+ # in the matlab implementation, 4.5 was used
18
+ ABS_MAX_Z_SCORE = 3.5
19
+ # upper frequency limit when generating EEG spectrograms
20
+ SPECTROGRAM_UPPER_FREQ = 64
12
21
 
13
22
 
14
- # really don't change these
23
+ # very unlikely you will want to change values from here onwards
15
24
  # config file location
16
25
  CONFIG_FILE = "config.json"
17
26
  # number of times to include the EMG power in a training image
@@ -20,8 +29,15 @@ EMG_COPIES = 9
20
29
  MIN_WINDOW_LEN = 5
21
30
  # frequency above which to downsample EEG spectrograms
22
31
  DOWNSAMPLING_START_FREQ = 20
23
- # upper frequency cutoff for EEG spectrograms
32
+ # highest EEG frequency used as model input
24
33
  UPPER_FREQ = 50
34
+ # height in pixels of each training image
35
+ IMAGE_HEIGHT = (
36
+ len(np.arange(0, DOWNSAMPLING_START_FREQ, 1 / MIN_WINDOW_LEN))
37
+ + len(np.arange(DOWNSAMPLING_START_FREQ, UPPER_FREQ, 2 / MIN_WINDOW_LEN))
38
+ + EMG_COPIES
39
+ )
40
+
25
41
  # classification model types
26
42
  DEFAULT_MODEL_TYPE = "default" # current epoch is centered
27
43
  REAL_TIME_MODEL_TYPE = "real-time" # current epoch on the right
Binary file
accusleepy/gui/main.py CHANGED
@@ -47,6 +47,7 @@ from accusleepy.constants import (
47
47
  DEFAULT_MOMENTUM,
48
48
  DEFAULT_TRAINING_EPOCHS,
49
49
  LABEL_FILE_TYPE,
50
+ MESSAGE_BOX_MAX_DEPTH,
50
51
  MODEL_FILE_TYPE,
51
52
  REAL_TIME_MODEL_TYPE,
52
53
  RECORDING_FILE_TYPES,
@@ -72,14 +73,15 @@ from accusleepy.signal_processing import (
72
73
  create_training_images,
73
74
  resample_and_standardize,
74
75
  )
76
+ from accusleepy.validation import (
77
+ check_label_validity,
78
+ LABEL_LENGTH_ERROR,
79
+ check_config_consistency,
80
+ )
75
81
 
76
82
  # note: functions using torch or scipy are lazily imported
77
83
 
78
- # max number of messages to display
79
- MESSAGE_BOX_MAX_DEPTH = 200
80
- LABEL_LENGTH_ERROR = "label file length does not match recording length"
81
- # relative path to config guide txt file
82
- CONFIG_GUIDE_FILE = os.path.normpath(r"text/config_guide.txt")
84
+ # relative path to user manual
83
85
  MAIN_GUIDE_FILE = os.path.normpath(r"text/main_guide.md")
84
86
 
85
87
 
@@ -219,10 +221,9 @@ class AccuSleepWindow(QMainWindow):
219
221
 
220
222
  :param default_selected: whether default option is selected
221
223
  """
222
- if default_selected:
223
- self.model_type = DEFAULT_MODEL_TYPE
224
- else:
225
- self.model_type = REAL_TIME_MODEL_TYPE
224
+ self.model_type = (
225
+ DEFAULT_MODEL_TYPE if default_selected else REAL_TIME_MODEL_TYPE
226
+ )
226
227
 
227
228
  def export_recording_list(self) -> None:
228
229
  """Save current list of recordings to file"""
@@ -1279,11 +1280,11 @@ class AccuSleepWindow(QMainWindow):
1279
1280
  state.enabled_widget.stateChanged.connect(
1280
1281
  partial(self.set_brain_state_enabled, digit)
1281
1282
  )
1282
- state.name_widget.editingFinished.connect(self.finished_editing_state_name)
1283
+ state.name_widget.editingFinished.connect(self.check_config_validity)
1283
1284
  state.is_scored_widget.stateChanged.connect(
1284
1285
  partial(self.is_scored_changed, digit)
1285
1286
  )
1286
- state.frequency_widget.valueChanged.connect(self.state_frequency_changed)
1287
+ state.frequency_widget.valueChanged.connect(self.check_config_validity)
1287
1288
 
1288
1289
  def set_brain_state_enabled(self, digit, e) -> None:
1289
1290
  """Called when user clicks "enabled" checkbox
@@ -1309,17 +1310,6 @@ class AccuSleepWindow(QMainWindow):
1309
1310
  # check that configuration is valid
1310
1311
  _ = self.check_config_validity()
1311
1312
 
1312
- def finished_editing_state_name(self) -> None:
1313
- """Called when user finishes editing a brain state's name"""
1314
- _ = self.check_config_validity()
1315
-
1316
- def state_frequency_changed(self, new_value) -> None:
1317
- """Called when user edits a brain state's frequency
1318
-
1319
- :param new_value: unused
1320
- """
1321
- _ = self.check_config_validity()
1322
-
1323
1313
  def is_scored_changed(self, digit, e) -> None:
1324
1314
  """Called when user sets whether a state is scored
1325
1315
 
@@ -1464,128 +1454,6 @@ class AccuSleepWindow(QMainWindow):
1464
1454
  self.ui.training_epochs_spinbox.setValue(DEFAULT_TRAINING_EPOCHS)
1465
1455
 
1466
1456
 
1467
- def check_label_validity(
1468
- labels: np.array,
1469
- confidence_scores: np.array,
1470
- samples_in_recording: int,
1471
- sampling_rate: int | float,
1472
- epoch_length: int | float,
1473
- brain_state_set: BrainStateSet,
1474
- ) -> str | None:
1475
- """Check whether a set of brain state labels is valid
1476
-
1477
- This returns an error message if a problem is found with the
1478
- brain state labels.
1479
-
1480
- :param labels: brain state labels
1481
- :param confidence_scores: confidence scores
1482
- :param samples_in_recording: number of samples in the recording
1483
- :param sampling_rate: sampling rate, in Hz
1484
- :param epoch_length: epoch length, in seconds
1485
- :param brain_state_set: BrainStateMapper object
1486
- :return: error message
1487
- """
1488
- # check that number of labels is correct
1489
- samples_per_epoch = round(sampling_rate * epoch_length)
1490
- epochs_in_recording = round(samples_in_recording / samples_per_epoch)
1491
- if epochs_in_recording != labels.size:
1492
- return LABEL_LENGTH_ERROR
1493
-
1494
- # check that entries are valid
1495
- if not set(labels.tolist()).issubset(
1496
- set([b.digit for b in brain_state_set.brain_states] + [UNDEFINED_LABEL])
1497
- ):
1498
- return "label file contains invalid entries"
1499
-
1500
- if confidence_scores is not None:
1501
- if np.min(confidence_scores) < 0 or np.max(confidence_scores) > 1:
1502
- return "label file contains invalid confidence scores"
1503
-
1504
- return None
1505
-
1506
-
1507
- def check_config_consistency(
1508
- current_brain_states: dict,
1509
- model_brain_states: dict,
1510
- current_epoch_length: int | float,
1511
- model_epoch_length: int | float,
1512
- ) -> list[str]:
1513
- """Compare current brain state config to the model's config
1514
-
1515
- This only displays warnings - the user should decide whether to proceed
1516
-
1517
- :param current_brain_states: current brain state config
1518
- :param model_brain_states: brain state config when the model was created
1519
- :param current_epoch_length: current epoch length setting
1520
- :param model_epoch_length: epoch length used when the model was created
1521
- """
1522
- output = list()
1523
-
1524
- # make lists of names and digits for scored brain states
1525
- current_scored_states = {
1526
- f: [b[f] for b in current_brain_states if b["is_scored"]]
1527
- for f in ["name", "digit"]
1528
- }
1529
- model_scored_states = {
1530
- f: [b[f] for b in model_brain_states if b["is_scored"]]
1531
- for f in ["name", "digit"]
1532
- }
1533
-
1534
- # generate message comparing the brain state configs
1535
- config_comparisons = list()
1536
- for config, config_name in zip(
1537
- [current_scored_states, model_scored_states], ["current", "model's"]
1538
- ):
1539
- config_comparisons.append(
1540
- f"Scored brain states in {config_name} configuration: "
1541
- f"""{
1542
- ", ".join(
1543
- [
1544
- f"{x}: {y}"
1545
- for x, y in zip(
1546
- config["digit"],
1547
- config["name"],
1548
- )
1549
- ]
1550
- )
1551
- }"""
1552
- )
1553
-
1554
- # check if the number of scored states is different
1555
- len_diff = len(current_scored_states["name"]) - len(model_scored_states["name"])
1556
- if len_diff != 0:
1557
- output.append(
1558
- (
1559
- "WARNING: current brain state configuration has "
1560
- f"{'fewer' if len_diff < 0 else 'more'} "
1561
- "scored brain states than the model's configuration."
1562
- )
1563
- )
1564
- output = output + config_comparisons
1565
- else:
1566
- # the length is the same, but names might be different
1567
- if current_scored_states["name"] != model_scored_states["name"]:
1568
- output.append(
1569
- (
1570
- "WARNING: current brain state configuration appears "
1571
- "to contain different brain states than "
1572
- "the model's configuration."
1573
- )
1574
- )
1575
- output = output + config_comparisons
1576
-
1577
- if current_epoch_length != model_epoch_length:
1578
- output.append(
1579
- (
1580
- "Warning: the epoch length used when training this model "
1581
- f"({model_epoch_length} seconds) "
1582
- "does not match the current epoch length setting."
1583
- )
1584
- )
1585
-
1586
- return output
1587
-
1588
-
1589
1457
  def run_primary_window() -> None:
1590
1458
  app = QApplication(sys.argv)
1591
1459
  AccuSleepWindow()
@@ -79,6 +79,12 @@ UNDEFINED_STATE = "undefined"
79
79
  SCROLL_BOUNDARY = 0.35
80
80
  # max number of sequential undo actions allowed
81
81
  UNDO_LIMIT = 1000
82
+ # brightness scaling factors for the spectrogram
83
+ BRIGHTER_SCALE_FACTOR = 0.96
84
+ DIMMER_SCALE_FACTOR = 1.07
85
+ # zoom factor for upper plots
86
+ ZOOM_IN_FACTOR = 0.45
87
+ ZOOM_OUT_FACTOR = 1.017
82
88
 
83
89
 
84
90
  @dataclass
@@ -232,23 +238,6 @@ class ManualScoringWindow(QDialog):
232
238
  keypress_zoom_out_x = QShortcut(QKeySequence(Qt.Key.Key_Minus), self)
233
239
  keypress_zoom_out_x.activated.connect(partial(self.zoom_x, ZOOM_OUT))
234
240
 
235
- keypress_modify_label = list()
236
- for brain_state in self.brain_state_set.brain_states:
237
- keypress_modify_label.append(
238
- QShortcut(
239
- QKeySequence(Qt.Key[f"Key_{brain_state.digit}"]),
240
- self,
241
- )
242
- )
243
- keypress_modify_label[-1].activated.connect(
244
- partial(self.modify_current_epoch_label, brain_state.digit)
245
- )
246
-
247
- keypress_delete_label = QShortcut(QKeySequence(Qt.Key.Key_Backspace), self)
248
- keypress_delete_label.activated.connect(
249
- partial(self.modify_current_epoch_label, UNDEFINED_LABEL)
250
- )
251
-
252
241
  keypress_quit = QShortcut(
253
242
  QKeySequence(QKeyCombination(Qt.Modifier.CTRL, Qt.Key.Key_W)),
254
243
  self,
@@ -261,36 +250,37 @@ class ManualScoringWindow(QDialog):
261
250
  )
262
251
  keypress_save.activated.connect(self.save)
263
252
 
253
+ keypress_modify_label = list()
264
254
  keypress_roi = list()
265
- for brain_state in self.brain_state_set.brain_states:
255
+ digit_key_label_pairs = [
256
+ (Qt.Key[f"Key_{brain_state.digit}"], brain_state.digit)
257
+ for brain_state in self.brain_state_set.brain_states
258
+ ] + [(Qt.Key.Key_Backspace, UNDEFINED_LABEL)]
259
+
260
+ for digit_key, digit_label in digit_key_label_pairs:
261
+ keypress_modify_label.append(
262
+ QShortcut(
263
+ QKeySequence(digit_key),
264
+ self,
265
+ )
266
+ )
267
+ keypress_modify_label[-1].activated.connect(
268
+ partial(self.modify_current_epoch_label, digit_label)
269
+ )
266
270
  keypress_roi.append(
267
271
  QShortcut(
268
272
  QKeySequence(
269
273
  QKeyCombination(
270
274
  Qt.Modifier.SHIFT,
271
- Qt.Key[f"Key_{brain_state.digit}"],
275
+ digit_key,
272
276
  )
273
277
  ),
274
278
  self,
275
279
  )
276
280
  )
277
281
  keypress_roi[-1].activated.connect(
278
- partial(self.enter_label_roi_mode, brain_state.digit)
279
- )
280
- keypress_roi.append(
281
- QShortcut(
282
- QKeySequence(
283
- QKeyCombination(
284
- Qt.Modifier.SHIFT,
285
- Qt.Key.Key_Backspace,
286
- )
287
- ),
288
- self,
282
+ partial(self.enter_label_roi_mode, digit_label)
289
283
  )
290
- )
291
- keypress_roi[-1].activated.connect(
292
- partial(self.enter_label_roi_mode, UNDEFINED_LABEL)
293
- )
294
284
 
295
285
  keypress_esc = QShortcut(QKeySequence(Qt.Key.Key_Escape), self)
296
286
  keypress_esc.activated.connect(self.exit_label_roi_mode)
@@ -626,9 +616,9 @@ class ManualScoringWindow(QDialog):
626
616
  """
627
617
  vmin, vmax = self.ui.upperfigure.spec_ref.get_clim()
628
618
  if direction == BRIGHTER:
629
- self.ui.upperfigure.spec_ref.set(clim=(vmin, vmax * 0.96))
619
+ self.ui.upperfigure.spec_ref.set(clim=(vmin, vmax * BRIGHTER_SCALE_FACTOR))
630
620
  else:
631
- self.ui.upperfigure.spec_ref.set(clim=(vmin, vmax * 1.07))
621
+ self.ui.upperfigure.spec_ref.set(clim=(vmin, vmax * DIMMER_SCALE_FACTOR))
632
622
  self.ui.upperfigure.canvas.draw()
633
623
 
634
624
  def update_epochs_shown(self, direction: str) -> None:
@@ -725,31 +715,29 @@ class ManualScoringWindow(QDialog):
725
715
 
726
716
  :param direction: in, out, or reset
727
717
  """
728
- zoom_in_factor = 0.45
729
- zoom_out_factor = 1.017
730
718
  epochs_shown = self.upper_right_epoch - self.upper_left_epoch + 1
731
719
  if direction == ZOOM_IN:
732
720
  self.upper_left_epoch = max(
733
721
  [
734
722
  self.upper_left_epoch,
735
- round(self.epoch - zoom_in_factor * epochs_shown),
723
+ round(self.epoch - ZOOM_IN_FACTOR * epochs_shown),
736
724
  ]
737
725
  )
738
726
 
739
727
  self.upper_right_epoch = min(
740
728
  [
741
729
  self.upper_right_epoch,
742
- round(self.epoch + zoom_in_factor * epochs_shown),
730
+ round(self.epoch + ZOOM_IN_FACTOR * epochs_shown),
743
731
  ]
744
732
  )
745
733
 
746
734
  elif direction == ZOOM_OUT:
747
735
  self.upper_left_epoch = max(
748
- [0, round(self.epoch - zoom_out_factor * epochs_shown)]
736
+ [0, round(self.epoch - ZOOM_OUT_FACTOR * epochs_shown)]
749
737
  )
750
738
 
751
739
  self.upper_right_epoch = min(
752
- [self.n_epochs - 1, round(self.epoch + zoom_out_factor * epochs_shown)]
740
+ [self.n_epochs - 1, round(self.epoch + ZOOM_OUT_FACTOR * epochs_shown)]
753
741
  )
754
742
 
755
743
  else: # reset
@@ -1085,7 +1073,9 @@ def create_upper_emg_signal(
1085
1073
  epoch_length,
1086
1074
  emg_filter,
1087
1075
  )
1088
- return np.clip(emg_rms, np.min(emg_rms), np.mean(emg_rms) + np.std(emg_rms) * 2.5)
1076
+ return np.clip(
1077
+ emg_rms, np.percentile(emg_rms, 0.1), np.mean(emg_rms) + np.std(emg_rms) * 2.5
1078
+ )
1089
1079
 
1090
1080
 
1091
1081
  def transform_eeg_emg(eeg: np.array, emg: np.array) -> (np.array, np.array):
@@ -8,8 +8,20 @@
8
8
  ## WARNING! All changes made in this file will be lost when recompiling UI file!
9
9
  ################################################################################
10
10
 
11
- from PySide6.QtCore import QCoreApplication, QMetaObject, QRect, QSize, Qt
12
- from PySide6.QtGui import QBrush, QColor, QFont, QIcon, QPalette
11
+ from PySide6.QtCore import (
12
+ QCoreApplication,
13
+ QMetaObject,
14
+ QRect,
15
+ QSize,
16
+ Qt,
17
+ )
18
+ from PySide6.QtGui import (
19
+ QBrush,
20
+ QColor,
21
+ QFont,
22
+ QIcon,
23
+ QPalette,
24
+ )
13
25
  from PySide6.QtWidgets import (
14
26
  QCheckBox,
15
27
  QComboBox,
@@ -2785,7 +2797,10 @@ class Ui_PrimaryWindow(object):
2785
2797
  QCoreApplication.translate("PrimaryWindow", "MainWindow", None)
2786
2798
  )
2787
2799
  self.epochlengthlabel.setText(
2788
- QCoreApplication.translate("PrimaryWindow", "Epoch length (sec):", None)
2800
+ QCoreApplication.translate("PrimaryWindow", "Epoch length:", None)
2801
+ )
2802
+ self.epoch_length_input.setSuffix(
2803
+ QCoreApplication.translate("PrimaryWindow", " sec", None)
2789
2804
  )
2790
2805
  self.recordinglistgroupbox.setTitle(
2791
2806
  QCoreApplication.translate("PrimaryWindow", "Recording list", None)
@@ -2840,7 +2855,10 @@ class Ui_PrimaryWindow(object):
2840
2855
  )
2841
2856
  )
2842
2857
  self.samplingratelabel.setText(
2843
- QCoreApplication.translate("PrimaryWindow", "Sampling rate (Hz):", None)
2858
+ QCoreApplication.translate("PrimaryWindow", "Sampling rate:", None)
2859
+ )
2860
+ self.sampling_rate_input.setSuffix(
2861
+ QCoreApplication.translate("PrimaryWindow", " Hz", None)
2844
2862
  )
2845
2863
  # if QT_CONFIG(tooltip)
2846
2864
  self.recording_file_button.setToolTip(
@@ -2948,11 +2966,12 @@ class Ui_PrimaryWindow(object):
2948
2966
  QCoreApplication.translate("PrimaryWindow", "Save confidence scores", None)
2949
2967
  )
2950
2968
  self.boutlengthlabel.setText(
2951
- QCoreApplication.translate(
2952
- "PrimaryWindow", "Minimum bout length (sec):", None
2953
- )
2969
+ QCoreApplication.translate("PrimaryWindow", "Minimum bout length:", None)
2954
2970
  )
2955
2971
  self.bout_length_input.setPrefix("")
2972
+ self.bout_length_input.setSuffix(
2973
+ QCoreApplication.translate("PrimaryWindow", " sec", None)
2974
+ )
2956
2975
  self.lower_tab_widget.setTabText(
2957
2976
  self.lower_tab_widget.indexOf(self.classification_tab),
2958
2977
  QCoreApplication.translate("PrimaryWindow", "Classification", None),
@@ -3096,6 +3115,9 @@ class Ui_PrimaryWindow(object):
3096
3115
  self.label_17.setText(
3097
3116
  QCoreApplication.translate("PrimaryWindow", "Epoch length:", None)
3098
3117
  )
3118
+ self.default_epoch_input.setSuffix(
3119
+ QCoreApplication.translate("PrimaryWindow", " sec", None)
3120
+ )
3099
3121
  self.overwrite_default_checkbox.setText(
3100
3122
  QCoreApplication.translate(
3101
3123
  "PrimaryWindow", "Only overwrite undefined epochs", None
@@ -3105,9 +3127,10 @@ class Ui_PrimaryWindow(object):
3105
3127
  QCoreApplication.translate("PrimaryWindow", "Save confidence scores", None)
3106
3128
  )
3107
3129
  self.label_19.setText(
3108
- QCoreApplication.translate(
3109
- "PrimaryWindow", "Minimum bout length (sec):", None
3110
- )
3130
+ QCoreApplication.translate("PrimaryWindow", "Minimum bout length:", None)
3131
+ )
3132
+ self.default_min_bout_length_spinbox.setSuffix(
3133
+ QCoreApplication.translate("PrimaryWindow", " sec", None)
3111
3134
  )
3112
3135
  self.ui_default_description_label.setText(
3113
3136
  QCoreApplication.translate(
@@ -152,7 +152,7 @@
152
152
  </sizepolicy>
153
153
  </property>
154
154
  <property name="text">
155
- <string>Epoch length (sec):</string>
155
+ <string>Epoch length:</string>
156
156
  </property>
157
157
  </widget>
158
158
  </item>
@@ -167,6 +167,9 @@
167
167
  <property name="alignment">
168
168
  <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
169
169
  </property>
170
+ <property name="suffix">
171
+ <string> sec</string>
172
+ </property>
170
173
  <property name="maximum">
171
174
  <double>100000.000000000000000</double>
172
175
  </property>
@@ -529,7 +532,7 @@ color: rgb(244, 195, 68);</string>
529
532
  </sizepolicy>
530
533
  </property>
531
534
  <property name="text">
532
- <string>Sampling rate (Hz):</string>
535
+ <string>Sampling rate:</string>
533
536
  </property>
534
537
  </widget>
535
538
  </item>
@@ -544,6 +547,9 @@ color: rgb(244, 195, 68);</string>
544
547
  <property name="alignment">
545
548
  <set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
546
549
  </property>
550
+ <property name="suffix">
551
+ <string> Hz</string>
552
+ </property>
547
553
  <property name="minimum">
548
554
  <double>0.000000000000000</double>
549
555
  </property>
@@ -1148,7 +1154,7 @@ color: rgb(244, 195, 68);</string>
1148
1154
  <string notr="true">background-color: transparent;</string>
1149
1155
  </property>
1150
1156
  <property name="text">
1151
- <string>Minimum bout length (sec):</string>
1157
+ <string>Minimum bout length:</string>
1152
1158
  </property>
1153
1159
  </widget>
1154
1160
  </item>
@@ -1166,6 +1172,9 @@ color: rgb(244, 195, 68);</string>
1166
1172
  <property name="prefix">
1167
1173
  <string/>
1168
1174
  </property>
1175
+ <property name="suffix">
1176
+ <string> sec</string>
1177
+ </property>
1169
1178
  <property name="decimals">
1170
1179
  <number>2</number>
1171
1180
  </property>
@@ -3811,6 +3820,9 @@ Each brain state has several attributes:
3811
3820
  <verstretch>0</verstretch>
3812
3821
  </sizepolicy>
3813
3822
  </property>
3823
+ <property name="suffix">
3824
+ <string> sec</string>
3825
+ </property>
3814
3826
  <property name="maximum">
3815
3827
  <double>100000.000000000000000</double>
3816
3828
  </property>
@@ -3920,7 +3932,7 @@ Each brain state has several attributes:
3920
3932
  </sizepolicy>
3921
3933
  </property>
3922
3934
  <property name="text">
3923
- <string>Minimum bout length (sec):</string>
3935
+ <string>Minimum bout length:</string>
3924
3936
  </property>
3925
3937
  </widget>
3926
3938
  </item>
@@ -3932,6 +3944,9 @@ Each brain state has several attributes:
3932
3944
  <verstretch>0</verstretch>
3933
3945
  </sizepolicy>
3934
3946
  </property>
3947
+ <property name="suffix">
3948
+ <string> sec</string>
3949
+ </property>
3935
3950
  <property name="maximum">
3936
3951
  <double>1000.000000000000000</double>
3937
3952
  </property>
accusleepy/models.py CHANGED
@@ -1,24 +1,13 @@
1
- import numpy as np
2
1
  from torch import device, flatten, nn
3
2
  from torch import load as torch_load
4
3
  from torch import save as torch_save
5
4
 
6
5
  from accusleepy.brain_state_set import BRAIN_STATES_KEY, BrainStateSet
7
6
  from accusleepy.constants import (
8
- DOWNSAMPLING_START_FREQ,
9
- EMG_COPIES,
10
- MIN_WINDOW_LEN,
11
- UPPER_FREQ,
7
+ IMAGE_HEIGHT,
12
8
  )
13
9
  from accusleepy.temperature_scaling import ModelWithTemperature
14
10
 
15
- # height in pixels of each training image
16
- IMAGE_HEIGHT = (
17
- len(np.arange(0, DOWNSAMPLING_START_FREQ, 1 / MIN_WINDOW_LEN))
18
- + len(np.arange(DOWNSAMPLING_START_FREQ, UPPER_FREQ, 2 / MIN_WINDOW_LEN))
19
- + EMG_COPIES
20
- )
21
-
22
11
 
23
12
  class SSANN(nn.Module):
24
13
  """Small CNN for classifying images"""
@@ -8,6 +8,7 @@ from tqdm import trange
8
8
 
9
9
  from accusleepy.brain_state_set import BrainStateSet
10
10
  from accusleepy.constants import (
11
+ ABS_MAX_Z_SCORE,
11
12
  ANNOTATIONS_FILENAME,
12
13
  CALIBRATION_ANNOTATION_FILENAME,
13
14
  DEFAULT_MODEL_TYPE,
@@ -17,18 +18,13 @@ from accusleepy.constants import (
17
18
  LABEL_COL,
18
19
  MIN_WINDOW_LEN,
19
20
  UPPER_FREQ,
21
+ SPECTROGRAM_UPPER_FREQ,
20
22
  )
21
23
  from accusleepy.fileio import Recording, load_labels, load_recording, EMGFilter
22
24
  from accusleepy.multitaper import spectrogram
23
25
 
24
26
  # note: scipy is lazily imported
25
27
 
26
- # clip mixture z-scores above and below this level
27
- # in the matlab implementation, I used 4.5
28
- ABS_MAX_Z_SCORE = 3.5
29
- # upper frequency limit when generating EEG spectrograms
30
- SPECTROGRAM_UPPER_FREQ = 64
31
-
32
28
 
33
29
  def resample(
34
30
  eeg: np.array, emg: np.array, sampling_rate: int | float, epoch_length: int | float
@@ -0,0 +1,128 @@
1
+ import numpy as np
2
+
3
+ from accusleepy.brain_state_set import BrainStateSet
4
+ from accusleepy.constants import UNDEFINED_LABEL
5
+
6
+ LABEL_LENGTH_ERROR = "label file length does not match recording length"
7
+
8
+
9
+ def check_label_validity(
10
+ labels: np.array,
11
+ confidence_scores: np.array,
12
+ samples_in_recording: int,
13
+ sampling_rate: int | float,
14
+ epoch_length: int | float,
15
+ brain_state_set: BrainStateSet,
16
+ ) -> str | None:
17
+ """Check whether a set of brain state labels is valid
18
+
19
+ This returns an error message if a problem is found with the
20
+ brain state labels.
21
+
22
+ :param labels: brain state labels
23
+ :param confidence_scores: confidence scores
24
+ :param samples_in_recording: number of samples in the recording
25
+ :param sampling_rate: sampling rate, in Hz
26
+ :param epoch_length: epoch length, in seconds
27
+ :param brain_state_set: BrainStateMapper object
28
+ :return: error message
29
+ """
30
+ # check that number of labels is correct
31
+ samples_per_epoch = round(sampling_rate * epoch_length)
32
+ epochs_in_recording = round(samples_in_recording / samples_per_epoch)
33
+ if epochs_in_recording != labels.size:
34
+ return LABEL_LENGTH_ERROR
35
+
36
+ # check that entries are valid
37
+ if not set(labels.tolist()).issubset(
38
+ set([b.digit for b in brain_state_set.brain_states] + [UNDEFINED_LABEL])
39
+ ):
40
+ return "label file contains invalid entries"
41
+
42
+ if confidence_scores is not None:
43
+ if np.min(confidence_scores) < 0 or np.max(confidence_scores) > 1:
44
+ return "label file contains invalid confidence scores"
45
+
46
+ return None
47
+
48
+
49
+ def check_config_consistency(
50
+ current_brain_states: dict,
51
+ model_brain_states: dict,
52
+ current_epoch_length: int | float,
53
+ model_epoch_length: int | float,
54
+ ) -> list[str]:
55
+ """Compare current brain state config to the model's config
56
+
57
+ This only displays warnings - the user should decide whether to proceed
58
+
59
+ :param current_brain_states: current brain state config
60
+ :param model_brain_states: brain state config when the model was created
61
+ :param current_epoch_length: current epoch length setting
62
+ :param model_epoch_length: epoch length used when the model was created
63
+ """
64
+ output = list()
65
+
66
+ # make lists of names and digits for scored brain states
67
+ current_scored_states = {
68
+ f: [b[f] for b in current_brain_states if b["is_scored"]]
69
+ for f in ["name", "digit"]
70
+ }
71
+ model_scored_states = {
72
+ f: [b[f] for b in model_brain_states if b["is_scored"]]
73
+ for f in ["name", "digit"]
74
+ }
75
+
76
+ # generate message comparing the brain state configs
77
+ config_comparisons = list()
78
+ for config, config_name in zip(
79
+ [current_scored_states, model_scored_states], ["current", "model's"]
80
+ ):
81
+ config_comparisons.append(
82
+ f"Scored brain states in {config_name} configuration: "
83
+ f"""{
84
+ ", ".join(
85
+ [
86
+ f"{x}: {y}"
87
+ for x, y in zip(
88
+ config["digit"],
89
+ config["name"],
90
+ )
91
+ ]
92
+ )
93
+ }"""
94
+ )
95
+
96
+ # check if the number of scored states is different
97
+ len_diff = len(current_scored_states["name"]) - len(model_scored_states["name"])
98
+ if len_diff != 0:
99
+ output.append(
100
+ (
101
+ "WARNING: current brain state configuration has "
102
+ f"{'fewer' if len_diff < 0 else 'more'} "
103
+ "scored brain states than the model's configuration."
104
+ )
105
+ )
106
+ output = output + config_comparisons
107
+ else:
108
+ # the length is the same, but names might be different
109
+ if current_scored_states["name"] != model_scored_states["name"]:
110
+ output.append(
111
+ (
112
+ "WARNING: current brain state configuration appears "
113
+ "to contain different brain states than "
114
+ "the model's configuration."
115
+ )
116
+ )
117
+ output = output + config_comparisons
118
+
119
+ if current_epoch_length != model_epoch_length:
120
+ output.append(
121
+ (
122
+ "Warning: the epoch length used when training this model "
123
+ f"({model_epoch_length} seconds) "
124
+ "does not match the current epoch length setting."
125
+ )
126
+ )
127
+
128
+ return output
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: accusleepy
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Python implementation of AccuSleep
5
5
  License: GPL-3.0-only
6
6
  Author: Zeke Barger
@@ -76,6 +76,7 @@ to the [config file](accusleepy/config.json).
76
76
 
77
77
  ## Changelog
78
78
 
79
+ - 0.7.1: Bugfixes, code cleanup
79
80
  - 0.7.0: More settings can be configured in the UI
80
81
  - 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
81
82
  since the new calibration feature will make the confidence scores more accurate.
@@ -4,7 +4,7 @@ accusleepy/bouts.py,sha256=F_y6DxnpKFfImYb7vCZluZ2eD5I_33gZXmRM8mvebsg,5679
4
4
  accusleepy/brain_state_set.py,sha256=fRkrArHLIbEKimub804yt_mUXoyfsjJEfiJnTjeCMkY,3233
5
5
  accusleepy/classification.py,sha256=mF35xMrD9QXGldSnl3vkdHbm7CAptPUNjHxUA_agOTA,9778
6
6
  accusleepy/config.json,sha256=Ip0qTMAn2LZfof9GVA_azOvpXP0WKnqLCZeSaya1sss,819
7
- accusleepy/constants.py,sha256=t7x-wzncJ_wVm0Oj6LiUzGukpsTsxfhO0KJiAAsuMN4,2244
7
+ accusleepy/constants.py,sha256=r53FWeMMefuKA-mR_b2E1KBeJbq2Ck9c5VHoJ1WGZjg,2815
8
8
  accusleepy/fileio.py,sha256=woIF0zgJt6Lx6T9KBXAQ-AlbQAwOK1_RUmVF710nltI,7383
9
9
  accusleepy/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  accusleepy/gui/icons/brightness_down.png,sha256=PLT1fb83RHIhSRuU7MMMx0G7oJAY7o9wUcnqM8veZfM,12432
@@ -18,24 +18,25 @@ accusleepy/gui/icons/save.png,sha256=J3EA8iU1BqLYRSsrq_OdoZlqrv2yfL7oV54DklTy_DI
18
18
  accusleepy/gui/icons/up_arrow.png,sha256=V9yF9t1WgjPaUu-mF1YGe_DfaRHg2dUpR_sUVVcvVvY,3329
19
19
  accusleepy/gui/icons/zoom_in.png,sha256=MFWnKZp7Rvh4bLPq4Cqo4sB_jQYedUUtT8-ZO8tNYyc,13589
20
20
  accusleepy/gui/icons/zoom_out.png,sha256=IB8Jecb3i0U4qjWRR46ridjLpvLCSe7PozBaLqQqYSw,13055
21
- accusleepy/gui/images/primary_window.png,sha256=ABu49UXTBfI5UwNHrpWPhJoTC7zhyLVvbsbywIuu1nc,602640
21
+ accusleepy/gui/images/primary_window.png,sha256=VVCjwolHsi4HdYehe3ZG_QpZ0XK39Qrz4Q6SqBAc5Cg,597045
22
22
  accusleepy/gui/images/viewer_window.png,sha256=b_B7m9WSLMAOzNjctq76SyekO1WfC6qYZVNnYfhjPe8,977197
23
23
  accusleepy/gui/images/viewer_window_annotated.png,sha256=uMNUmsZIdzDlQpyoiS3lJGoWlg_T325Oj5hDZhM3Y14,146817
24
- accusleepy/gui/main.py,sha256=Ywk3XwfwMjAZL76qwDnZRHQ96L7T1DWyMCU8la0N5Qg,62447
25
- accusleepy/gui/manual_scoring.py,sha256=xk0TERVbH33owj4QYJkMA2LF_qR8jtS5W2enq36_njk,40907
24
+ accusleepy/gui/main.py,sha256=sLHe1imVU6xz3f6sJ9u7sxrTny9X213ev29ldoxXpX0,57740
25
+ accusleepy/gui/manual_scoring.py,sha256=7jKdw-D9HdpYC2G_2RHt5UALLMjrIsP-hvLFkNM25Ek,40553
26
26
  accusleepy/gui/mplwidget.py,sha256=rJSTtWmLjHn8r3c9Kb23Rc4XzXl3i9B-JrjNjjlNnmQ,13492
27
- accusleepy/gui/primary_window.py,sha256=sVIsZXnszBsq3ZUC9OIOwB13jPQRcisaxi0Vg-O6A-8,135475
28
- accusleepy/gui/primary_window.ui,sha256=_SorGY7qJG2B6e45UNt51OheeYLkpzAxxezX1hEfI3Y,201341
27
+ accusleepy/gui/primary_window.py,sha256=Qc3WWiwJHbgmGDzGzNI3fBycBKtlpT5YdskwnHr6OWs,136070
28
+ accusleepy/gui/primary_window.ui,sha256=u7HQdeNKObKsUSOlk91gEMat-RrXITEsnBedZPd7y6Y,201950
29
29
  accusleepy/gui/resources.qrc,sha256=wqPendnTLAuKfVI6v2lKHiRqAWM0oaz2ZuF5cucJdS4,803
30
30
  accusleepy/gui/resources_rc.py,sha256=Z2e34h30U4snJjnYdZVV9B6yjATKxxfvgTRt5uXtQdo,329727
31
31
  accusleepy/gui/text/main_guide.md,sha256=iZDRp5OWyQX9LV7CMeUFIYv2ryKlIcGALRLXjxR8HpI,8288
32
32
  accusleepy/gui/text/manual_scoring_guide.md,sha256=ow_RMSjFy05NupEDSCuJtu-V65-BPnIkrZqtssFoZCQ,999
33
33
  accusleepy/gui/viewer_window.py,sha256=O4ceqLMYdahxQ9s6DYhloUnNESim-cqIZxFeXEiRjog,24444
34
34
  accusleepy/gui/viewer_window.ui,sha256=jsjydsSSyN49AwJw4nVS2mEJ2JBIUTXesAJsij1JNV0,31530
35
- accusleepy/models.py,sha256=15VjtFoWaYXblyGPbtYgp0yJdyUfGu7t3zCShdtr_7c,3799
35
+ accusleepy/models.py,sha256=kqkcQJoKi7gpnM8gZ7nZbWGvpv7ruNnFLaB7ED1X6Dc,3493
36
36
  accusleepy/multitaper.py,sha256=D5-iglwkFBRciL5tKSNcunMtcq0rM3zHwRHUVPgem1U,25679
37
- accusleepy/signal_processing.py,sha256=NOkQuLmUVINd0tFvt48RXRxSl4TDjV42m54XWc_EB9s,16987
37
+ accusleepy/signal_processing.py,sha256=47fEAx8Aqqkiqix1ai2YEK9Fhq6UHoQcwAcOi-a8ewo,16834
38
38
  accusleepy/temperature_scaling.py,sha256=glvPcvxHpBdFjwjGfZdNku9L_BozycEmdqZhKKUCCNg,5749
39
- accusleepy-0.7.0.dist-info/METADATA,sha256=yLkBaWfg1wrN7DevZlLLg881Yq9hhGoTsqHSgCJ0IYk,4536
40
- accusleepy-0.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
- accusleepy-0.7.0.dist-info/RECORD,,
39
+ accusleepy/validation.py,sha256=VpLWK-wD5tCU6lTBG3KYgTi3PWGuYh6NitMgMoMH8JM,4434
40
+ accusleepy-0.7.1.dist-info/METADATA,sha256=UXIUq2dyYIn7ehR0V0aq3NctxurAodZGGCnFrcNXj6c,4568
41
+ accusleepy-0.7.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
42
+ accusleepy-0.7.1.dist-info/RECORD,,