misleep 0.2.8__tar.gz → 0.2.9__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 (74) hide show
  1. {misleep-0.2.8 → misleep-0.2.9}/PKG-INFO +1 -1
  2. {misleep-0.2.8 → misleep-0.2.9}/misleep/__init__.py +1 -0
  3. {misleep-0.2.8 → misleep-0.2.9}/misleep/config.ini +3 -3
  4. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/dialog.py +63 -0
  5. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/main_window.py +102 -27
  6. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/thread.py +15 -1
  7. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/main_window_ui.py +13 -5
  8. misleep-0.2.9/misleep/gui/uis/save_data_dialog_ui.py +67 -0
  9. {misleep-0.2.8 → misleep-0.2.9}/misleep/io/signal_io.py +94 -1
  10. {misleep-0.2.8 → misleep-0.2.9}/misleep/utils/signals.py +5 -2
  11. {misleep-0.2.8 → misleep-0.2.9}/misleep.egg-info/PKG-INFO +1 -1
  12. {misleep-0.2.8 → misleep-0.2.9}/setup.py +1 -1
  13. misleep-0.2.9/test/__pycache__/test_annotation_io.cpython-38-pytest-8.3.5.pyc +0 -0
  14. misleep-0.2.9/test/test_signal_io.py +77 -0
  15. misleep-0.2.8/misleep/gui/uis/save_data_dialog_ui.py +0 -48
  16. misleep-0.2.8/test/test_signal_io.py +0 -39
  17. {misleep-0.2.8 → misleep-0.2.9}/LICENSE +0 -0
  18. {misleep-0.2.8 → misleep-0.2.9}/README.md +0 -0
  19. {misleep-0.2.8 → misleep-0.2.9}/misleep/__main__.py +0 -0
  20. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/__init__.py +0 -0
  21. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage.py +0 -0
  22. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/P30_EEG_F_lightgbm.pkl +0 -0
  23. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/P30_EEG_P_lightgbm.pkl +0 -0
  24. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/ado_EEG_F_lightgbm.pkl +0 -0
  25. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/ado_EEG_P_lightgbm.pkl +0 -0
  26. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/adult_EEG_F_lightgbm.pkl +0 -0
  27. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/auto_stage_model/adult_EEG_P_lightgbm.pkl +0 -0
  28. {misleep-0.2.8 → misleep-0.2.9}/misleep/analysis/detection.py +0 -0
  29. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/__init__.py +0 -0
  30. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/about.py +0 -0
  31. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/resources/__init__.py +0 -0
  32. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/resources/entire_logo.png +0 -0
  33. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/resources/logo.png +0 -0
  34. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/resources/misleep.py +0 -0
  35. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/resources/misleep.qrc +0 -0
  36. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/show.py +0 -0
  37. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/spec_window.py +0 -0
  38. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/SWA_detect_dialog_ui.py +0 -0
  39. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/__init__.py +0 -0
  40. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/about_ui.py +0 -0
  41. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/auto_stage_dialog_ui.py +0 -0
  42. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/horizontal_line_dialog_ui.py +0 -0
  43. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/label_dialog_ui.py +0 -0
  44. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/spec_window_ui.py +0 -0
  45. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/spindle_detect_dialog_ui.py +0 -0
  46. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/state_spectral_dialog_ui.py +0 -0
  47. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/uis/transfer_result_dialog_ui.py +0 -0
  48. {misleep-0.2.8 → misleep-0.2.9}/misleep/gui/utils.py +0 -0
  49. {misleep-0.2.8 → misleep-0.2.9}/misleep/io/__init__.py +0 -0
  50. {misleep-0.2.8 → misleep-0.2.9}/misleep/io/annotation_io.py +0 -0
  51. {misleep-0.2.8 → misleep-0.2.9}/misleep/io/base.py +0 -0
  52. {misleep-0.2.8 → misleep-0.2.9}/misleep/preprocessing/__init__.py +0 -0
  53. {misleep-0.2.8 → misleep-0.2.9}/misleep/preprocessing/channel.py +0 -0
  54. {misleep-0.2.8 → misleep-0.2.9}/misleep/preprocessing/signals.py +0 -0
  55. {misleep-0.2.8 → misleep-0.2.9}/misleep/preprocessing/spectral.py +0 -0
  56. {misleep-0.2.8 → misleep-0.2.9}/misleep/utils/__init__.py +0 -0
  57. {misleep-0.2.8 → misleep-0.2.9}/misleep/utils/annotation.py +0 -0
  58. {misleep-0.2.8 → misleep-0.2.9}/misleep/utils/logger_handler.py +0 -0
  59. {misleep-0.2.8 → misleep-0.2.9}/misleep/utils/self_antropy.py +0 -0
  60. {misleep-0.2.8 → misleep-0.2.9}/misleep/viz/__init__.py +0 -0
  61. {misleep-0.2.8 → misleep-0.2.9}/misleep/viz/hypnogram.py +0 -0
  62. {misleep-0.2.8 → misleep-0.2.9}/misleep/viz/signals.py +0 -0
  63. {misleep-0.2.8 → misleep-0.2.9}/misleep/viz/spectral.py +0 -0
  64. {misleep-0.2.8 → misleep-0.2.9}/misleep.egg-info/SOURCES.txt +0 -0
  65. {misleep-0.2.8 → misleep-0.2.9}/misleep.egg-info/dependency_links.txt +0 -0
  66. {misleep-0.2.8 → misleep-0.2.9}/misleep.egg-info/requires.txt +0 -0
  67. {misleep-0.2.8 → misleep-0.2.9}/misleep.egg-info/top_level.txt +0 -0
  68. {misleep-0.2.8 → misleep-0.2.9}/setup.cfg +0 -0
  69. {misleep-0.2.8 → misleep-0.2.9}/test/test_annotation_io.py +0 -0
  70. {misleep-0.2.8 → misleep-0.2.9}/test/test_loadmat73.py +0 -0
  71. {misleep-0.2.8 → misleep-0.2.9}/test/test_midata.py +0 -0
  72. {misleep-0.2.8 → misleep-0.2.9}/test/test_show.py +0 -0
  73. {misleep-0.2.8 → misleep-0.2.9}/test/test_signals_viz.py +0 -0
  74. {misleep-0.2.8 → misleep-0.2.9}/test/test_spectral_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: misleep
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: MiSleep: Mice Sleep EEG/EMG visualization, scoring and analysis.
5
5
  Home-page: https://github.com/BryanWang0702/MiSleep/
6
6
  Download-URL: https://github.com/BryanWang0702/MiSleep/
@@ -5,6 +5,7 @@ from .preprocessing import *
5
5
  from .utils import *
6
6
  from .viz import *
7
7
  from .gui import *
8
+ from .analysis import *
8
9
 
9
10
  __author__ = "Xueqiang Wang <swang9194@gmail.com>"
10
11
  __version__ = "0.0.1"
@@ -1,7 +1,7 @@
1
1
  [gui]
2
- version = v0.2.8
3
- updatetime = 2025/4/1
4
- marker = ['good', 'first REM', 'WindEEG', 'W-R ', 'maker']
2
+ version = v0.2.9
3
+ updatetime = 2025/4/23
4
+ marker = ['half_signal', 'first REM', 'WindEEG', 'W-R ', 'maker']
5
5
  startend = ['burst-supression', 'REM', 'Wake', 'Spindle', 'SWA', 'start end label', 'start end label']
6
6
  statemap = {"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT", "5": "IS", "6": "MicroArousal"}
7
7
  statecolor = {"1": "orange", "2": "skyblue", "3": "red", "4": "white", "5": "green", "6": "pink"}
@@ -20,6 +20,7 @@ from misleep.gui.uis.horizontal_line_dialog_ui import Ui_horizontal_line_dialog
20
20
  from misleep.gui.uis.SWA_detect_dialog_ui import Ui_SWADetectDialog
21
21
  from misleep.gui.uis.spindle_detect_dialog_ui import Ui_SpindleDetectDialog
22
22
  from misleep.gui.uis.auto_stage_dialog_ui import Ui_AutoStageDialog
23
+ from misleep.gui.uis.save_data_dialog_ui import Ui_SaveDataDialog
23
24
  from misleep.gui.thread import SaveThread
24
25
  from misleep.io.annotation_io import transfer_result
25
26
  from misleep.utils.signals import signal_filter
@@ -981,6 +982,68 @@ class AutoStageDialog(QDialog, Ui_AutoStageDialog):
981
982
  event.ignore()
982
983
  self.closed = True
983
984
  self.hide()
985
+
986
+
987
+ class SaveData_dialog(QDialog, Ui_SaveDataDialog):
988
+ def __init__(self, parent=None):
989
+ """
990
+ Initialize the save data dialog of MiSleep
991
+ """
992
+ super().__init__(parent)
993
+
994
+ self.setupUi(self)
995
+
996
+ self.CropStartTimeEditor.setDisabled(True)
997
+ self.CropEndTimeEditor.setDisabled(True)
998
+
999
+ self.CropStartTimeEditor.setDisabled(True)
1000
+ self.CropEndTimeEditor.setDisabled(True)
1001
+ self.CropDataStartCheckBox.clicked.connect(self.crop_start_time_editor_changed)
1002
+ self.CropDataEndCheckBox.clicked.connect(self.crop_end_time_editor_changed)
1003
+
1004
+ self.channel_slm = QStringListModel()
1005
+
1006
+ self.OKBtn.clicked.connect(self.okEvent)
1007
+ self.CancelBtn.clicked.connect(self.cancelEvent)
1008
+ self.closed = True
1009
+
1010
+ def crop_start_time_editor_changed(self):
1011
+ if self.CropDataStartCheckBox.isChecked():
1012
+ self.CropStartTimeEditor.setEnabled(True)
1013
+ if not self.CropDataStartCheckBox.isChecked():
1014
+ self.CropStartTimeEditor.setDisabled(True)
1015
+
1016
+ def crop_end_time_editor_changed(self):
1017
+ if self.CropDataEndCheckBox.isChecked():
1018
+ self.CropEndTimeEditor.setEnabled(True)
1019
+ if not self.CropDataEndCheckBox.isChecked():
1020
+ self.CropEndTimeEditor.setDisabled(True)
1021
+
1022
+ def fill_midata_params(self, midata):
1023
+ """
1024
+ Fill the midata params to the dialog, including time, channels, sf, etc.
1025
+ """
1026
+ self.channel_slm.setStringList(midata.channels)
1027
+ self.ChannelListView.setModel(self.channel_slm)
1028
+
1029
+ self.CropStartTimeEditor.setDateTime(datetime.datetime.strptime(midata.time, "%Y%m%d-%H:%M:%S"))
1030
+ # End time should add the midata.duration to format the time
1031
+ end_time = datetime.datetime.strptime(midata.time, "%Y%m%d-%H:%M:%S") + datetime.timedelta(seconds=midata.duration)
1032
+ self.CropEndTimeEditor.setDateTime(end_time)
1033
+
1034
+ def okEvent(self):
1035
+ self.closed = False
1036
+ self.hide()
1037
+
1038
+ def cancelEvent(self):
1039
+ """Triggered by the `cancel` button"""
1040
+ self.closed = True
1041
+ self.hide()
1042
+
1043
+ def closeEvent(self, event):
1044
+ event.ignore()
1045
+ self.closed = True
1046
+ self.hide()
984
1047
 
985
1048
 
986
1049
 
@@ -28,7 +28,7 @@ from misleep.gui.utils import create_new_mianno, identify_startend_color
28
28
  from misleep.utils.annotation import lst2group
29
29
  from misleep.gui.about import about_dialog
30
30
  from misleep.gui.dialog import label_dialog, transferResult_dialog, stateSpectral_dialog, \
31
- horizontalLine_dialog, SWADetectionDialog, SpindleDetectionDialog, AutoStageDialog
31
+ horizontalLine_dialog, SWADetectionDialog, SpindleDetectionDialog, AutoStageDialog, SaveData_dialog
32
32
  from misleep.gui.spec_window import SpecWindow
33
33
  from misleep.gui.uis.main_window_ui import Ui_MiSleep
34
34
  from misleep.preprocessing.spectral import spectrogram, spectrum, band_power
@@ -129,6 +129,9 @@ class main_window(QMainWindow, Ui_MiSleep):
129
129
  # Initial state spectral dialog
130
130
  self.state_spectral_dialog = stateSpectral_dialog()
131
131
 
132
+ # Initial data saving dialog
133
+ self.save_data_dialog = SaveData_dialog()
134
+
132
135
  # Initial horizontal line dialog
133
136
  self.horizontal_line_dialog = horizontalLine_dialog()
134
137
  # The horizontal_line dict contains the line value, line color, line comment
@@ -166,6 +169,7 @@ class main_window(QMainWindow, Ui_MiSleep):
166
169
  self.actionSWA_detection.triggered.connect(self.swa_detection)
167
170
  self.actionSpindle_Detection.triggered.connect(self.spindle_detection)
168
171
  self.actionAuto_Stage.triggered.connect(self.auto_stage)
172
+ self.actionSaveData.triggered.connect(self.save_data)
169
173
 
170
174
  # Spectrogram percentile change
171
175
  self.PercentileSpin.setValue(self.spectrogram_percentile)
@@ -384,12 +388,17 @@ class main_window(QMainWindow, Ui_MiSleep):
384
388
 
385
389
  def load_anno(self):
386
390
  """Triggered by actionLoad_Annotation"""
387
- anno_path, _ = QFileDialog.getOpenFileName(
391
+ anno_path, _ = QFileDialog.getSaveFileName(
388
392
  self, "Select annotation file",
389
393
  f"{self.config['gui']['openpath']}",
390
394
  "txt Files (*.txt *.TXT)"
391
395
  )
392
396
 
397
+ if not os.path.exists(anno_path):
398
+ # Create the file if it doesn't exist
399
+ with open(anno_path, 'w') as file:
400
+ file.write("")
401
+
393
402
  if anno_path == "":
394
403
  return
395
404
  self.anno_path = anno_path
@@ -1009,10 +1018,11 @@ class main_window(QMainWindow, Ui_MiSleep):
1009
1018
  # Clear start end label
1010
1019
  self.start_end = []
1011
1020
  self.start_end.append(sec)
1021
+ elif sec < self.start_end[0]:
1022
+ # Clear start end label and add the second as start
1023
+ self.start_end = []
1024
+ self.start_end.append(sec)
1012
1025
  else:
1013
- if sec < self.start_end[0]:
1014
- QMessageBox.about(self, "Error", "End should be larger than Start!")
1015
- return
1016
1026
  self.start_end.append(sec + 1)
1017
1027
 
1018
1028
  self.plot_start_end_line()
@@ -1365,28 +1375,36 @@ class main_window(QMainWindow, Ui_MiSleep):
1365
1375
  int(end_*self.midata.sf[channel])
1366
1376
  ]
1367
1377
 
1368
- freq, psd = spectrum(signal=signal_data,
1369
- sf=self.midata.sf[channel],
1370
- relative=True)
1371
-
1372
- f, t, Sxx = spectrogram(signal=signal_data,
1378
+ try:
1379
+ freq, psd = spectrum(signal=signal_data,
1373
1380
  sf=self.midata.sf[channel],
1374
- step=1, window=5, norm=True)
1381
+ relative=True)
1382
+
1383
+ f, t, Sxx = spectrogram(signal=signal_data,
1384
+ sf=self.midata.sf[channel],
1385
+ step=1, window=5, norm=True)
1375
1386
 
1376
- bandPower = band_power(psd=psd, freq=freq,
1377
- bands=[[0.5, 4, 'delta'], [4, 9, 'theta']])
1378
-
1379
- ratio = bandPower['delta'] / bandPower['theta']
1380
-
1381
- self.spec_window.show_(spectrum=[psd, freq],
1382
- spectrogram=[f, t, Sxx],
1383
- percentile_=self.spectrogram_percentile,
1384
- ratio=ratio, start_end=[start_, end_])
1385
-
1386
- self.spec_window.activateWindow()
1387
- self.spec_window.setWindowState(
1388
- self.spec_window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
1389
- self.spec_window.showNormal()
1387
+ bandPower = band_power(psd=psd, freq=freq,
1388
+ bands=[[0.5, 4, 'delta'], [4, 9, 'theta']])
1389
+
1390
+ ratio = bandPower['delta'] / bandPower['theta']
1391
+
1392
+ self.spec_window.show_(spectrum=[psd, freq],
1393
+ spectrogram=[f, t, Sxx],
1394
+ percentile_=self.spectrogram_percentile,
1395
+ ratio=ratio, start_end=[start_, end_])
1396
+
1397
+ self.spec_window.activateWindow()
1398
+ self.spec_window.setWindowState(
1399
+ self.spec_window.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
1400
+ self.spec_window.showNormal()
1401
+ except Exception as e:
1402
+ QMessageBox.about(
1403
+ self,
1404
+ "Error",
1405
+ "Filter error.",
1406
+ )
1407
+ return
1390
1408
 
1391
1409
  def set_show_duration(self, type_="Combo"):
1392
1410
  """Set show_duration with ShowRangeCombo or SecondNumSpin"""
@@ -1646,12 +1664,17 @@ class main_window(QMainWindow, Ui_MiSleep):
1646
1664
  For when I only want to save a file but not cover the current labels
1647
1665
  """
1648
1666
  if self.anno_path == "":
1649
- anno_path, _ = QFileDialog.getOpenFileName(
1667
+ anno_path, _ = QFileDialog.getSaveFileName(
1650
1668
  self, "Select annotation file",
1651
1669
  f"{self.config['gui']['openpath']}",
1652
1670
  "txt Files (*.txt *.TXT)"
1653
1671
  )
1654
1672
 
1673
+ if not os.path.exists(anno_path):
1674
+ # Create the file if it doesn't exist
1675
+ with open(anno_path, 'w') as file:
1676
+ file.write("")
1677
+
1655
1678
  if anno_path == "":
1656
1679
  return
1657
1680
  if not just_save:
@@ -1667,7 +1690,59 @@ class main_window(QMainWindow, Ui_MiSleep):
1667
1690
  save_thread.quit()
1668
1691
 
1669
1692
  def save_data(self):
1670
- """Save data into file"""
1693
+ """Save data into file"""
1694
+
1695
+ # Pop up dialog to get save data options
1696
+ self.save_data_dialog.fill_midata_params(midata=self.midata)
1697
+ self.save_data_dialog.exec()
1698
+ if self.save_data_dialog.closed:
1699
+ return
1700
+
1701
+ # Get the selected channels to save data
1702
+ selected_channels = [each.row() for each in self.save_data_dialog.ChannelListView.selectedIndexes()]
1703
+ # If did not select any channel, save all channels
1704
+ if selected_channels == []:
1705
+ selected_channels = list(range(self.midata.n_channels))
1706
+ midata_to_save = self.midata.pick_chs([self.midata.channels[each] for each in selected_channels])
1707
+
1708
+ # Crop data with the checkbox and time editor
1709
+ start_sec = 0
1710
+ end_sec = self.midata.duration
1711
+
1712
+ if self.save_data_dialog.CropDataStartCheckBox.isChecked():
1713
+ start_time = self.save_data_dialog.CropStartTimeEditor.dateTime().toPyDateTime()
1714
+ midata_to_save._time = start_time.strftime("%Y%m%d-%H:%M:%S")
1715
+ start_sec = int(datetime.timedelta.total_seconds(start_time - self.ac_time))
1716
+ if start_sec < 0:
1717
+ start_sec = 0
1718
+
1719
+ if self.save_data_dialog.CropDataEndCheckBox.isChecked():
1720
+ end_time = self.save_data_dialog.CropEndTimeEditor.dateTime().toPyDateTime()
1721
+ end_sec = int(datetime.timedelta.total_seconds(end_time - self.ac_time))
1722
+ if end_sec > self.midata.duration:
1723
+ end_sec = self.midata.duration
1724
+
1725
+ if end_sec <= start_sec:
1726
+ start_sec = 0
1727
+ end_sec = self.midata.duration
1728
+
1729
+ midata_to_save = midata_to_save.crop([start_sec, end_sec])
1730
+
1731
+ data_path, _ = QFileDialog.getSaveFileName(
1732
+ self, "Select a file to save data",
1733
+ f"{self.config['gui']['openpath']}_misleep_saved",
1734
+ "*mat Files (*.mat *.MAT);;*edf Files (*.edf *.EDF);"
1735
+ )
1736
+ if data_path == "":
1737
+ return
1738
+
1739
+ save_thread = SaveThread(file=midata_to_save, file_path=data_path)
1740
+ saved = save_thread.save_data()
1741
+ if saved:
1742
+ QMessageBox.about(self, "Info", f"Data Saved to {data_path}")
1743
+ else:
1744
+ QMessageBox.about(self, "Error", "Data save ERROR")
1745
+ save_thread.quit()
1671
1746
 
1672
1747
  def auto_save(self):
1673
1748
  """Auto save every 5 mins"""
@@ -1,7 +1,7 @@
1
1
  from PyQt5.QtCore import QObject, QThread
2
2
  from misleep.gui.utils import second2time
3
3
  from misleep.utils.annotation import lst2group
4
- from misleep.io.signal_io import load_mat
4
+ from misleep.io.signal_io import load_mat, write_edf, write_mat
5
5
  import datetime
6
6
 
7
7
  class SaveThread(QThread):
@@ -69,6 +69,20 @@ class SaveThread(QThread):
69
69
  with open(self.file_path, 'w') as f:
70
70
  f.write('\n'.join(annos))
71
71
  return True
72
+
73
+ def save_data(self):
74
+ """Data save function"""
75
+ midata= self.file
76
+ if midata is None:
77
+ return False
78
+
79
+ if self.file_path.endswith('.mat'):
80
+ write_mat(midata.signals, midata.channels, midata.sf, midata.time, self.file_path)
81
+ elif self.file_path.endswith('.edf'):
82
+ write_edf(midata.signals, midata.channels, midata.sf, midata.time, self.file_path)
83
+ else:
84
+ raise ValueError("File type not supported!")
85
+ return True
72
86
 
73
87
  class LoadThread(QThread):
74
88
 
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
 
3
- # Form implementation generated from reading ui file '.\main_window.ui'
3
+ # Form implementation generated from reading ui file 'misleep/gui/uis/main_window.ui'
4
4
  #
5
5
  # Created by: PyQt5 UI code generator 5.15.9
6
6
  #
@@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
14
14
  class Ui_MiSleep(object):
15
15
  def setupUi(self, MiSleep):
16
16
  MiSleep.setObjectName("MiSleep")
17
- MiSleep.resize(1445, 971)
17
+ MiSleep.resize(1451, 967)
18
18
  MiSleep.setFocusPolicy(QtCore.Qt.WheelFocus)
19
19
  icon = QtGui.QIcon()
20
20
  icon.addPixmap(QtGui.QPixmap(":/logo/logo.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
@@ -35,7 +35,7 @@ class Ui_MiSleep(object):
35
35
  self.HypnoArea.setWidgetResizable(True)
36
36
  self.HypnoArea.setObjectName("HypnoArea")
37
37
  self.scrollAreaWidgetContents_2 = QtWidgets.QWidget()
38
- self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 1063, 128))
38
+ self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 1069, 128))
39
39
  self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2")
40
40
  self.HypnoArea.setWidget(self.scrollAreaWidgetContents_2)
41
41
  self.gridLayout_3.addWidget(self.HypnoArea, 2, 0, 1, 1)
@@ -44,7 +44,7 @@ class Ui_MiSleep(object):
44
44
  self.SignalArea.setWidgetResizable(True)
45
45
  self.SignalArea.setObjectName("SignalArea")
46
46
  self.scrollAreaWidgetContents = QtWidgets.QWidget()
47
- self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1063, 756))
47
+ self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 1069, 764))
48
48
  self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
49
49
  self.SignalArea.setWidget(self.scrollAreaWidgetContents)
50
50
  self.gridLayout_3.addWidget(self.SignalArea, 0, 0, 1, 1)
@@ -291,7 +291,7 @@ class Ui_MiSleep(object):
291
291
  self.TimeDock.setWidget(self.dockWidgetContents_7)
292
292
  MiSleep.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.TimeDock)
293
293
  self.menuBar = QtWidgets.QMenuBar(MiSleep)
294
- self.menuBar.setGeometry(QtCore.QRect(0, 0, 1445, 26))
294
+ self.menuBar.setGeometry(QtCore.QRect(0, 0, 1451, 22))
295
295
  self.menuBar.setObjectName("menuBar")
296
296
  self.menuFile = QtWidgets.QMenu(self.menuBar)
297
297
  self.menuFile.setObjectName("menuFile")
@@ -322,8 +322,14 @@ class Ui_MiSleep(object):
322
322
  self.actionSpindle_Detection.setObjectName("actionSpindle_Detection")
323
323
  self.actionAuto_Stage = QtWidgets.QAction(MiSleep)
324
324
  self.actionAuto_Stage.setObjectName("actionAuto_Stage")
325
+ self.actionLoad_AccuSleep_Data = QtWidgets.QAction(MiSleep)
326
+ self.actionLoad_AccuSleep_Data.setObjectName("actionLoad_AccuSleep_Data")
327
+ self.actionSaveData = QtWidgets.QAction(MiSleep)
328
+ self.actionSaveData.setObjectName("actionSaveData")
325
329
  self.menuFile.addAction(self.actionLoadData)
326
330
  self.menuFile.addAction(self.actionLoadAnnotation)
331
+ self.menuFile.addSeparator()
332
+ self.menuFile.addAction(self.actionSaveData)
327
333
  self.menuEvent_Detection.addAction(self.actionSWA_detection)
328
334
  self.menuEvent_Detection.addAction(self.actionSpindle_Detection)
329
335
  self.menuTools.addAction(self.actionAddLine)
@@ -399,4 +405,6 @@ class Ui_MiSleep(object):
399
405
  self.actionSWA_detection.setText(_translate("MiSleep", "SWA Detection"))
400
406
  self.actionSpindle_Detection.setText(_translate("MiSleep", "Spindle Detection"))
401
407
  self.actionAuto_Stage.setText(_translate("MiSleep", "Auto Stage"))
408
+ self.actionLoad_AccuSleep_Data.setText(_translate("MiSleep", "Load AccuSleep Data"))
409
+ self.actionSaveData.setText(_translate("MiSleep", "Save Data"))
402
410
  from misleep.gui.resources import misleep
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Form implementation generated from reading ui file 'misleep/gui/uis/save_data_dialog.ui'
4
+ #
5
+ # Created by: PyQt5 UI code generator 5.15.9
6
+ #
7
+ # WARNING: Any manual changes made to this file will be lost when pyuic5 is
8
+ # run again. Do not edit this file unless you know what you are doing.
9
+
10
+
11
+ from PyQt5 import QtCore, QtGui, QtWidgets
12
+
13
+
14
+ class Ui_SaveDataDialog(object):
15
+ def setupUi(self, SaveDataDialog):
16
+ SaveDataDialog.setObjectName("SaveDataDialog")
17
+ SaveDataDialog.resize(409, 516)
18
+ self.gridLayout_2 = QtWidgets.QGridLayout(SaveDataDialog)
19
+ self.gridLayout_2.setObjectName("gridLayout_2")
20
+ self.OKBtn = QtWidgets.QPushButton(SaveDataDialog)
21
+ self.OKBtn.setObjectName("OKBtn")
22
+ self.gridLayout_2.addWidget(self.OKBtn, 1, 0, 1, 1)
23
+ self.CancelBtn = QtWidgets.QPushButton(SaveDataDialog)
24
+ self.CancelBtn.setObjectName("CancelBtn")
25
+ self.gridLayout_2.addWidget(self.CancelBtn, 1, 1, 1, 1)
26
+ self.ACTimeEdit = QtWidgets.QGroupBox(SaveDataDialog)
27
+ self.ACTimeEdit.setObjectName("ACTimeEdit")
28
+ self.gridLayout = QtWidgets.QGridLayout(self.ACTimeEdit)
29
+ self.gridLayout.setObjectName("gridLayout")
30
+ self.CropDataStartCheckBox = QtWidgets.QCheckBox(self.ACTimeEdit)
31
+ self.CropDataStartCheckBox.setObjectName("CropDataStartCheckBox")
32
+ self.gridLayout.addWidget(self.CropDataStartCheckBox, 2, 0, 1, 1)
33
+ self.CropStartTimeEditor = QtWidgets.QDateTimeEdit(self.ACTimeEdit)
34
+ self.CropStartTimeEditor.setObjectName("CropStartTimeEditor")
35
+ self.gridLayout.addWidget(self.CropStartTimeEditor, 3, 0, 1, 1)
36
+ self.ChannelListView = QtWidgets.QListView(self.ACTimeEdit)
37
+ self.ChannelListView.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
38
+ self.ChannelListView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
39
+ self.ChannelListView.setTabKeyNavigation(True)
40
+ self.ChannelListView.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
41
+ self.ChannelListView.setObjectName("ChannelListView")
42
+ self.gridLayout.addWidget(self.ChannelListView, 1, 0, 1, 2)
43
+ self.label_3 = QtWidgets.QLabel(self.ACTimeEdit)
44
+ self.label_3.setObjectName("label_3")
45
+ self.gridLayout.addWidget(self.label_3, 0, 0, 1, 1)
46
+ self.CropDataEndCheckBox = QtWidgets.QCheckBox(self.ACTimeEdit)
47
+ self.CropDataEndCheckBox.setObjectName("CropDataEndCheckBox")
48
+ self.gridLayout.addWidget(self.CropDataEndCheckBox, 4, 0, 1, 1)
49
+ self.CropEndTimeEditor = QtWidgets.QDateTimeEdit(self.ACTimeEdit)
50
+ self.CropEndTimeEditor.setObjectName("CropEndTimeEditor")
51
+ self.gridLayout.addWidget(self.CropEndTimeEditor, 5, 0, 1, 1)
52
+ self.gridLayout_2.addWidget(self.ACTimeEdit, 0, 0, 1, 2)
53
+
54
+ self.retranslateUi(SaveDataDialog)
55
+ QtCore.QMetaObject.connectSlotsByName(SaveDataDialog)
56
+
57
+ def retranslateUi(self, SaveDataDialog):
58
+ _translate = QtCore.QCoreApplication.translate
59
+ SaveDataDialog.setWindowTitle(_translate("SaveDataDialog", "Dialog"))
60
+ self.OKBtn.setText(_translate("SaveDataDialog", "OK"))
61
+ self.CancelBtn.setText(_translate("SaveDataDialog", "Cancel"))
62
+ self.ACTimeEdit.setTitle(_translate("SaveDataDialog", "Save data options"))
63
+ self.CropDataStartCheckBox.setText(_translate("SaveDataDialog", "Crop data - Set Start Time"))
64
+ self.CropStartTimeEditor.setDisplayFormat(_translate("SaveDataDialog", "yyyy/MM/dd HH:mm:ss"))
65
+ self.label_3.setText(_translate("SaveDataDialog", "Select Channels to Save"))
66
+ self.CropDataEndCheckBox.setText(_translate("SaveDataDialog", "Crop data - Set End Time"))
67
+ self.CropEndTimeEditor.setDisplayFormat(_translate("SaveDataDialog", "yyyy/MM/dd HH:mm:ss"))
@@ -61,7 +61,7 @@ def load_mat(data_path):
61
61
  raw_data = raw_data[0][0]
62
62
  # Whether saved by python
63
63
  if 'save' in names:
64
- channels = list(raw_data['channels'])
64
+ channels = list(names[:-4])
65
65
  sf = [float(each) for each in raw_data['sf'][0]]
66
66
  signals = [raw_data[each][0] for each in channels]
67
67
  time = raw_data['time'][0]
@@ -191,3 +191,96 @@ def load_edf(data_path):
191
191
  channels=[each['label'] for each in signal_headers],
192
192
  sf=[each['sample_frequency'] for each in signal_headers],
193
193
  time=meta['startdate'].strftime('%Y%m%d-%H:%M:%S'))
194
+
195
+
196
+ def write_edf(signals, channels, sf, time, edf_file=None):
197
+ """
198
+ Write data to an EDF (European Data Format) file.
199
+
200
+ This function saves the provided signal data, channel names, sampling frequencies,
201
+ and start time into an EDF file at the specified file path.
202
+
203
+ Parameters
204
+ ----------
205
+ signals : list
206
+ A list containing the signal data for each channel. Each element
207
+ corresponds to the signal data for one channel.
208
+ channels : list of str
209
+ A list of channel names corresponding to the signal data.
210
+ sf : list of float
211
+ A list of sampling frequencies for each channel.
212
+ time : str
213
+ The start time of the recording in the format "YYYYMMDD-HH:MM:SS".
214
+ edf_file : str
215
+ The file path where the EDF file will be saved.
216
+
217
+ Returns
218
+ -------
219
+ None
220
+ The function writes the data to the specified file and does not return any value.
221
+
222
+ Notes
223
+ -----
224
+ - The function assumes that the length of `signals`, `channels`, and `sf` are the same.
225
+ - The `pyedflib` library is used to handle the EDF file writing process.
226
+ - Ensure that the `edf_file` is a valid path where the file can be created.
227
+
228
+ Example
229
+ -------
230
+ >>> signals = [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]]
231
+ >>> channels = ["EEG1", "EEG2"]
232
+ >>> sf = [256.0, 256.0]
233
+ >>> time = "20250423-12:00:00"
234
+ >>> edf_file = "output.edf"
235
+ >>> write_edf(signals, channels, sf, time, edf_file)
236
+ """
237
+ import pyedflib
238
+
239
+
240
+ if edf_file is None:
241
+ edf_file = f"./{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}_saved.edf"
242
+
243
+ # Create a dictionary for the EDF header
244
+ header = {'technician': '',
245
+ 'recording_additional': '',
246
+ 'patientname': 'MiSleep',
247
+ 'patient_additional': '',
248
+ 'patientcode': '',
249
+ 'equipment': '',
250
+ 'admincode': '',
251
+ 'sex': '',
252
+ 'startdate': datetime.datetime.strptime(time, "%Y%m%d-%H:%M:%S"),
253
+ 'birthdate': '',
254
+ 'gender': '',
255
+ 'annotations': []
256
+ }
257
+
258
+ signal_headers = [
259
+ {'label': each,
260
+ 'dimension': 'uV',
261
+ 'sample_rate': sf[idx],
262
+ 'sample_frequency': sf[idx],
263
+ 'physical_max': 10417.0,
264
+ 'physical_min': -10417.0,
265
+ 'digital_max': 32767,
266
+ 'digital_min': -32768,
267
+ 'prefilter': '',
268
+ 'transducer': ''
269
+ } for idx, each in enumerate(channels)
270
+ ]
271
+
272
+ try:
273
+
274
+ with pyedflib.EdfWriter(edf_file, len(signals)) as edf_writer:
275
+ # Set the header information
276
+ edf_writer.setHeader(header)
277
+
278
+ # Add each signal to the EDF file
279
+ for i, signal in enumerate(signals):
280
+ edf_writer.setSignalHeader(i, signal_headers[i])
281
+ edf_writer.writeSamples(signals)
282
+
283
+ except Exception as e:
284
+ logger.error(f"Write data ERROR: {e}")
285
+
286
+
@@ -66,8 +66,11 @@ def signal_filter(data, sf=256., btype='lowpass', low=0.5, high=30.):
66
66
  % btype)
67
67
 
68
68
  # Use irrfilter of scipy.signal to construct a filter
69
- b, a = signal.iirfilter(N=3, Wn=fnorm, btype=btype, analog=False,
70
- output='ba', ftype='butter', fs=None)
69
+ try:
70
+ b, a = signal.iirfilter(N=3, Wn=fnorm, btype=btype, analog=False,
71
+ output='ba', ftype='butter', fs=None)
72
+ except ValueError as e:
73
+ raise ValueError(e)
71
74
 
72
75
  filtered_data = signal.filtfilt(b=b, a=a, x=data)
73
76
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: misleep
3
- Version: 0.2.8
3
+ Version: 0.2.9
4
4
  Summary: MiSleep: Mice Sleep EEG/EMG visualization, scoring and analysis.
5
5
  Home-page: https://github.com/BryanWang0702/MiSleep/
6
6
  Download-URL: https://github.com/BryanWang0702/MiSleep/
@@ -11,7 +11,7 @@ MAINTAINER_EMAIL = "swang@gmail.com"
11
11
  URL = "https://github.com/BryanWang0702/MiSleep/"
12
12
  LICENSE = "BSD (3-clause)"
13
13
  DOWNLOAD_URL = "https://github.com/BryanWang0702/MiSleep/"
14
- VERSION = "0.2.8"
14
+ VERSION = "0.2.9"
15
15
 
16
16
  INSTALL_REQUIRES = [
17
17
  "numpy>=1.18.1",
@@ -0,0 +1,77 @@
1
+ # -*- coding: UTF-8 -*-
2
+ """
3
+ @Project: misleep
4
+ @File: test_signal_io.py
5
+ @Author: Xueqiang Wang
6
+ @Date: 2024/2/22
7
+ @Description:
8
+ """
9
+
10
+ import pytest
11
+ from misleep.io.signal_io import load_mat, load_edf, write_mat, write_edf
12
+ from time import time
13
+ import os
14
+
15
+
16
+ class TestSignalIO():
17
+ def test_load_mat(self):
18
+ """Test load_mat"""
19
+ mat_path = r'./test/test_data/10mins_example_mat.mat'
20
+ s = time()
21
+ midata = load_mat(data_path=mat_path)
22
+ e = time()
23
+ print(f"Load data from .mat file, cost {e-s} seconds\n"
24
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
25
+
26
+ def test_load_edf(self):
27
+ """Test load_edf"""
28
+ edf_path = r'./test/test_data/10mins_example_edf.edf'
29
+ s = time()
30
+ midata = load_edf(data_path=edf_path)
31
+ e = time()
32
+ print(f"Load data from .edf file, cost {e-s} seconds\n"
33
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
34
+
35
+ def test_write_mat(self):
36
+ """Test write_mat"""
37
+ mat_path = r'./test/test_data/10mins_example_mat.mat'
38
+ s = time()
39
+ midata = load_mat(data_path=mat_path)
40
+ e = time()
41
+ print(f"Load data from .mat file, cost {e-s} seconds\n"
42
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
43
+
44
+ write_mat(midata.signals, midata.channels, midata.sf, midata.time,
45
+ r'./test/test_data/10mins_example_mat_saved_by_python.mat')
46
+ print('Saved to .mat successfully')
47
+
48
+ def test_write_edf(self):
49
+ """Test write_edf"""
50
+ mat_path = r'./test/test_data/10mins_example_mat.mat'
51
+ s = time()
52
+ midata = load_mat(data_path=mat_path)
53
+ e = time()
54
+ print(f"Load data from .mat file, cost {e-s} seconds\n"
55
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
56
+
57
+ write_edf(midata.signals, midata.channels, midata.sf, midata.time,
58
+ r'./test/test_data/10mins_example_mat_saved_by_python.edf')
59
+ print('Saved to .edf successfully')
60
+
61
+ def test_load_written_edf(self):
62
+ """Test load written edf"""
63
+ edf_path = r'./test/test_data/10mins_example_mat_saved_by_python.edf'
64
+ s = time()
65
+ midata = load_edf(data_path=edf_path)
66
+ e = time()
67
+ print(f"Load data from .mat file, cost {e-s} seconds\n"
68
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
69
+
70
+ def test_load_written_mat(self):
71
+ """Test load written mat"""
72
+ mat_path = r'./test/test_data/10mins_example_mat_saved_by_python.mat'
73
+ s = time()
74
+ midata = load_mat(data_path=mat_path)
75
+ e = time()
76
+ print(f"Load data from .mat file, cost {e-s} seconds\n"
77
+ f"Signals shape {len(midata.signals)}, \nchannels: {midata.channels}, \nsampling frequency: {midata.sf}, \nacquisition time: {midata.time}")
@@ -1,48 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- # Form implementation generated from reading ui file 'misleep/gui/uis/save_data_dialog.ui'
4
- #
5
- # Created by: PyQt5 UI code generator 5.15.10
6
- #
7
- # WARNING: Any manual changes made to this file will be lost when pyuic5 is
8
- # run again. Do not edit this file unless you know what you are doing.
9
-
10
-
11
- from PyQt5 import QtCore, QtGui, QtWidgets
12
-
13
-
14
- class Ui_Dialog(object):
15
- def setupUi(self, Dialog):
16
- Dialog.setObjectName("Dialog")
17
- Dialog.resize(432, 154)
18
- self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
19
- self.verticalLayout.setObjectName("verticalLayout")
20
- self.ACTimeEdit = QtWidgets.QGroupBox(Dialog)
21
- self.ACTimeEdit.setObjectName("ACTimeEdit")
22
- self.gridLayout = QtWidgets.QGridLayout(self.ACTimeEdit)
23
- self.gridLayout.setObjectName("gridLayout")
24
- self.dateTimeEdit = QtWidgets.QDateTimeEdit(self.ACTimeEdit)
25
- self.dateTimeEdit.setObjectName("dateTimeEdit")
26
- self.gridLayout.addWidget(self.dateTimeEdit, 1, 0, 1, 1)
27
- self.ResetACTimeEdit = QtWidgets.QCheckBox(self.ACTimeEdit)
28
- self.ResetACTimeEdit.setObjectName("ResetACTimeEdit")
29
- self.gridLayout.addWidget(self.ResetACTimeEdit, 0, 0, 1, 1)
30
- spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
31
- self.gridLayout.addItem(spacerItem, 1, 1, 1, 1)
32
- self.verticalLayout.addWidget(self.ACTimeEdit)
33
- self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
34
- self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
35
- self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
36
- self.buttonBox.setObjectName("buttonBox")
37
- self.verticalLayout.addWidget(self.buttonBox)
38
-
39
- self.retranslateUi(Dialog)
40
- self.buttonBox.accepted.connect(Dialog.accept) # type: ignore
41
- self.buttonBox.rejected.connect(Dialog.reject) # type: ignore
42
- QtCore.QMetaObject.connectSlotsByName(Dialog)
43
-
44
- def retranslateUi(self, Dialog):
45
- _translate = QtCore.QCoreApplication.translate
46
- Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
47
- self.ACTimeEdit.setTitle(_translate("Dialog", "Save data options"))
48
- self.ResetACTimeEdit.setText(_translate("Dialog", "Reset acquisition time?"))
@@ -1,39 +0,0 @@
1
- # -*- coding: UTF-8 -*-
2
- """
3
- @Project: misleep
4
- @File: test_signal_io.py
5
- @Author: Xueqiang Wang
6
- @Date: 2024/2/22
7
- @Description:
8
- """
9
-
10
- import pytest
11
- from misleep.io.signal_io import load_mat, load_edf, write_mat
12
- from time import time
13
-
14
-
15
- class TestSignalIO():
16
- def test_load_mat_write_mat(self):
17
- """Test load_mat and write_mat"""
18
- mat_path = r'E:\workplace\EEGProcessing\00_DATA\20231117_test+vid_6pin\data_mini_saved_python.mat'
19
- s = time()
20
- midata = load_mat(data_path=mat_path)
21
- e = time()
22
- print(f"Load data from .mat file, cost {e-s} seconds\n"
23
- f"Signals shape {len(midata.signals)}, channels: {midata.channels}, sampling frequency: {midata.sf}")
24
-
25
- write_mat(midata.signals, midata.channels, midata.sf, midata.time,
26
- r'E:\workplace\EEGProcessing\00_DATA\20231117_test+vid_6pin\data_mini_saved_python.mat')
27
- print('Saved to .mat successfully')
28
-
29
- def test_load_edf_write_mat(self):
30
- """Test load_edf and write_mat"""
31
- edf_path = r'../datasets/learn-nsrr01.edf'
32
- s = time()
33
- signals, channels, sf = load_edf(data_path=edf_path)
34
- e = time()
35
- print(f"Load data from .edf file, cost {e-s} seconds\n"
36
- f"Signals shape {len(signals)}, channels: {channels}, sampling frequency: {sf}")
37
-
38
- write_mat(signals, channels, sf, r'../../../datasets/mat_from_edf.mat')
39
- print('Saved to .mat successfully')
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes