physioex 2.0.3__tar.gz → 2.0.4__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 (314) hide show
  1. {physioex-2.0.3 → physioex-2.0.4}/CHANGELOG.md +21 -0
  2. {physioex-2.0.3 → physioex-2.0.4}/PKG-INFO +1 -1
  3. physioex-2.0.4/physioex/__init__.py +1 -0
  4. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/alzheimers.py +9 -2
  5. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/sleepedf.py +7 -0
  6. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/wsc.py +200 -69
  7. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/modality.py +4 -0
  8. {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_modality.py +10 -0
  9. {physioex-2.0.3 → physioex-2.0.4}/tests/test_alzheimers_parkinsons.py +21 -7
  10. {physioex-2.0.3 → physioex-2.0.4}/tests/test_data_layer_hardening.py +11 -0
  11. {physioex-2.0.3 → physioex-2.0.4}/tests/test_phase_b_datasets.py +14 -0
  12. {physioex-2.0.3 → physioex-2.0.4}/tests/test_wsc_dataset.py +156 -0
  13. physioex-2.0.3/physioex/__init__.py +0 -1
  14. {physioex-2.0.3 → physioex-2.0.4}/.claudeignore +0 -0
  15. {physioex-2.0.3 → physioex-2.0.4}/.github/copilot-instructions.md +0 -0
  16. {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/black.yml +0 -0
  17. {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/ci.yml +0 -0
  18. {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/publish.yaml +0 -0
  19. {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/test.yml +0 -0
  20. {physioex-2.0.3 → physioex-2.0.4}/.gitignore +0 -0
  21. {physioex-2.0.3 → physioex-2.0.4}/LICENSE +0 -0
  22. {physioex-2.0.3 → physioex-2.0.4}/MANIFEST.in +0 -0
  23. {physioex-2.0.3 → physioex-2.0.4}/README.md +0 -0
  24. {physioex-2.0.3 → physioex-2.0.4}/REPO_MAP.txt +0 -0
  25. {physioex-2.0.3 → physioex-2.0.4}/docs/_ext/related_works.py +0 -0
  26. {physioex-2.0.3 → physioex-2.0.4}/docs/_static/custom.css +0 -0
  27. {physioex-2.0.3 → physioex-2.0.4}/docs/api/index.md +0 -0
  28. {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/data/sequence_viz.png +0 -0
  29. {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo.psd +0 -0
  30. {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo.svg +0 -0
  31. {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo_bar.svg +0 -0
  32. {physioex-2.0.3 → physioex-2.0.4}/docs/conf.py +0 -0
  33. {physioex-2.0.3 → physioex-2.0.4}/docs/index.md +0 -0
  34. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/data.md +0 -0
  35. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/explain.md +0 -0
  36. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/models.md +0 -0
  37. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/overview.md +0 -0
  38. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/train.md +0 -0
  39. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/testing/overview.md +0 -0
  40. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/contribute.md +0 -0
  41. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/data.md +0 -0
  42. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/explain/explain.md +0 -0
  43. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/preprocess.md +0 -0
  44. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/cli.md +0 -0
  45. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/networks/chambon2018.md +0 -0
  46. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/networks.md +0 -0
  47. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/train.md +0 -0
  48. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/unavailable.md +0 -0
  49. {physioex-2.0.3 → physioex-2.0.4}/docs/pages/user_guide.md +0 -0
  50. {physioex-2.0.3 → physioex-2.0.4}/docs/related/_thumbs/_placeholder.svg +0 -0
  51. {physioex-2.0.3 → physioex-2.0.4}/docs/related/fetch_citations.py +0 -0
  52. {physioex-2.0.3 → physioex-2.0.4}/docs/related/index.md +0 -0
  53. {physioex-2.0.3 → physioex-2.0.4}/docs/related/works.yaml +0 -0
  54. {physioex-2.0.3 → physioex-2.0.4}/examples/__init__.py +0 -0
  55. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/__init__.py +0 -0
  56. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/compare_strategies.py +0 -0
  57. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/extract_all_concepts.py +0 -0
  58. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/report.py +0 -0
  59. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/utils.py +0 -0
  60. {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/visualize_concepts.py +0 -0
  61. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/README.md +0 -0
  62. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/explain_csd.py +0 -0
  63. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/train_probe.py +0 -0
  64. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/utils.py +0 -0
  65. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/visualize.py +0 -0
  66. {physioex-2.0.3 → physioex-2.0.4}/examples/explain/lrp_seqsleepnet_n3.py +0 -0
  67. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/bendr/extract_embeddings.py +0 -0
  68. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/biot/extract_embeddings.py +0 -0
  69. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/cbramod/extract_embeddings.py +0 -0
  70. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/labram/extract_embeddings.py +0 -0
  71. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/neurolm/extract_embeddings.py +0 -0
  72. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/reve/extract_embeddings.py +0 -0
  73. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/sjepa/extract_embeddings.py +0 -0
  74. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/sleepfm/extract_embeddings.py +0 -0
  75. {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/tfc/extract_embeddings.py +0 -0
  76. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/extract_embeddings.py +0 -0
  77. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/extract_embeddings.sbatch +0 -0
  78. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/quickstart.py +0 -0
  79. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/smoke.sbatch +0 -0
  80. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/test_pretrained.py +0 -0
  81. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/train.py +0 -0
  82. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/train.sbatch +0 -0
  83. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/coresleep-kontras/extract_embeddings.py +0 -0
  84. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/coresleep-kontras/train.py +0 -0
  85. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/extract_embeddings.py +0 -0
  86. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/quickstart.py +0 -0
  87. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/smoke.sbatch +0 -0
  88. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/test_pretrained.py +0 -0
  89. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/test_pretrained.sbatch +0 -0
  90. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/train.py +0 -0
  91. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/train.sbatch +0 -0
  92. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/protosleepnet/README.md +0 -0
  93. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/protosleepnet/quickstart.py +0 -0
  94. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/extract_embeddings.py +0 -0
  95. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/extract_embeddings.sbatch +0 -0
  96. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/quickstart.py +0 -0
  97. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test.py +0 -0
  98. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_pretrained.py +0 -0
  99. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_pretrained.sbatch +0 -0
  100. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_stages_sites.py +0 -0
  101. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train.py +0 -0
  102. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train.sbatch +0 -0
  103. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train_shhs.py +0 -0
  104. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train_shhs.sbatch +0 -0
  105. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/extract_embeddings.py +0 -0
  106. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/extract_embeddings.sbatch +0 -0
  107. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/quickstart.py +0 -0
  108. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/smoke.sbatch +0 -0
  109. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/test_pretrained.py +0 -0
  110. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/test_pretrained.sbatch +0 -0
  111. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/train.py +0 -0
  112. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/train.sbatch +0 -0
  113. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.py +0 -0
  114. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.sbatch +0 -0
  115. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/quickstart.py +0 -0
  116. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/smoke.sbatch +0 -0
  117. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/test_pretrained.py +0 -0
  118. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/test_pretrained.sbatch +0 -0
  119. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/train.py +0 -0
  120. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/train.sbatch +0 -0
  121. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/extract_embeddings.py +0 -0
  122. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/quickstart.py +0 -0
  123. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/smoke.sbatch +0 -0
  124. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/test_pretrained.py +0 -0
  125. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/train.py +0 -0
  126. {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/train.sbatch +0 -0
  127. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/notebook.ipynb +0 -0
  128. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_barplot.png +0 -0
  129. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_hypnogram.png +0 -0
  130. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_barplot.png +0 -0
  131. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_hypnogram.png +0 -0
  132. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell17_ig_tiny.png +0 -0
  133. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell19_ig_seq.png +0 -0
  134. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell19b_attr_vs_psd.png +0 -0
  135. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell5_raw_n3_psd.png +0 -0
  136. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell7_spec_n3_psd.png +0 -0
  137. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/run_notebook.py +0 -0
  138. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/script.pdf +0 -0
  139. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/slides.pdf +0 -0
  140. {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/util.py +0 -0
  141. {physioex-2.0.3 → physioex-2.0.4}/lectures/README.md +0 -0
  142. {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/kuleuven_logo.png +0 -0
  143. {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/physioex_logo.png +0 -0
  144. {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/physioex_logo.svg +0 -0
  145. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/__init__.py +0 -0
  146. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/base.py +0 -0
  147. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/cache.py +0 -0
  148. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/collate.py +0 -0
  149. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datareader.py +0 -0
  150. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/dataset.py +0 -0
  151. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/__init__.py +0 -0
  152. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/_nsrr.py +0 -0
  153. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/_splits/shhs.json +0 -0
  154. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/ccshs.py +0 -0
  155. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/channels_table.md +0 -0
  156. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/dcsm.py +0 -0
  157. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/hmc.py +0 -0
  158. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/hpap.py +0 -0
  159. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mass.py +0 -0
  160. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mesa.py +0 -0
  161. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mros.py +0 -0
  162. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/parkinsons.py +0 -0
  163. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/shhs.py +0 -0
  164. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/stages.py +0 -0
  165. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/events.py +0 -0
  166. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/multi.py +0 -0
  167. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/pipeline.py +0 -0
  168. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/presets.py +0 -0
  169. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/__init__.py +0 -0
  170. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/annotations.py +0 -0
  171. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/edf.py +0 -0
  172. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/sampling.py +0 -0
  173. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/__init__.py +0 -0
  174. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/bandpass.py +0 -0
  175. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/highpass.py +0 -0
  176. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/identity.py +0 -0
  177. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/normalize.py +0 -0
  178. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/notch.py +0 -0
  179. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/resample.py +0 -0
  180. {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/spectrogram.py +0 -0
  181. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/__init__.py +0 -0
  182. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/__init__.py +0 -0
  183. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/csd.py +0 -0
  184. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/multichannel_sg.py +0 -0
  185. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/report.py +0 -0
  186. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/sleep_bands.py +0 -0
  187. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/specificity.py +0 -0
  188. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/__init__.py +0 -0
  189. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/_functional.py +0 -0
  190. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/_rules.py +0 -0
  191. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/attributor.py +0 -0
  192. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/canonizers.py +0 -0
  193. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/composites.py +0 -0
  194. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/diagnostics.py +0 -0
  195. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/model.py +0 -0
  196. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/pooling.py +0 -0
  197. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/recurrent.py +0 -0
  198. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/transformer.py +0 -0
  199. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/__init__.py +0 -0
  200. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/filters.py +0 -0
  201. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/functionizer.py +0 -0
  202. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/gradients.py +0 -0
  203. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/metrics/__init__.py +0 -0
  204. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/spectralgradients.py +0 -0
  205. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/vidft.py +0 -0
  206. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/vistdft.py +0 -0
  207. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/__init__.py +0 -0
  208. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/local.py +0 -0
  209. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/__init__.py +0 -0
  210. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/nmf.py +0 -0
  211. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/utils.py +0 -0
  212. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/vq.py +0 -0
  213. {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/reconstruct.py +0 -0
  214. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/__init__.py +0 -0
  215. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/bendr.py +0 -0
  216. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/biot.py +0 -0
  217. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/cbramod.py +0 -0
  218. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/chambon2018.py +0 -0
  219. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/coresleep.py +0 -0
  220. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/embed.py +0 -0
  221. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_base.py +0 -0
  222. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_channel_mapping.py +0 -0
  223. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_checkpoints.py +0 -0
  224. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_datasets.py +0 -0
  225. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_preproc.py +0 -0
  226. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/labram.py +0 -0
  227. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/lseqsleepnet.py +0 -0
  228. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/neurolm.py +0 -0
  229. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/pretrained.py +0 -0
  230. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/protosleepnet.py +0 -0
  231. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/reve.py +0 -0
  232. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/seqsleepnet.py +0 -0
  233. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sjepa.py +0 -0
  234. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleep_tokenizer.py +0 -0
  235. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleepfm.py +0 -0
  236. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleeptransformer.py +0 -0
  237. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tfc.py +0 -0
  238. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tinysleepnet.py +0 -0
  239. {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tsinalis.py +0 -0
  240. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/__init__.py +0 -0
  241. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/__init__.py +0 -0
  242. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/_common.py +0 -0
  243. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/finetune.py +0 -0
  244. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/test.py +0 -0
  245. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/train.py +0 -0
  246. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/logger.py +0 -0
  247. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/metrics.py +0 -0
  248. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/__init__.py +0 -0
  249. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/check_table.csv +0 -0
  250. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/load.py +0 -0
  251. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/multidevicetrainer.py +0 -0
  252. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/progress.py +0 -0
  253. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/stats.py +0 -0
  254. {physioex-2.0.3 → physioex-2.0.4}/physioex/train/trainer.py +0 -0
  255. {physioex-2.0.3 → physioex-2.0.4}/pyproject.toml +0 -0
  256. {physioex-2.0.3 → physioex-2.0.4}/tests/__init__.py +0 -0
  257. {physioex-2.0.3 → physioex-2.0.4}/tests/conftest.py +0 -0
  258. {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_base_smoke.py +0 -0
  259. {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_multi.py +0 -0
  260. {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_readers_annotations.py +0 -0
  261. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/foundational/test_sleep_bands.py +0 -0
  262. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/foundational/test_specificity.py +0 -0
  263. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/__init__.py +0 -0
  264. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_functional.py +0 -0
  265. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_lrp.py +0 -0
  266. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_model.py +0 -0
  267. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_real_models.py +0 -0
  268. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_recurrent.py +0 -0
  269. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_transformer.py +0 -0
  270. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/__init__.py +0 -0
  271. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_gradients.py +0 -0
  272. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_metrics.py +0 -0
  273. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_spectralgradients.py +0 -0
  274. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_vidft.py +0 -0
  275. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_vistdft.py +0 -0
  276. {physioex-2.0.3 → physioex-2.0.4}/tests/explain/prototypes/posthoc/test_nmf_vq.py +0 -0
  277. {physioex-2.0.3 → physioex-2.0.4}/tests/factories/__init__.py +0 -0
  278. {physioex-2.0.3 → physioex-2.0.4}/tests/factories/edf.py +0 -0
  279. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_architectures.py +0 -0
  280. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_base.py +0 -0
  281. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_encoders.py +0 -0
  282. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_preproc.py +0 -0
  283. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_pretrained_embed_helpers.py +0 -0
  284. {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_sleep_tokenizer.py +0 -0
  285. {physioex-2.0.3 → physioex-2.0.4}/tests/test_api_surface.py +0 -0
  286. {physioex-2.0.3 → physioex-2.0.4}/tests/test_cache.py +0 -0
  287. {physioex-2.0.3 → physioex-2.0.4}/tests/test_cache_enabled.py +0 -0
  288. {physioex-2.0.3 → physioex-2.0.4}/tests/test_ccshs.py +0 -0
  289. {physioex-2.0.3 → physioex-2.0.4}/tests/test_channel_acc_tracking.py +0 -0
  290. {physioex-2.0.3 → physioex-2.0.4}/tests/test_channel_resolution.py +0 -0
  291. {physioex-2.0.3 → physioex-2.0.4}/tests/test_cli_workflows.py +0 -0
  292. {physioex-2.0.3 → physioex-2.0.4}/tests/test_dataset_config.py +0 -0
  293. {physioex-2.0.3 → physioex-2.0.4}/tests/test_e2e_smoke.py +0 -0
  294. {physioex-2.0.3 → physioex-2.0.4}/tests/test_events.py +0 -0
  295. {physioex-2.0.3 → physioex-2.0.4}/tests/test_explain_fixes.py +0 -0
  296. {physioex-2.0.3 → physioex-2.0.4}/tests/test_label_hardening.py +0 -0
  297. {physioex-2.0.3 → physioex-2.0.4}/tests/test_load_model.py +0 -0
  298. {physioex-2.0.3 → physioex-2.0.4}/tests/test_mass_dataset.py +0 -0
  299. {physioex-2.0.3 → physioex-2.0.4}/tests/test_metrics_and_packaging.py +0 -0
  300. {physioex-2.0.3 → physioex-2.0.4}/tests/test_modality_dropout.py +0 -0
  301. {physioex-2.0.3 → physioex-2.0.4}/tests/test_phase_c_datasets.py +0 -0
  302. {physioex-2.0.3 → physioex-2.0.4}/tests/test_pipeline.py +0 -0
  303. {physioex-2.0.3 → physioex-2.0.4}/tests/test_presets.py +0 -0
  304. {physioex-2.0.3 → physioex-2.0.4}/tests/test_raw_dataset_integration.py +0 -0
  305. {physioex-2.0.3 → physioex-2.0.4}/tests/test_reading_modes.py +0 -0
  306. {physioex-2.0.3 → physioex-2.0.4}/tests/test_sampling.py +0 -0
  307. {physioex-2.0.3 → physioex-2.0.4}/tests/test_stages_dataset.py +0 -0
  308. {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_enhancements.py +0 -0
  309. {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_fixes.py +0 -0
  310. {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_integration.py +0 -0
  311. {physioex-2.0.3 → physioex-2.0.4}/tests/test_wake_trimming.py +0 -0
  312. {physioex-2.0.3 → physioex-2.0.4}/tests/train/test_logger.py +0 -0
  313. {physioex-2.0.3 → physioex-2.0.4}/tests/train/test_progress.py +0 -0
  314. {physioex-2.0.3 → physioex-2.0.4}/tests/train/test_stats.py +0 -0
@@ -4,6 +4,27 @@ All notable changes to PhysioEx are documented here. Versions follow the
4
4
  `physioex.__version__` string (read dynamically by the build) and the `v*` git tags
5
5
  that publish to PyPI.
6
6
 
7
+ ## 2.0.4 — 2026-10-16
8
+
9
+ Data-layer fixes found by an audit of what physioex reads against the raw files of every dataset. Label caches of
10
+ Sleep-EDF, WSC and Alzheimers are recomputed on first use (`LABELS_REVISION` 2, 2 and 3). Cached WSC events are not
11
+ versioned: delete `events/<subject>.json` of the 26 allscore nights whose events changed.
12
+
13
+ ### Fixed
14
+ - **WSC `.allscore.txt`:** labels and events are anchored at `START RECORDING`. A `STAGE - NO STAGE` line timed before it
15
+ moved the time origin back by hours and shifted every later label (45 of 743 files). Midnight is a backward jump of
16
+ the clock, `PAUSED` to `RECORDING RESUMED` stretches are removed (the EDF has no samples there), and labels or events
17
+ past the end of the EDF are dropped. A file without `START RECORDING` yields no labels, as before.
18
+ - **Sleep-EDF:** the hypnogram always runs to 24 h; its tail is cut at the end of the PSG instead of being paired with
19
+ zero padding (in one night 614 epochs of `Wake`).
20
+ - **Alzheimers:** labels past the end of a truncated EDF are dropped.
21
+ - **MESA:** `*_Off` electrode-status flags (1 Hz) are classified `OTHER`, not EEG/EOG/EMG.
22
+
23
+ ### Added
24
+ - **WSC events from `.sco.txt`** (the 1827 Gamma-system nights; only the 743 Twin-system `.allscore.txt` nights had
25
+ events). Same event types as the allscore parser. `Scan # x2` counts samples at twice the EDF rate; a few files write
26
+ the scan with two extra digits (the clock column is used) and 548 write the length ×100 with a negative sign.
27
+
7
28
  ## 2.0.3 — 2026-10-15
8
29
 
9
30
  CCSHS dataset, opt-in utilities for multi-dataset training, and three data-layer fixes. STAGES and Alzheimers label
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: physioex
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: A python package for explainable sleep staging via deep learning
5
5
  Project-URL: Homepage, https://github.com/guidogagl/physioex
6
6
  Project-URL: Documentation, https://guidogagl.github.io/physioex
@@ -0,0 +1 @@
1
+ __version__ = "2.0.4"
@@ -42,7 +42,8 @@ class AlzheimersDataset(BasePhysioDataset):
42
42
  DATASET_NAME = "alzheimers"
43
43
  DATASET_SUBDIR = "AlzheimerData"
44
44
  #: 2: labels are indexed by event onset (gaps stay -1); revision-1 caches are compacted and misaligned.
45
- LABELS_REVISION = 2
45
+ #: 3: labels past the end of the EDF are dropped (truncated recordings).
46
+ LABELS_REVISION = 3
46
47
  DEFAULT_EPOCH_LENGTH_SEC = 30.0
47
48
 
48
49
  # Channel preference lists.
@@ -208,4 +209,10 @@ class AlzheimersDataset(BasePhysioDataset):
208
209
  for epoch_idx, stage in rows:
209
210
  labels[epoch_idx] = stage
210
211
 
211
- return labels
212
+ # Some EDFs are shorter than the annotated night (truncated recordings): labels
213
+ # past the end of the signal are dropped instead of being paired with zero padding.
214
+ import pyedflib
215
+
216
+ with pyedflib.EdfReader(str(spec.edf_path)) as f:
217
+ n_signal = int(f.getFileDuration() // self.epoch_length_sec)
218
+ return labels[:n_signal]
@@ -50,6 +50,8 @@ class SleepEDFDataset(BasePhysioDataset):
50
50
  """PhysioNet Sleep-EDF Cassette dataset (~153 recordings, ~78 subjects)."""
51
51
 
52
52
  DATASET_NAME = "sleepedf"
53
+ #: 2: hypnogram labels are cut at the end of the PSG (the 24 h hypnogram tail is dropped).
54
+ LABELS_REVISION = 2
53
55
  DATASET_SUBDIR = "physionet-sleep-data"
54
56
  DEFAULT_EPOCH_LENGTH_SEC = 30.0
55
57
 
@@ -190,6 +192,11 @@ class SleepEDFDataset(BasePhysioDataset):
190
192
 
191
193
  # Total recording span determines number of epochs
192
194
  total_sec = max(float(o) + float(d) for o, d in zip(onsets, durations))
195
+ # The Sleep-EDF hypnograms always run to 24 h (86400 s) while the PSG stops earlier;
196
+ # the tail is "Sleep stage ?" or, in some nights, "Sleep stage W". Labels past the
197
+ # end of the signal would be paired with zero padding, so they are dropped here.
198
+ with pyedflib.EdfReader(str(spec.edf_path)) as g:
199
+ total_sec = min(total_sec, float(g.getFileDuration()))
193
200
  n_epochs = int(total_sec // self.epoch_length_sec)
194
201
  labels = np.full(n_epochs, -1, dtype=np.int16)
195
202
 
@@ -67,6 +67,8 @@ class WSCDataset(BasePhysioDataset):
67
67
  """
68
68
 
69
69
  DATASET_NAME = "wsc"
70
+ #: 2: allscore labels anchored at START RECORDING (paused stretches removed), labels cut at the EDF end.
71
+ LABELS_REVISION = 2
70
72
  DATASET_SUBDIR = "wsc"
71
73
  DEFAULT_EPOCH_LENGTH_SEC = 30.0
72
74
 
@@ -199,11 +201,17 @@ class WSCDataset(BasePhysioDataset):
199
201
  path = spec.label_path
200
202
  suffix = str(path)
201
203
  if suffix.endswith(".allscore.txt"):
202
- return self._parse_allscore(path)
204
+ labels = self._parse_allscore(path)
203
205
  elif suffix.endswith(".stg.txt"):
204
- return self._parse_stg(path)
206
+ labels = self._parse_stg(path)
205
207
  else:
206
208
  return np.array([], dtype=np.int16)
209
+ # In some nights the scoring runs past the end of the EDF; labels without signal are dropped.
210
+ import pyedflib
211
+
212
+ with pyedflib.EdfReader(str(spec.edf_path)) as f:
213
+ n_signal = int(f.getFileDuration() // self.epoch_length_sec)
214
+ return labels[:n_signal]
207
215
 
208
216
  # ------------------------------------------------------------------
209
217
  # .stg.txt parser
@@ -247,6 +255,48 @@ class WSCDataset(BasePhysioDataset):
247
255
  # .allscore.txt parser
248
256
  # ------------------------------------------------------------------
249
257
 
258
+ @staticmethod
259
+ def _allscore_offsets(lines):
260
+ """Seconds from "START RECORDING" for every line of an ``.allscore.txt``, ``None`` where unusable.
261
+
262
+ Only lines from the START RECORDING line on are timed: some files list a
263
+ ``STAGE - NO STAGE`` line with a clock time before the recording starts, which
264
+ used to pull the time origin back by hours and shift every later label.
265
+ Midnight is detected as a large backward jump of the clock, not by the hour of day.
266
+ The time between a PAUSED line and the next RECORDING RESUMED line is removed,
267
+ because the EDF has no samples for it.
268
+ Returns ``None`` when there is no START RECORDING line.
269
+ """
270
+
271
+ def _clock(line):
272
+ t = datetime.datetime.strptime(line.split("\t")[0].strip(), "%H:%M:%S.%f")
273
+ return t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1e6
274
+
275
+ start_i = next((i for i, ln in enumerate(lines) if "START RECORDING" in ln), None)
276
+ if start_i is None:
277
+ return None
278
+ start_sec = _clock(lines[start_i])
279
+ offsets = [None] * len(lines)
280
+ day, prev = 0, start_sec
281
+ skipped, pause_start = 0.0, None # the EDF does not contain the paused stretches
282
+ for i in range(start_i, len(lines)):
283
+ try:
284
+ t = _clock(lines[i])
285
+ except (ValueError, TypeError):
286
+ continue
287
+ if t < prev - 6 * 3600:
288
+ day += 1
289
+ prev = t
290
+ raw = day * 86400 + t - start_sec
291
+ if "RECORDING RESUMED" in lines[i]:
292
+ if pause_start is not None:
293
+ skipped += raw - pause_start
294
+ pause_start = None
295
+ elif "PAUSED" in lines[i] and pause_start is None:
296
+ pause_start = raw
297
+ offsets[i] = raw - skipped
298
+ return offsets
299
+
250
300
  @staticmethod
251
301
  def _parse_allscore(path: Path) -> np.ndarray:
252
302
  """Parse a WSC ``.allscore.txt`` file.
@@ -261,23 +311,16 @@ class WSCDataset(BasePhysioDataset):
261
311
  with open(path, encoding="latin1") as f:
262
312
  lines = f.readlines()
263
313
 
264
- # Find START RECORDING time
265
- start_time = None
266
- for line in lines:
267
- if "START RECORDING" in line:
268
- time_str = line.split("\t")[0]
269
- start_time = datetime.datetime.strptime(time_str, "%H:%M:%S.%f")
270
- break
271
-
272
- if start_time is None:
314
+ offsets = WSCDataset._allscore_offsets(lines)
315
+ if offsets is None:
273
316
  return np.array([], dtype=np.int16)
274
317
 
275
318
  stages = []
276
- current_time = start_time
319
+ current = 0.0
277
320
  stage = 7 # unscored at the beginning (maps to -1)
278
321
 
279
- for line in lines:
280
- if "STAGE - " not in line:
322
+ for line, off in zip(lines, offsets):
323
+ if off is None or "STAGE - " not in line:
281
324
  continue
282
325
 
283
326
  new_stage_str = line.split("STAGE - ")[-1].strip()
@@ -286,35 +329,20 @@ class WSCDataset(BasePhysioDataset):
286
329
  # Unrecognised stage label -- treat as unscored
287
330
  new_stage = -1
288
331
 
289
- stage_time_str = line.split("\t")[0]
290
- stage_time = datetime.datetime.strptime(stage_time_str, "%H:%M:%S.%f")
291
-
292
- # Handle midnight crossing: hours <= 12 assumed next day
293
- if stage_time.hour <= 12:
294
- stage_time += datetime.timedelta(days=1)
295
-
296
- # Fill epochs from current_time to stage_time with the previous stage
297
- time_passed = stage_time - current_time
298
- passed_epochs = int(time_passed.total_seconds() / 30)
332
+ # Fill epochs from the current stage onset to this one with the previous stage
333
+ passed_epochs = int((off - current) / 30)
299
334
  # Map the initial "unscored" sentinel (7) to -1
300
335
  fill_val = -1 if stage == 7 else stage
301
- stages.extend([fill_val] * passed_epochs)
336
+ stages.extend([fill_val] * max(passed_epochs, 0))
302
337
 
303
338
  stage = new_stage
304
- current_time = stage_time
339
+ current = max(off, current)
305
340
 
306
- # Handle the tail: last STAGE event to end of file
307
- if lines:
308
- end_time_str = lines[-1].split("\t")[0]
309
- try:
310
- end_time = datetime.datetime.strptime(end_time_str, "%H:%M:%S.%f")
311
- end_time += datetime.timedelta(days=1)
312
- time_passed = end_time - current_time
313
- passed_epochs = int(time_passed.total_seconds() / 30)
314
- fill_val = -1 if stage == 7 else stage
315
- stages.extend([fill_val] * passed_epochs)
316
- except ValueError:
317
- pass # malformed last line -- ignore
341
+ # Tail: last stage event to the last timed line of the file
342
+ last = max((o for o in offsets if o is not None), default=current)
343
+ passed_epochs = int((last - current) / 30)
344
+ fill_val = -1 if stage == 7 else stage
345
+ stages.extend([fill_val] * max(passed_epochs, 0))
318
346
 
319
347
  return np.array(stages, dtype=np.int16)
320
348
 
@@ -324,11 +352,136 @@ class WSCDataset(BasePhysioDataset):
324
352
 
325
353
  def _read_subject_events(self, spec):
326
354
  """Parse non-stage events from WSC .allscore.txt annotation files."""
327
- if spec.label_path is None or not str(spec.label_path).endswith(
328
- ".allscore.txt"
329
- ):
355
+ if spec.label_path is None:
330
356
  return []
331
- return self._parse_allscore_events(spec.label_path)
357
+ label = str(spec.label_path)
358
+ events = self._read_subject_events_unfiltered(spec, label)
359
+ if not events:
360
+ return events
361
+ # A few EDFs are cuts of a longer recording: events past the end of the signal are dropped.
362
+ import pyedflib
363
+
364
+ with pyedflib.EdfReader(str(spec.edf_path)) as f:
365
+ end = float(f.getFileDuration())
366
+ return [e for e in events if e.onset_sec <= end]
367
+
368
+ def _read_subject_events_unfiltered(self, spec, label):
369
+ if label.endswith(".allscore.txt"):
370
+ return self._parse_allscore_events(spec.label_path)
371
+ if label.endswith(".stg.txt"):
372
+ # Gamma-system nights (2000-2009): events are in the ``.sco.txt`` next to the ``.stg.txt``
373
+ sco = Path(label[: -len(".stg.txt")] + ".sco.txt")
374
+ if sco.exists():
375
+ import pyedflib
376
+
377
+ # "Scan # x2" counts samples at twice the EDF rate (200/s for the 100 Hz
378
+ # files, 256/s for the 128 Hz ones); checked against the clock column.
379
+ with pyedflib.EdfReader(str(spec.edf_path)) as f:
380
+ scans_per_sec = 2.0 * float(f.getSampleFrequency(0))
381
+ start = f.getStartdatetime()
382
+ start_sec = start.hour * 3600 + start.minute * 60 + start.second
383
+ return self._parse_sco_events(sco, scans_per_sec, start_sec)
384
+ return []
385
+
386
+ @staticmethod
387
+ def _sco_event(text: str):
388
+ """Map the ``Marker Text`` of a ``.sco.txt`` row to ``(type, concept)``; ``None`` to skip it."""
389
+ t = text.strip().lower()
390
+ if not t:
391
+ return None
392
+ if "arousal" in t or t in ("respa", "spona"):
393
+ if t.startswith("resp"):
394
+ return "arousal", "Respiratory arousal"
395
+ if t.startswith("spon"):
396
+ return "arousal", "Spontaneous arousal"
397
+ return "arousal", "Arousal"
398
+ if "hypopnea" in t:
399
+ return "respiratory", "Central hypopnea" if t.startswith("central") else "Hypopnea"
400
+ if "apnea" in t or t == "oa":
401
+ if t.startswith(("obs", "oa")):
402
+ return "respiratory", "Obstructive apnea"
403
+ if t.startswith("central"):
404
+ return "respiratory", "Central apnea"
405
+ if t.startswith("mixed"):
406
+ return "respiratory", "Mixed apnea"
407
+ return "respiratory", "Apnea"
408
+ if t == "ca":
409
+ return "respiratory", "Central apnea"
410
+ if t == "ma":
411
+ return "respiratory", "Mixed apnea"
412
+ if t == "sao2":
413
+ return "desaturation", "SaO2 desaturation"
414
+ if t.startswith("plm"):
415
+ return "limb_movement", "Periodic limb movement"
416
+ if t in ("lma", "lm a"):
417
+ return "limb_movement", "Limb movement with arousal"
418
+ if t == "lm":
419
+ return "limb_movement", "Limb movement"
420
+ if t.startswith("bad ") or "artifact" in t:
421
+ return "artifact", text.strip()
422
+ return None
423
+
424
+ @staticmethod
425
+ def _parse_sco_events(
426
+ path: Path, scans_per_sec: float = 200.0, start_clock_sec: Optional[float] = None
427
+ ):
428
+ """Parse the event rows of a WSC ``.sco.txt`` (Gamma system, 2000-2009).
429
+
430
+ Columns: ``Epoch#, Scan # x2, Length (Scanx2), Marker Code, Marker Text,
431
+ Channel #, Time, Value, Drop, Length``. Onset and duration are scans divided
432
+ by ``scans_per_sec`` (the length is not measured for limb movements and arousals; a negative
433
+ length means the value was written 100 times too large). Rows without a marker code are padding.
434
+
435
+ A few files write the scan columns with two extra digits. When ``start_clock_sec``
436
+ (clock time of the EDF start, in seconds) is given, the ``Time`` column is used as a
437
+ check and, where it disagrees with the scan by more than 5 s, it gives the onset.
438
+ """
439
+ from physioex.data.events import SleepEvent
440
+
441
+ rate = float(scans_per_sec)
442
+ events = []
443
+ with open(path, encoding="latin1") as f:
444
+ rows = f.read().splitlines()[1:]
445
+ for line in rows:
446
+ parts = [x.strip() for x in line.split("\t")]
447
+ if len(parts) < 7 or not parts[3] or not parts[1]:
448
+ continue
449
+ kind = WSCDataset._sco_event(parts[4])
450
+ if kind is None:
451
+ continue
452
+ try:
453
+ onset = float(parts[1]) / rate
454
+ length = float(parts[2]) if parts[2] else 0.0
455
+ # Some files store the length with two extra digits and a negative sign
456
+ duration = length / rate if length >= 0 else -length / (rate * 100.0)
457
+ except ValueError:
458
+ continue
459
+ if start_clock_sec is not None:
460
+ try:
461
+ h, m, sec = (float(x) for x in parts[6].split(":")[:3] + ["0"] * (3 - len(parts[6].split(":"))))
462
+ clock = (h * 3600 + m * 60 + sec - start_clock_sec) % 86400
463
+ except ValueError:
464
+ clock = None
465
+ if clock is not None and abs(onset - clock) > 5.0:
466
+ scale = 100.0 if onset > 50 * max(clock, 1.0) else 1.0 # the "two extra digits" files
467
+ onset, duration = clock, duration / scale
468
+ extra = {}
469
+ if kind[0] == "desaturation":
470
+ for key, idx in (("spO2_min", 7), ("spO2_drop", 8)):
471
+ try:
472
+ extra[key] = float(parts[idx])
473
+ except (ValueError, IndexError):
474
+ pass
475
+ events.append(
476
+ SleepEvent(
477
+ type=kind[0],
478
+ concept=kind[1],
479
+ onset_sec=onset,
480
+ duration_sec=duration,
481
+ extra=extra,
482
+ )
483
+ )
484
+ return events
332
485
 
333
486
  @staticmethod
334
487
  def _parse_allscore_events(path: Path):
@@ -344,26 +497,10 @@ class WSCDataset(BasePhysioDataset):
344
497
  with open(path, encoding="latin1") as f:
345
498
  lines = f.readlines()
346
499
 
347
- # Find START RECORDING time to compute absolute offsets
348
- start_time = None
349
- for line in lines:
350
- if "START RECORDING" in line:
351
- time_str = line.split("\t")[0]
352
- start_time = datetime.datetime.strptime(time_str, "%H:%M:%S.%f")
353
- break
354
-
355
- if start_time is None:
500
+ offsets = WSCDataset._allscore_offsets(lines)
501
+ if offsets is None:
356
502
  return []
357
503
 
358
- def _to_seconds(time_str: str) -> float:
359
- """Convert a time string to seconds from recording start."""
360
- t = datetime.datetime.strptime(time_str.strip(), "%H:%M:%S.%f")
361
- # Handle midnight crossing: hours <= 12 assumed next day
362
- if t.hour <= 12:
363
- t += datetime.timedelta(days=1)
364
- diff = t - start_time
365
- return diff.total_seconds()
366
-
367
504
  # Patterns for event lines
368
505
  dur_pattern = re.compile(r"DUR:\s*([\d.]+)\s*SEC", re.IGNORECASE)
369
506
  desat_pattern = re.compile(r"DESAT\s+([\d.]+)\s*%", re.IGNORECASE)
@@ -372,7 +509,9 @@ class WSCDataset(BasePhysioDataset):
372
509
  )
373
510
 
374
511
  events = []
375
- for line in lines:
512
+ for line, onset in zip(lines, offsets):
513
+ if onset is None:
514
+ continue
376
515
  parts = line.split("\t")
377
516
  if len(parts) < 2:
378
517
  continue
@@ -385,14 +524,6 @@ class WSCDataset(BasePhysioDataset):
385
524
  if "LIGHTS OFF" in content or "LIGHTS ON" in content:
386
525
  continue
387
526
 
388
- try:
389
- onset = _to_seconds(time_str)
390
- except (ValueError, TypeError):
391
- continue
392
-
393
- if onset < 0:
394
- continue
395
-
396
527
  # Extract duration if present
397
528
  dur_match = dur_pattern.search(content)
398
529
  duration = float(dur_match.group(1)) if dur_match else 0.0
@@ -62,6 +62,10 @@ def infer_channel_modality(channel_name: str, hint: Optional[str] = None) -> Mod
62
62
  u = n.upper()
63
63
  c = re.sub(r"[-_\s.()#/]", "", u)
64
64
 
65
+ # Electrode-off status flags (MESA "EEG1_Off", "EOG-L_Off", ...) are 1 Hz indicators, not signals.
66
+ if re.search(r"[_\s-]OFF$", u):
67
+ return ModalityType.OTHER
68
+
65
69
  # ── EEG ──
66
70
  if (
67
71
  c.startswith("EEG")
@@ -115,3 +115,13 @@ def test_empty_channel_name_is_device():
115
115
  # Empty/whitespace collapses to "" which the DEVICE bucket claims.
116
116
  assert infer_channel_modality("") == ModalityType.DEVICE
117
117
  assert infer_channel_modality(" ") == ModalityType.DEVICE
118
+
119
+
120
+ @pytest.mark.parametrize("name", ["EEG1_Off", "EEG3_Off", "EOG-L_Off", "EOG-R_Off", "EMG_Off", "EKG_Off"])
121
+ def test_electrode_off_flags_are_other(name):
122
+ assert infer_channel_modality(name) == ModalityType.OTHER
123
+
124
+
125
+ def test_off_rule_does_not_touch_signals():
126
+ assert infer_channel_modality("EEG1") == ModalityType.EEG
127
+ assert infer_channel_modality("EOG-L") == ModalityType.EOG
@@ -41,6 +41,8 @@ def make_alzheimers_subject(
41
41
  stages: list = None,
42
42
  channel_names=None,
43
43
  duration_sec: float = 150.0,
44
+ fs: int = 200,
45
+ onset_sec: int = 0,
44
46
  ):
45
47
  """Create a fake Alzheimer's subject: EDF + TSV with comment headers."""
46
48
  if channel_names is None:
@@ -58,14 +60,14 @@ def make_alzheimers_subject(
58
60
  n_channels=len(channel_names),
59
61
  duration_sec=duration_sec,
60
62
  channel_names=channel_names,
61
- fs_per_channel=[200] * len(channel_names),
63
+ fs_per_channel=[fs] * len(channel_names),
62
64
  )
63
65
 
64
66
  tsv_path = data_dir / f"{subj_id}_r1_a1.tsv"
65
- _write_alzheimers_tsv(tsv_path, stages, subj_id=subj_id)
67
+ _write_alzheimers_tsv(tsv_path, stages, subj_id=subj_id, onset_sec=onset_sec)
66
68
 
67
69
 
68
- def _write_alzheimers_tsv(path: Path, stages: list, subj_id: str = "AD008"):
70
+ def _write_alzheimers_tsv(path: Path, stages: list, subj_id: str = "AD008", onset_sec: int = 0):
69
71
  """Write a fake Alzheimer's TSV with comment header lines."""
70
72
  with open(path, "w") as f:
71
73
  f.write(f"# Source: Alzheimer disease Database UZ Leuven\n")
@@ -75,7 +77,7 @@ def _write_alzheimers_tsv(path: Path, stages: list, subj_id: str = "AD008"):
75
77
  f.write(f"# Offset wrt orignal PSG: 900\n")
76
78
  f.write(f"#Start & end of sleep annotations: 40830.02- 78564.14 \n")
77
79
  f.write(f"\n") # blank line after comments
78
- onset = 40860
80
+ onset = onset_sec
79
81
  for stage in stages:
80
82
  end = onset + 30
81
83
  f.write(f"{onset}\t{end}\t{stage}\t\n")
@@ -266,9 +268,8 @@ def test_alz_tsv_parsing():
266
268
  )
267
269
  spec = ds._subjects[0]
268
270
  labels = ds._read_subject_labels(spec)
269
- # Labels are aligned by absolute onset epoch (onset 40860s / 30s =
270
- # epoch 1362): pre-sleep epochs are -1, the 5 scored epochs carry the
271
- # stages. Assert the scored (non-masked) sequence, robust to the offset.
271
+ # Labels are aligned by absolute onset epoch; unscored epochs are -1.
272
+ # Assert the scored (non-masked) sequence.
272
273
  scored = [int(x) for x in labels if x != -1]
273
274
  report("ALZ TSV parsing (onset-aligned scored epochs)",
274
275
  scored == [0, 1, 2, 3, 4],
@@ -751,3 +752,16 @@ def test_real_data_smoke():
751
752
  report(f"{name} channels probe", False, f"SKIP: {e}")
752
753
 
753
754
 
755
+
756
+
757
+ def test_alz_labels_aligned_by_onset_and_cut_at_signal_end():
758
+ with tempfile.TemporaryDirectory() as data_dir, tempfile.TemporaryDirectory() as cache_dir:
759
+ data_dir = Path(data_dir)
760
+ # 1 Hz EDF of 600 s (20 epochs); 5 scored epochs from 480 s (epoch 16): epochs 16..19 fit, epoch 20 is past the signal
761
+ make_alzheimers_subject(data_dir, "AD001", stages=["Wake", "S1", "S2", "S3", "REM"],
762
+ duration_sec=600.0, fs=1, onset_sec=480)
763
+ ds = AlzheimersDataset(root=str(data_dir), channels=["EEG"], pipelines="raw", sequence_length=1, cache_dir=cache_dir)
764
+ labels = ds._read_subject_labels(ds._subjects[0])
765
+ assert len(labels) == 20
766
+ assert list(labels[:16]) == [-1] * 16
767
+ assert list(labels[16:]) == [0, 1, 2, 3]
@@ -205,3 +205,14 @@ def test_stages_spo2_artifact_is_its_own_type(tmp_path):
205
205
  by = {e.concept: e.type for e in events}
206
206
  assert by["SpO2 artifact"] == "artifact"
207
207
  assert by["Desaturation"] == "desaturation"
208
+
209
+
210
+ def test_label_readers_fixed_in_the_dataset_audit_have_a_new_revision():
211
+ """Caches written by the old readers (shifted WSC labels, SleepEDF tail, truncated Alzheimer EDFs) are recomputed."""
212
+ from physioex.data.datasets.alzheimers import AlzheimersDataset
213
+ from physioex.data.datasets.sleepedf import SleepEDFDataset
214
+ from physioex.data.datasets.wsc import WSCDataset
215
+
216
+ assert SleepEDFDataset.LABELS_REVISION >= 2
217
+ assert WSCDataset.LABELS_REVISION >= 2
218
+ assert AlzheimersDataset.LABELS_REVISION >= 3
@@ -236,6 +236,20 @@ def test_sleepedf_label_parsing():
236
236
  )
237
237
 
238
238
 
239
+ def test_sleepedf_labels_cut_at_end_of_signal():
240
+ """Hypnogram longer than the PSG (Sleep-EDF runs to 24 h): labels stop where the signal stops."""
241
+ stages = ["Sleep stage W"] * 4 + ["Sleep stage 2"] * 4 + ["Sleep stage ?"] * 3 + ["Sleep stage W"] * 5 # 16 epochs of labels
242
+ with tempfile.TemporaryDirectory() as data_dir, tempfile.TemporaryDirectory() as cache_dir:
243
+ data_dir = Path(data_dir)
244
+ make_sleepedf_subject(data_dir, "SC4001E0-PSG.edf", "SC4001EC-Hypnogram.edf", stages=stages)
245
+ write_fake_edf(data_dir / "SC4001E0-PSG.edf", n_channels=3, duration_sec=10 * 30.0, fs_per_channel=[100] * 3,
246
+ channel_names=["EEG Fpz-Cz", "EOG horizontal", "EMG submental"]) # signal of 10 epochs
247
+ ds = SleepEDFDataset(root=str(data_dir), channels=["EEG"], pipelines="raw", sequence_length=5, cache_dir=cache_dir)
248
+ labels = ds._read_subject_labels(ds._subjects[0])
249
+ assert len(labels) == 10
250
+ assert list(labels) == [0] * 4 + [2] * 4 + [-1] * 2
251
+
252
+
239
253
  def test_sleepedf_end_to_end():
240
254
  """6. ds[0] returns a valid dict with expected keys and shapes."""
241
255
  stages = ["Sleep stage W"] * 5 + ["Sleep stage R"] * 5