misleep 0.3.1__tar.gz → 0.3.2__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.

Potentially problematic release.


This version of misleep might be problematic. Click here for more details.

Files changed (123) hide show
  1. {misleep-0.3.1 → misleep-0.3.2}/CHANGELOG.md +16 -0
  2. {misleep-0.3.1/src/misleep.egg-info → misleep-0.3.2}/PKG-INFO +1 -1
  3. {misleep-0.3.1 → misleep-0.3.2}/docs/config_file.md +4 -4
  4. {misleep-0.3.1 → misleep-0.3.2}/pyproject.toml +1 -1
  5. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/__init__.py +1 -1
  6. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/config/default_config.ini +2 -2
  7. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/data/midata.py +15 -5
  8. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/about.ui +2 -2
  9. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/about_ui.py +2 -2
  10. {misleep-0.3.1 → misleep-0.3.2/src/misleep.egg-info}/PKG-INFO +1 -1
  11. {misleep-0.3.1 → misleep-0.3.2}/tests/test_data.py +53 -0
  12. {misleep-0.3.1 → misleep-0.3.2}/tests/test_gui.py +1 -1
  13. {misleep-0.3.1 → misleep-0.3.2}/LICENSE +0 -0
  14. {misleep-0.3.1 → misleep-0.3.2}/MANIFEST.in +0 -0
  15. {misleep-0.3.1 → misleep-0.3.2}/README.md +0 -0
  16. {misleep-0.3.1 → misleep-0.3.2}/docs/api_reference.md +0 -0
  17. {misleep-0.3.1 → misleep-0.3.2}/docs/data_formats.md +0 -0
  18. {misleep-0.3.1 → misleep-0.3.2}/docs/developer_guide.md +0 -0
  19. {misleep-0.3.1 → misleep-0.3.2}/docs/getting_started.md +0 -0
  20. {misleep-0.3.1 → misleep-0.3.2}/docs/imgs/entire_logo.png +0 -0
  21. {misleep-0.3.1 → misleep-0.3.2}/docs/imgs/gui/main_light.png +0 -0
  22. {misleep-0.3.1 → misleep-0.3.2}/docs/imgs/logo.png +0 -0
  23. {misleep-0.3.1 → misleep-0.3.2}/docs/index.md +0 -0
  24. {misleep-0.3.1 → misleep-0.3.2}/docs/user_guide.md +0 -0
  25. {misleep-0.3.1 → misleep-0.3.2}/examples/auto_stage_example.py +0 -0
  26. {misleep-0.3.1 → misleep-0.3.2}/examples/basic_usage.py +0 -0
  27. {misleep-0.3.1 → misleep-0.3.2}/examples/extend_io_example.py +0 -0
  28. {misleep-0.3.1 → misleep-0.3.2}/setup.cfg +0 -0
  29. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/__main__.py +0 -0
  30. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/_compat.py +0 -0
  31. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/__init__.py +0 -0
  32. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/auto_stage.py +0 -0
  33. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/__init__.py +0 -0
  34. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/benchmark.py +0 -0
  35. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/features.py +0 -0
  36. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/hmm.py +0 -0
  37. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/model.py +0 -0
  38. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/autostage/postprocess.py +0 -0
  39. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/detection.py +0 -0
  40. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/features.py +0 -0
  41. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/models/__init__.py +0 -0
  42. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/models/benchmark_models.pkl +0 -0
  43. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/__init__.py +0 -0
  44. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/checkpoints/CausalTransformer_best.pt +0 -0
  45. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/checkpoints/__init__.py +0 -0
  46. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/configs.py +0 -0
  47. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/data.py +0 -0
  48. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/inference.py +0 -0
  49. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/models.py +0 -0
  50. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/analysis/transformer/preprocessing.py +0 -0
  51. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/config/__init__.py +0 -0
  52. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/data/__init__.py +0 -0
  53. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/data/annotation.py +0 -0
  54. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/__init__.py +0 -0
  55. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/app.py +0 -0
  56. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/config_dialog.py +0 -0
  57. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/dialogs.py +0 -0
  58. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/event_filters.py +0 -0
  59. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/main_window.py +0 -0
  60. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/qt_utils.py +0 -0
  61. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/__init__.py +0 -0
  62. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/entire_logo.png +0 -0
  63. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/logo.png +0 -0
  64. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/misleep.ico +0 -0
  65. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/misleep.qrc +0 -0
  66. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/resources/misleep_rc.py +0 -0
  67. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/spec_window.py +0 -0
  68. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/style.py +0 -0
  69. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/SWA_detect_dialog.ui +0 -0
  70. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/SWA_detect_dialog_ui.py +0 -0
  71. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/__init__.py +0 -0
  72. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog.ui +0 -0
  73. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/auto_stage_causalTransformer_dialog_ui.py +0 -0
  74. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/auto_stage_lightGBM_dialog.ui +0 -0
  75. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/auto_stage_lightGBM_dialog_ui.py +0 -0
  76. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/horizontal_line_dialog.ui +0 -0
  77. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/horizontal_line_dialog_ui.py +0 -0
  78. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/label_dialog.ui +0 -0
  79. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/label_dialog_ui.py +0 -0
  80. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/main_window.ui +0 -0
  81. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/main_window_ui.py +0 -0
  82. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/save_data_dialog.ui +0 -0
  83. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/save_data_dialog_ui.py +0 -0
  84. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/spec_window.ui +0 -0
  85. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/spec_window_ui.py +0 -0
  86. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/spindle_detect_dialog.ui +0 -0
  87. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/spindle_detect_dialog_ui.py +0 -0
  88. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/state_spectral_dialog.ui +0 -0
  89. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/state_spectral_dialog_ui.py +0 -0
  90. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/transfer_result_dialog.ui +0 -0
  91. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/uis/transfer_result_dialog_ui.py +0 -0
  92. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/gui/workers.py +0 -0
  93. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/__init__.py +0 -0
  94. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/annotation.py +0 -0
  95. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/array.py +0 -0
  96. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/base.py +0 -0
  97. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/edf.py +0 -0
  98. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/io/mat.py +0 -0
  99. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/logger.py +0 -0
  100. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/preprocessing/__init__.py +0 -0
  101. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/preprocessing/artifacts.py +0 -0
  102. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/preprocessing/filtering.py +0 -0
  103. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/preprocessing/segment.py +0 -0
  104. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/preprocessing/spectral.py +0 -0
  105. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/utils/__init__.py +0 -0
  106. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/utils/annotation.py +0 -0
  107. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/utils/entropy.py +0 -0
  108. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/utils/misc.py +0 -0
  109. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/utils/time_utils.py +0 -0
  110. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/viz/__init__.py +0 -0
  111. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/viz/hypnogram.py +0 -0
  112. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/viz/signals.py +0 -0
  113. {misleep-0.3.1 → misleep-0.3.2}/src/misleep/viz/spectral.py +0 -0
  114. {misleep-0.3.1 → misleep-0.3.2}/src/misleep.egg-info/SOURCES.txt +0 -0
  115. {misleep-0.3.1 → misleep-0.3.2}/src/misleep.egg-info/dependency_links.txt +0 -0
  116. {misleep-0.3.1 → misleep-0.3.2}/src/misleep.egg-info/entry_points.txt +0 -0
  117. {misleep-0.3.1 → misleep-0.3.2}/src/misleep.egg-info/requires.txt +0 -0
  118. {misleep-0.3.1 → misleep-0.3.2}/src/misleep.egg-info/top_level.txt +0 -0
  119. {misleep-0.3.1 → misleep-0.3.2}/tests/test_analysis.py +0 -0
  120. {misleep-0.3.1 → misleep-0.3.2}/tests/test_io.py +0 -0
  121. {misleep-0.3.1 → misleep-0.3.2}/tests/test_preprocessing.py +0 -0
  122. {misleep-0.3.1 → misleep-0.3.2}/tests/test_segment.py +0 -0
  123. {misleep-0.3.1 → misleep-0.3.2}/tests/test_viz_config.py +0 -0
@@ -5,6 +5,22 @@ All notable changes to MiSleep are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.2] — 2026-08-24
9
+
10
+ ### Fixed
11
+
12
+ - **Filtering broke on fractional sampling frequencies**: `MiData.add()` (used
13
+ by `MiData.filter()`) recomputed the duration with
14
+ `math.floor(len(signal) / sf)` from a signal that had already been
15
+ truncated to `int(duration * sf)` samples. When `sf` is a decimal (e.g.
16
+ `99.9927` Hz), that came out one second short, so filtering either raised a
17
+ spurious "new signal is shorter" error or produced a channel whose length /
18
+ duration did not match the original signal, which then failed the signal /
19
+ label consistency checks in the GUI. The check now compares sample counts
20
+ against `int(duration * sf)` directly, so filtering preserves the sample
21
+ count and duration for any positive `sf` while still rejecting genuinely
22
+ short signals.
23
+
8
24
  ## [0.3.1] — 2026-08-18
9
25
 
10
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: misleep
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: MiSleep: Mice sleep EEG/EMG visualization, scoring, and analysis
5
5
  Author-email: Xueqiang Wang <swang9194@gmail.com>
6
6
  Maintainer-email: Xueqiang Wang <swang9194@gmail.com>
@@ -23,8 +23,8 @@ wipe your personal settings. Open the user file from the GUI via
23
23
 
24
24
  | key | default | meaning |
25
25
  |-----|---------|---------|
26
- | `version` | `v0.3.1` | displayed in the About dialog |
27
- | `updatetime` | `2026/08/18` | displayed in the About dialog |
26
+ | `version` | `v0.3.2` | displayed in the About dialog |
27
+ | `updatetime` | `2026/08/24` | displayed in the About dialog |
28
28
  | `marker` | `['half_signal', 'first REM', ...]` | marker label list (label picker) |
29
29
  | `startend` | `['high_theta', 'REM', ...]` | start-end label list (label picker) |
30
30
  | `statemap` | `{"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT", ...}` | state code -> name mapping (JSON) |
@@ -48,8 +48,8 @@ wipe your personal settings. Open the user file from the GUI via
48
48
 
49
49
  ```ini
50
50
  [gui]
51
- version = v0.3.1
52
- updatetime = 2026/08/18
51
+ version = v0.3.2
52
+ updatetime = 2026/08/24
53
53
  marker = ['half_signal', 'first REM', 'WindEEG', 'W-R', 'maker']
54
54
  startend = ['high_theta', 'REM', 'Wake', 'Spindle', 'SWA', 'start end label', 'start end label']
55
55
  statemap = {"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT", "5": "IS", "6": "MicroArousal"}
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "misleep"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "MiSleep: Mice sleep EEG/EMG visualization, scoring, and analysis"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -22,7 +22,7 @@ works without PySide6; import ``misleep.gui`` explicitly when needed.
22
22
  from importlib import import_module
23
23
 
24
24
  __author__ = "Xueqiang Wang <swang9194@gmail.com>"
25
- __version__ = "0.3.1"
25
+ __version__ = "0.3.2"
26
26
 
27
27
  # Importing ``misleep`` used to import scipy, pandas, sklearn and matplotlib
28
28
  # immediately. The GUI necessarily imports some of those later, but making
@@ -15,8 +15,8 @@
15
15
  # overwritten on the next package update.
16
16
 
17
17
  [gui]
18
- version = v0.3.1
19
- updatetime = 2026/08/18
18
+ version = v0.3.2
19
+ updatetime = 2026/08/24
20
20
  marker = ['half_signal', 'first REM', 'WindEEG', 'W-R', 'maker']
21
21
  startend = ['high_theta', 'REM', 'Wake', 'Spindle', 'SWA', 'start end label', 'start end label']
22
22
  statemap = {"1": "NREM", "2": "REM", "3": "Wake", "4": "INIT"}
@@ -210,15 +210,25 @@ class MiData:
210
210
  if not isinstance(sf, (int, float)) or not math.isfinite(sf) or sf <= 0:
211
211
  raise TypeError(f"Sample frequency should be a float, got {type(sf)}")
212
212
 
213
- _duration = math.floor(len(signal) / sf)
214
- if _duration < self._duration:
213
+ # Every channel shares the same integer duration: the sample count a
214
+ # channel of ``sf`` Hz needs to cover ``self._duration`` seconds is
215
+ # ``int(self._duration * sf)`` (the same truncation used in
216
+ # :meth:`__init__`). Compare sample counts directly instead of
217
+ # re-deriving the duration from an already-truncated length -- with a
218
+ # fractional sampling frequency (e.g. 99.9927 Hz) the naive
219
+ # ``math.floor(len(signal) / sf)`` can come out one second short and
220
+ # wrongly reject signals that are exactly as long as the existing
221
+ # channels (this made ``MiData.filter()`` fail on fractional-sf data).
222
+ expected_samples = int(self._duration * sf)
223
+ if len(signal) < expected_samples:
215
224
  raise ValueError(
216
- f"The new signal is shorter ({_duration}s) than the existing data "
217
- f"({self._duration}s)")
225
+ f"The new signal is shorter ({len(signal)} samples) than the "
226
+ f"existing data ({self._duration}s, {expected_samples} samples "
227
+ f"at {sf} Hz)")
218
228
 
219
229
  channel = _unique_name(channel, self.channels)
220
230
 
221
- self._signals.append(signal[:int(self._duration * sf)])
231
+ self._signals.append(signal[:expected_samples])
222
232
  self._channels.append(channel)
223
233
  self._n_channels = len(self._channels)
224
234
  self._sf.append(sf)
@@ -58,14 +58,14 @@
58
58
  <item>
59
59
  <widget class="QLabel" name="VersionLabel">
60
60
  <property name="text">
61
- <string>Version: 0.3.1</string>
61
+ <string>Version: 0.3.2</string>
62
62
  </property>
63
63
  </widget>
64
64
  </item>
65
65
  <item>
66
66
  <widget class="QLabel" name="UpdateLabel">
67
67
  <property name="text">
68
- <string>Update: 2026/08/18</string>
68
+ <string>Update: 2026/08/24</string>
69
69
  </property>
70
70
  </widget>
71
71
  </item>
@@ -73,8 +73,8 @@ class Ui_AboutDialog(object):
73
73
  AboutDialog.setWindowTitle(QCoreApplication.translate("AboutDialog", u"About", None))
74
74
  self.label_4.setText(QCoreApplication.translate("AboutDialog", u"<html><head/><body><p><img src=\":/logo/entire_logo.png\"/></p></body></html>", None))
75
75
  self.groupBox.setTitle(QCoreApplication.translate("AboutDialog", u"MiSleep", None))
76
- self.VersionLabel.setText(QCoreApplication.translate("AboutDialog", u"Version: 0.3.1", None))
77
- self.UpdateLabel.setText(QCoreApplication.translate("AboutDialog", u"Update: 2026/08/18", None))
76
+ self.VersionLabel.setText(QCoreApplication.translate("AboutDialog", u"Version: 0.3.2", None))
77
+ self.UpdateLabel.setText(QCoreApplication.translate("AboutDialog", u"Update: 2026/08/24", None))
78
78
  self.CopyrightLabel.setText(QCoreApplication.translate("AboutDialog", u"Copyright \u00a9 2023\u20132026 Xueqiang Wang.", None))
79
79
  # retranslateUi
80
80
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: misleep
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: MiSleep: Mice sleep EEG/EMG visualization, scoring, and analysis
5
5
  Author-email: Xueqiang Wang <swang9194@gmail.com>
6
6
  Maintainer-email: Xueqiang Wang <swang9194@gmail.com>
@@ -88,6 +88,59 @@ def test_midata_filter(midata):
88
88
  assert midata.channels[-1].startswith("EEG_bandpass")
89
89
 
90
90
 
91
+ @pytest.mark.parametrize("sf", [99.9927, 100.5, 256.256, 305.005])
92
+ def test_midata_filter_fractional_sf_preserves_length(sf):
93
+ """Filtering must not change the sample count or duration when sf is fractional.
94
+
95
+ Regression: ``MiData.add`` re-derived the duration with
96
+ ``math.floor(len(signal) / sf)`` from a signal already truncated to
97
+ ``int(duration * sf)`` samples; with a fractional sf that could come out
98
+ one second short and made ``filter()`` raise a spurious error (or produce
99
+ a shorter channel).
100
+ """
101
+ rng = np.random.default_rng(42)
102
+ n_raw = int(1000 * sf)
103
+ md = MiData(signals=[rng.standard_normal(n_raw),
104
+ rng.standard_normal(n_raw)],
105
+ channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
106
+ duration, lengths = md.duration, [len(s) for s in md.signals]
107
+
108
+ md.filter(chans=["EEG"], btype="bandpass", low=0.5, high=30)
109
+
110
+ assert md.n_channels == 3
111
+ assert md.duration == duration
112
+ assert len(md.signals) == 3
113
+ assert all(len(sig) == length for sig, length in zip(md.signals, lengths))
114
+
115
+
116
+ def test_midata_add_fractional_sf():
117
+ """A channel with ``int(duration * sf)`` samples must be accepted."""
118
+ sf = 99.9927
119
+ md = MiData(signals=[np.zeros(int(1000 * sf)), np.zeros(int(1000 * sf))],
120
+ channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
121
+ n = md.n_channels
122
+ duration, expected = md.duration, int(md.duration * sf)
123
+
124
+ # Exactly as many samples as the existing channels -> accepted as-is
125
+ md.add(np.zeros(expected), "EEG2", sf)
126
+ assert md.n_channels == n + 1
127
+ assert len(md.signals[-1]) == expected
128
+ assert md.duration == duration # duration unchanged by add()
129
+
130
+ # A genuinely shorter signal is still rejected
131
+ with pytest.raises(ValueError):
132
+ md.add(np.zeros(expected - 10), "TOO_SHORT", sf)
133
+
134
+
135
+ def test_midata_add_fractional_sf_rejects_short_signal():
136
+ """Fractional-sf channels that are truly too short must still be rejected."""
137
+ sf = 256.256
138
+ md = MiData(signals=[np.zeros(int(1000 * sf)), np.zeros(int(1000 * sf))],
139
+ channels=["EEG", "EMG"], sf=[sf, sf], time="20240101-00:00:00")
140
+ with pytest.raises(ValueError):
141
+ md.add(np.zeros(500 * 256), "TOO_SHORT", 256.0)
142
+
143
+
91
144
  def test_midata_differential(midata):
92
145
  midata.differential(chan1="EEG", chan2="EMG")
93
146
  assert midata.channels[-1] == "EEG_EMG_DIFF"
@@ -21,7 +21,7 @@ def _pyside6_available():
21
21
  def test_public_api():
22
22
  import misleep
23
23
 
24
- assert misleep.__version__ == "0.3.1"
24
+ assert misleep.__version__ == "0.3.2"
25
25
  for name in [
26
26
  "MiData",
27
27
  "MiAnnotation",
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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes