accusleepy 0.9.3__tar.gz → 0.10.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. {accusleepy-0.9.3 → accusleepy-0.10.1}/PKG-INFO +5 -8
  2. {accusleepy-0.9.3 → accusleepy-0.10.1}/README.md +2 -3
  3. accusleepy-0.10.1/accusleepy/__init__.py +1 -0
  4. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/__main__.py +2 -0
  5. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/bouts.py +2 -0
  6. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/brain_state_set.py +2 -0
  7. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/classification.py +2 -0
  8. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/config.json +2 -1
  9. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/constants.py +4 -0
  10. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/fileio.py +12 -3
  11. accusleepy-0.10.1/accusleepy/gui/__init__.py +1 -0
  12. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/primary_window.png +0 -0
  13. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/main.py +5 -7
  14. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/manual_scoring.py +105 -42
  15. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/mplwidget.py +2 -1
  16. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/primary_window.py +85 -85
  17. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/primary_window.ui +95 -107
  18. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/recording_manager.py +24 -14
  19. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/settings_widget.py +12 -0
  20. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/main_guide.md +15 -12
  21. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/manual_scoring_guide.md +1 -0
  22. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/models.py +2 -0
  23. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/multitaper.py +3 -60
  24. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/signal_processing.py +2 -0
  25. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/temperature_scaling.py +2 -0
  26. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/validation.py +2 -0
  27. {accusleepy-0.9.3 → accusleepy-0.10.1}/pyproject.toml +4 -5
  28. accusleepy-0.9.3/accusleepy/__init__.py +0 -0
  29. accusleepy-0.9.3/accusleepy/gui/__init__.py +0 -0
  30. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/dialogs.py +0 -0
  31. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/brightness_down.png +0 -0
  32. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/brightness_up.png +0 -0
  33. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/double_down_arrow.png +0 -0
  34. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/double_up_arrow.png +0 -0
  35. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/down_arrow.png +0 -0
  36. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/home.png +0 -0
  37. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/question.png +0 -0
  38. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/save.png +0 -0
  39. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/up_arrow.png +0 -0
  40. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/zoom_in.png +0 -0
  41. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/icons/zoom_out.png +0 -0
  42. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/viewer_window.png +0 -0
  43. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/images/viewer_window_annotated.png +0 -0
  44. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/resources.qrc +0 -0
  45. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/resources_rc.py +0 -0
  46. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/text/dev_guide.md +0 -0
  47. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/viewer_window.py +0 -0
  48. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/gui/viewer_window.ui +0 -0
  49. {accusleepy-0.9.3 → accusleepy-0.10.1}/accusleepy/services.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: accusleepy
3
- Version: 0.9.3
3
+ Version: 0.10.1
4
4
  Summary: Python implementation of AccuSleep
5
5
  License: GPL-3.0-only
6
6
  Author: Zeke Barger
@@ -11,14 +11,12 @@ Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
- Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
15
- Requires-Dist: joblib (>=1.4.2,<2.0.0)
16
14
  Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
17
15
  Requires-Dist: numpy (>=2.2.4,<3.0.0)
18
16
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
19
17
  Requires-Dist: pillow (>=11.1.0,<12.0.0)
20
- Requires-Dist: pre-commit (>=4.2.0,<5.0.0)
21
- Requires-Dist: pyside6 (>=6.9.0,<6.9.3)
18
+ Requires-Dist: pyarrow (>=23.0.0,<24.0.0)
19
+ Requires-Dist: pyside6 (>=6.10.1,<7.0.0)
22
20
  Requires-Dist: scipy (>=1.15.2,<2.0.0)
23
21
  Requires-Dist: torch (>=2.8.0,<3.0.0)
24
22
  Requires-Dist: torchvision (>=0.23.0,<1.0.0)
@@ -79,9 +77,8 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
79
77
 
80
78
  ## Changelog
81
79
 
82
- - 0.8.1-0.9.3: Improved error handling and code quality
83
- - 0.8.0: More configurable settings, visual improvements
84
- - 0.7.1-0.7.3: Bugfixes, code cleanup
80
+ - 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
81
+ - 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
85
82
  - 0.7.0: More settings can be configured in the UI
86
83
  - 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
87
84
  since the new calibration feature will make the confidence scores more accurate.
@@ -52,9 +52,8 @@ please consult the [developer guide](accusleepy/gui/text/dev_guide.md).
52
52
 
53
53
  ## Changelog
54
54
 
55
- - 0.8.1-0.9.3: Improved error handling and code quality
56
- - 0.8.0: More configurable settings, visual improvements
57
- - 0.7.1-0.7.3: Bugfixes, code cleanup
55
+ - 0.10.0-0.10.1: Improved zoom behavior, updated dependencies
56
+ - 0.7.1-0.9.3: Bugfixes, code cleanup, additional config settings
58
57
  - 0.7.0: More settings can be configured in the UI
59
58
  - 0.6.0: Confidence scores can now be displayed and saved. Retraining your models is recommended
60
59
  since the new calibration feature will make the confidence scores more accurate.
@@ -0,0 +1 @@
1
+ """AccuSleePy: Python implementation of AccuSleep for automated sleep scoring."""
@@ -1,3 +1,5 @@
1
+ """Entry point for running AccuSleePy as a module (python -m accusleepy)."""
2
+
1
3
  from accusleepy.gui.main import run_primary_window
2
4
 
3
5
  if __name__ == "__main__":
@@ -1,3 +1,5 @@
1
+ """Brain state bout length enforcement."""
2
+
1
3
  import re
2
4
  from dataclasses import dataclass
3
5
  from operator import attrgetter
@@ -1,3 +1,5 @@
1
+ """Define and organize brain states."""
2
+
1
3
  from dataclasses import dataclass
2
4
 
3
5
  import numpy as np
@@ -1,3 +1,5 @@
1
+ """Model training and brain state classification."""
2
+
1
3
  import os
2
4
 
3
5
  import numpy as np
@@ -35,5 +35,6 @@
35
35
  "training_epochs": 6
36
36
  },
37
37
  "epochs_to_show": 5,
38
- "autoscroll_state": false
38
+ "autoscroll_state": false,
39
+ "delete_training_images": true
39
40
  }
@@ -1,3 +1,5 @@
1
+ """Application-wide constants and default settings."""
2
+
1
3
  import numpy as np
2
4
 
3
5
  # probably don't change these unless you really need to
@@ -72,6 +74,7 @@ EMG_FILTER_KEY = "emg_filter"
72
74
  HYPERPARAMETERS_KEY = "hyperparameters"
73
75
  EPOCHS_TO_SHOW_KEY = "epochs_to_show"
74
76
  AUTOSCROLL_KEY = "autoscroll_state"
77
+ DELETE_TRAINING_IMAGES_KEY = "delete_training_images"
75
78
 
76
79
  # default values
77
80
  # default UI settings
@@ -87,6 +90,7 @@ DEFAULT_BATCH_SIZE = 64
87
90
  DEFAULT_LEARNING_RATE = 1e-3
88
91
  DEFAULT_MOMENTUM = 0.9
89
92
  DEFAULT_TRAINING_EPOCHS = 6
93
+ DEFAULT_DELETE_TRAINING_IMAGES_STATE = True
90
94
  # default manual scoring settings
91
95
  DEFAULT_EPOCHS_TO_SHOW = 5
92
96
  DEFAULT_AUTOSCROLL_STATE = False
@@ -1,3 +1,5 @@
1
+ """File I/O for recordings, labels, calibration data, and configuration."""
2
+
1
3
  import json
2
4
  import os
3
5
  from dataclasses import dataclass
@@ -5,7 +7,6 @@ from importlib.metadata import version, PackageNotFoundError
5
7
 
6
8
  import numpy as np
7
9
  import pandas as pd
8
- from PySide6.QtWidgets import QListWidgetItem
9
10
 
10
11
  from accusleepy.brain_state_set import BRAIN_STATES_KEY, BrainState, BrainStateSet
11
12
  import accusleepy.constants as c
@@ -43,6 +44,7 @@ class AccuSleePyConfig:
43
44
  hyperparameters: Hyperparameters
44
45
  epochs_to_show: int
45
46
  autoscroll_state: bool
47
+ delete_training_images: bool
46
48
 
47
49
 
48
50
  @dataclass
@@ -54,7 +56,6 @@ class Recording:
54
56
  label_file: str = "" # path to label file
55
57
  calibration_file: str = "" # path to calibration file
56
58
  sampling_rate: int | float = 0.0 # sampling rate, in Hz
57
- widget: QListWidgetItem = None # list item widget shown in the GUI
58
59
 
59
60
 
60
61
  def load_calibration_file(filename: str) -> tuple[np.ndarray, np.ndarray]:
@@ -139,7 +140,8 @@ def load_config() -> AccuSleePyConfig:
139
140
  EMG filter parameters,
140
141
  model training hyperparameters,
141
142
  default epochs to show for manual scoring,
142
- default autoscroll state for manual scoring
143
+ default autoscroll state for manual scoring,
144
+ setting to delete training images automatically
143
145
  """
144
146
  with open(
145
147
  os.path.join(os.path.dirname(os.path.abspath(__file__)), c.CONFIG_FILE), "r"
@@ -183,6 +185,9 @@ def load_config() -> AccuSleePyConfig:
183
185
  ),
184
186
  epochs_to_show=data.get(c.EPOCHS_TO_SHOW_KEY, c.DEFAULT_EPOCHS_TO_SHOW),
185
187
  autoscroll_state=data.get(c.AUTOSCROLL_KEY, c.DEFAULT_AUTOSCROLL_STATE),
188
+ delete_training_images=data.get(
189
+ c.DELETE_TRAINING_IMAGES_KEY, c.DEFAULT_DELETE_TRAINING_IMAGES_STATE
190
+ ),
186
191
  )
187
192
 
188
193
 
@@ -196,6 +201,7 @@ def save_config(
196
201
  hyperparameters: Hyperparameters,
197
202
  epochs_to_show: int,
198
203
  autoscroll_state: bool,
204
+ delete_training_images: bool,
199
205
  ) -> None:
200
206
  """Save configuration of brain state options to json file
201
207
 
@@ -210,6 +216,8 @@ def save_config(
210
216
  :param hyperparameters: model training hyperparameters
211
217
  :param epochs_to_show: default epochs to show for manual scoring,
212
218
  :param autoscroll_state: default autoscroll state for manual scoring
219
+ :param delete_training_images: whether to automatically delete images
220
+ created for model training once training is complete
213
221
  """
214
222
  output_dict = brain_state_set.to_output_dict()
215
223
  output_dict.update({c.DEFAULT_EPOCH_LENGTH_KEY: default_epoch_length})
@@ -220,6 +228,7 @@ def save_config(
220
228
  output_dict.update({c.HYPERPARAMETERS_KEY: hyperparameters.__dict__})
221
229
  output_dict.update({c.EPOCHS_TO_SHOW_KEY: epochs_to_show})
222
230
  output_dict.update({c.AUTOSCROLL_KEY: autoscroll_state})
231
+ output_dict.update({c.DELETE_TRAINING_IMAGES_KEY: delete_training_images})
223
232
  with open(
224
233
  os.path.join(os.path.dirname(os.path.abspath(__file__)), c.CONFIG_FILE), "w"
225
234
  ) as f:
@@ -0,0 +1 @@
1
+ """Graphical user interface components for AccuSleePy."""
@@ -1,5 +1,7 @@
1
- # AccuSleePy main window
2
- # Icon source: Arkinasi, https://www.flaticon.com/authors/arkinasi
1
+ """AccuSleePy main window.
2
+
3
+ Icon source: Arkinasi, https://www.flaticon.com/authors/arkinasi
4
+ """
3
5
 
4
6
  import logging
5
7
  import os
@@ -76,7 +78,6 @@ class TrainingSettings:
76
78
  """Settings for training a new model"""
77
79
 
78
80
  epochs_per_img: int = 9
79
- delete_images: bool = True
80
81
  model_type: str = DEFAULT_MODEL_TYPE
81
82
  calibrate: bool = True
82
83
 
@@ -176,9 +177,6 @@ class AccuSleepWindow(QMainWindow):
176
177
  self.ui.image_number_input.valueChanged.connect(
177
178
  lambda v: setattr(self.training, "epochs_per_img", v)
178
179
  )
179
- self.ui.delete_image_box.stateChanged.connect(
180
- lambda v: setattr(self.training, "delete_images", bool(v))
181
- )
182
180
  self.ui.calibrate_checkbox.stateChanged.connect(
183
181
  self.update_training_calibration
184
182
  )
@@ -304,7 +302,7 @@ class AccuSleepWindow(QMainWindow):
304
302
  emg_filter=self.config.emg_filter,
305
303
  hyperparameters=self.config.hyperparameters,
306
304
  model_filename=model_filename,
307
- delete_images=self.training.delete_images,
305
+ delete_images=self.config.delete_training_images,
308
306
  )
309
307
 
310
308
  # Display results
@@ -1,11 +1,13 @@
1
- # AccuSleePy manual scoring GUI
2
- # Icon sources:
3
- # Arkinasi, https://www.flaticon.com/authors/arkinasi
4
- # kendis lasman, https://www.flaticon.com/packs/ui-79
1
+ """AccuSleePy manual scoring GUI.
5
2
 
3
+ Icon sources:
4
+ Arkinasi, https://www.flaticon.com/authors/arkinasi
5
+ kendis lasman, https://www.flaticon.com/packs/ui-79
6
+ """
6
7
 
7
8
  import copy
8
9
  import os
10
+ import time
9
11
  from dataclasses import dataclass
10
12
  from functools import partial
11
13
  from types import SimpleNamespace
@@ -82,9 +84,10 @@ UNDO_LIMIT = 1000
82
84
  # brightness scaling factors for the spectrogram
83
85
  BRIGHTER_SCALE_FACTOR = 0.96
84
86
  DIMMER_SCALE_FACTOR = 1.07
85
- # zoom factor for upper plots
86
- ZOOM_IN_FACTOR = 0.45
87
- ZOOM_OUT_FACTOR = 1.017
87
+ # zoom factor for upper plots - larger values = bigger changes
88
+ ZOOM_FACTOR = 0.1
89
+ # rate limit for zoom events triggered by scrolling
90
+ MAX_SCROLL_EVENTS_PER_SEC = 24
88
91
 
89
92
 
90
93
  @dataclass
@@ -352,8 +355,10 @@ class ManualScoringWindow(QDialog):
352
355
  )
353
356
  keypress_redo.activated.connect(self.redo)
354
357
 
355
- # user input: clicks
358
+ # user input: mouse events
356
359
  self.ui.upperfigure.canvas.mpl_connect("button_press_event", self.click_to_jump)
360
+ self.ui.upperfigure.canvas.mpl_connect("scroll_event", self.scroll_zoom)
361
+ self.now_zooming = False # impose timeout on zoom events
357
362
 
358
363
  # user input: buttons
359
364
  self.ui.savebutton.clicked.connect(self.save)
@@ -483,7 +488,7 @@ class ManualScoringWindow(QDialog):
483
488
 
484
489
  def closeEvent(self, event: QCloseEvent) -> None:
485
490
  """Check if there are unsaved changes before closing"""
486
- if not all(self.labels == self.last_saved_labels):
491
+ if not np.array_equal(self.labels, self.last_saved_labels):
487
492
  result = QMessageBox.question(
488
493
  self,
489
494
  "Unsaved changes",
@@ -668,6 +673,8 @@ class ManualScoringWindow(QDialog):
668
673
  self.label_display_options,
669
674
  )
670
675
  self.update_figures()
676
+ # upper plot x limits might need to change
677
+ self.zoom_x(direction=None)
671
678
 
672
679
  def update_signal_offset(self, signal: str, direction: str) -> None:
673
680
  """Shift EEG or EMG up or down
@@ -713,39 +720,19 @@ class ManualScoringWindow(QDialog):
713
720
  (self.upper_left_epoch, self.upper_right_epoch + 1)
714
721
  )
715
722
 
716
- def zoom_x(self, direction: str) -> None:
723
+ def zoom_x(self, direction: str | None) -> None:
717
724
  """Change upper figure x-axis zoom level
718
725
 
719
- :param direction: in, out, or reset
726
+ :param direction: in, out, reset, or None
720
727
  """
721
- epochs_shown = self.upper_right_epoch - self.upper_left_epoch + 1
722
- if direction == ZOOM_IN:
723
- self.upper_left_epoch = max(
724
- [
725
- self.upper_left_epoch,
726
- round(self.epoch - ZOOM_IN_FACTOR * epochs_shown),
727
- ]
728
- )
729
-
730
- self.upper_right_epoch = min(
731
- [
732
- self.upper_right_epoch,
733
- round(self.epoch + ZOOM_IN_FACTOR * epochs_shown),
734
- ]
735
- )
736
-
737
- elif direction == ZOOM_OUT:
738
- self.upper_left_epoch = max(
739
- [0, round(self.epoch - ZOOM_OUT_FACTOR * epochs_shown)]
740
- )
741
-
742
- self.upper_right_epoch = min(
743
- [self.n_epochs - 1, round(self.epoch + ZOOM_OUT_FACTOR * epochs_shown)]
744
- )
745
-
746
- else: # reset
747
- self.upper_left_epoch = 0
748
- self.upper_right_epoch = self.n_epochs - 1
728
+ self.upper_left_epoch, self.upper_right_epoch = find_new_x_limits(
729
+ direction=direction,
730
+ left_epoch=self.upper_left_epoch,
731
+ right_epoch=self.upper_right_epoch,
732
+ min_n_shown=self.epochs_to_show,
733
+ total_epochs=self.n_epochs,
734
+ selected_epoch=self.epoch,
735
+ )
749
736
  self.adjust_upper_figure_x_limits()
750
737
  self.ui.upperfigure.canvas.draw()
751
738
 
@@ -806,8 +793,11 @@ class ManualScoringWindow(QDialog):
806
793
  # update upper plot if needed
807
794
  upper_epochs_shown = self.upper_right_epoch - self.upper_left_epoch + 1
808
795
  if (
809
- self.epoch
810
- > self.upper_left_epoch + (1 - SCROLL_BOUNDARY) * upper_epochs_shown
796
+ (
797
+ self.epoch
798
+ > self.upper_left_epoch + (1 - SCROLL_BOUNDARY) * upper_epochs_shown
799
+ or self.epoch + (self.epochs_to_show - 1) / 2 > self.upper_right_epoch
800
+ )
811
801
  and self.upper_right_epoch < (self.n_epochs - 1)
812
802
  and direction == DIRECTION_RIGHT
813
803
  ):
@@ -815,7 +805,11 @@ class ManualScoringWindow(QDialog):
815
805
  self.upper_right_epoch += 1
816
806
  self.adjust_upper_figure_x_limits()
817
807
  elif (
818
- self.epoch < self.upper_left_epoch + SCROLL_BOUNDARY * upper_epochs_shown
808
+ (
809
+ self.epoch
810
+ < self.upper_left_epoch + SCROLL_BOUNDARY * upper_epochs_shown
811
+ or self.epoch - (self.epochs_to_show - 1) / 2 < self.upper_left_epoch
812
+ )
819
813
  and self.upper_left_epoch > 0
820
814
  and direction == DIRECTION_LEFT
821
815
  ):
@@ -984,6 +978,23 @@ class ManualScoringWindow(QDialog):
984
978
 
985
979
  self.update_figures()
986
980
 
981
+ def scroll_zoom(self, event) -> None:
982
+ """Zoom on mouse scroll events"""
983
+ if self.now_zooming:
984
+ return
985
+
986
+ self.now_zooming = True
987
+ start_time = time.time()
988
+ if event.button == "up":
989
+ self.zoom_x(direction=ZOOM_IN)
990
+ else:
991
+ self.zoom_x(direction=ZOOM_OUT)
992
+ end_time = time.time()
993
+ time_elapsed = end_time - start_time
994
+ if time_elapsed < 1 / MAX_SCROLL_EVENTS_PER_SEC:
995
+ time.sleep(1 / MAX_SCROLL_EVENTS_PER_SEC - time_elapsed)
996
+ self.now_zooming = False
997
+
987
998
 
988
999
  def convert_labels(labels: np.array, style: str) -> np.array:
989
1000
  """Convert labels between "display" and "digit" formats
@@ -1095,3 +1106,55 @@ def transform_eeg_emg(eeg: np.array, emg: np.array) -> (np.array, np.array):
1095
1106
  eeg = eeg / np.percentile(eeg, 95) / 2.2
1096
1107
  emg = emg / np.percentile(emg, 95) / 2.2
1097
1108
  return eeg, emg
1109
+
1110
+
1111
+ def find_new_x_limits(
1112
+ direction: str | None,
1113
+ left_epoch: int,
1114
+ right_epoch: int,
1115
+ total_epochs: int,
1116
+ min_n_shown: int,
1117
+ selected_epoch: int,
1118
+ ) -> (int, int):
1119
+ """Calculate new plot x limits to allow zooming
1120
+
1121
+ :param direction: in, out, reset, or None
1122
+ :param left_epoch: index of current leftmost epoch
1123
+ :param right_epoch: index of current rightmost epoch
1124
+ :param total_epochs: total number of epochs in the recording
1125
+ :param min_n_shown: minimum number of epochs to display
1126
+ :param selected_epoch: currently selected epoch
1127
+ """
1128
+ # number of epochs currently displayed in the upper plots
1129
+ current_n_shown = right_epoch - left_epoch + 1
1130
+ if direction == ZOOM_IN:
1131
+ # can't display fewer than the number of epochs in the lower plot
1132
+ new_n_shown = max([min_n_shown, round(current_n_shown * (1 - ZOOM_FACTOR))])
1133
+ elif direction == ZOOM_OUT:
1134
+ # can't display more than the total number of epochs
1135
+ new_n_shown = min([total_epochs, round(current_n_shown / (1 - ZOOM_FACTOR))])
1136
+ elif direction == ZOOM_RESET:
1137
+ left_epoch = 0
1138
+ right_epoch = total_epochs - 1
1139
+ return left_epoch, right_epoch
1140
+ else: # just recalculating if min_n_shown has changed
1141
+ new_n_shown = int(
1142
+ np.clip(current_n_shown, a_min=min_n_shown, a_max=total_epochs)
1143
+ )
1144
+
1145
+ # count epochs to show on either side of the selected epoch
1146
+ epochs_on_left_side = int(np.ceil((new_n_shown - 1) / 2))
1147
+ epochs_on_right_side = new_n_shown - epochs_on_left_side - 1
1148
+ if selected_epoch - epochs_on_left_side < 0:
1149
+ # can't go further left than 0
1150
+ left_epoch = 0
1151
+ right_epoch = new_n_shown - 1
1152
+ elif selected_epoch + epochs_on_right_side >= total_epochs:
1153
+ # can't go further right than the total number of epochs
1154
+ left_epoch = total_epochs - new_n_shown
1155
+ right_epoch = total_epochs - 1
1156
+ else:
1157
+ left_epoch = selected_epoch - epochs_on_left_side
1158
+ right_epoch = selected_epoch + epochs_on_right_side
1159
+
1160
+ return left_epoch, right_epoch
@@ -1,4 +1,5 @@
1
- # Widget with a matplotlib FigureCanvas for manual scoring
1
+ """Matplotlib FigureCanvas widget for manual scoring."""
2
+
2
3
  from collections.abc import Callable
3
4
 
4
5
  import matplotlib.ticker as mticker