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.
- {physioex-2.0.3 → physioex-2.0.4}/CHANGELOG.md +21 -0
- {physioex-2.0.3 → physioex-2.0.4}/PKG-INFO +1 -1
- physioex-2.0.4/physioex/__init__.py +1 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/alzheimers.py +9 -2
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/sleepedf.py +7 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/wsc.py +200 -69
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/modality.py +4 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_modality.py +10 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_alzheimers_parkinsons.py +21 -7
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_data_layer_hardening.py +11 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_phase_b_datasets.py +14 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_wsc_dataset.py +156 -0
- physioex-2.0.3/physioex/__init__.py +0 -1
- {physioex-2.0.3 → physioex-2.0.4}/.claudeignore +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.github/copilot-instructions.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/black.yml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/ci.yml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/publish.yaml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.github/workflows/test.yml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/.gitignore +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/LICENSE +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/MANIFEST.in +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/README.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/REPO_MAP.txt +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/_ext/related_works.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/_static/custom.css +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/api/index.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/data/sequence_viz.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo.psd +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo.svg +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/assets/images/logo_bar.svg +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/conf.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/index.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/data.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/explain.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/models.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/overview.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/library/train.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/architecture/testing/overview.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/contribute.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/data.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/explain/explain.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/preprocess.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/cli.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/networks/chambon2018.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/networks.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/train/train.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/unavailable.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/pages/user_guide.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/related/_thumbs/_placeholder.svg +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/related/fetch_citations.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/related/index.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/docs/related/works.yaml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/compare_strategies.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/extract_all_concepts.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/report.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/utils.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/analyze_csd_concepts/visualize_concepts.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/README.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/explain_csd.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/train_probe.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/utils.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/conceptualspectraldecomposition/visualize.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/explain/lrp_seqsleepnet_n3.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/bendr/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/biot/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/cbramod/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/labram/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/neurolm/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/reve/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/sjepa/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/sleepfm/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/foundation/tfc/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/extract_embeddings.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/smoke.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/chambon2018/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/coresleep-kontras/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/coresleep-kontras/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/smoke.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/test_pretrained.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/lseqsleepnet-phan/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/protosleepnet/README.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/protosleepnet/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/extract_embeddings.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_pretrained.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/test_stages_sites.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train_shhs.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/seqsleepnet-phan/train_shhs.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/extract_embeddings.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/smoke.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/test_pretrained.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/sleeptransformer-phan/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/extract_embeddings.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/smoke.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/test_pretrained.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tinysleepnet-supratak/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/extract_embeddings.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/quickstart.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/smoke.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/test_pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/examples/pretrained/tsinalis-2016/train.sbatch +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/notebook.ipynb +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_barplot.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell11_tiny_hypnogram.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_barplot.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell13_seq_hypnogram.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell17_ig_tiny.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell19_ig_seq.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell19b_attr_vs_psd.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell5_raw_n3_psd.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/outputs/cell7_spec_n3_psd.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/run_notebook.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/script.pdf +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/slides.pdf +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/2026-06-24_ugent_xai/util.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/README.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/kuleuven_logo.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/physioex_logo.png +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/lectures/assets/physioex_logo.svg +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/base.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/cache.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/collate.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datareader.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/dataset.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/_nsrr.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/_splits/shhs.json +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/ccshs.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/channels_table.md +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/dcsm.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/hmc.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/hpap.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mass.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mesa.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/mros.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/parkinsons.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/shhs.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/datasets/stages.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/events.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/multi.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/pipeline.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/presets.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/annotations.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/readers/edf.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/sampling.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/bandpass.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/highpass.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/identity.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/normalize.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/notch.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/resample.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/data/steps/spectrogram.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/csd.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/multichannel_sg.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/report.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/sleep_bands.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/foundational/specificity.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/_functional.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/_rules.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/attributor.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/canonizers.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/composites.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/diagnostics.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/model.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/pooling.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/recurrent.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/lrp/transformer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/filters.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/functionizer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/gradients.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/metrics/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/spectralgradients.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/vidft.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/posthoc/vistdft.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/local.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/nmf.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/utils.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/posthoc/vq.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/explain/prototypes/reconstruct.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/bendr.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/biot.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/cbramod.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/chambon2018.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/coresleep.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/embed.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_base.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_channel_mapping.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_checkpoints.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_datasets.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/foundation_preproc.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/labram.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/lseqsleepnet.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/neurolm.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/pretrained.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/protosleepnet.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/reve.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/seqsleepnet.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sjepa.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleep_tokenizer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleepfm.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/sleeptransformer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tfc.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tinysleepnet.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/models/tsinalis.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/_common.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/finetune.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/test.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/bin/train.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/logger.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/metrics.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/check_table.csv +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/models/load.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/multidevicetrainer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/progress.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/stats.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/physioex/train/trainer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/pyproject.toml +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/conftest.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_base_smoke.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_multi.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/data/test_readers_annotations.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/foundational/test_sleep_bands.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/foundational/test_specificity.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_functional.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_lrp.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_model.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_real_models.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_recurrent.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/lrp/test_transformer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_gradients.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_metrics.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_spectralgradients.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_vidft.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/posthoc/test_vistdft.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/explain/prototypes/posthoc/test_nmf_vq.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/factories/__init__.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/factories/edf.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_architectures.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_base.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_encoders.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_foundation_preproc.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_pretrained_embed_helpers.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/models/test_sleep_tokenizer.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_api_surface.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_cache.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_cache_enabled.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_ccshs.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_channel_acc_tracking.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_channel_resolution.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_cli_workflows.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_dataset_config.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_e2e_smoke.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_events.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_explain_fixes.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_label_hardening.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_load_model.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_mass_dataset.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_metrics_and_packaging.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_modality_dropout.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_phase_c_datasets.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_pipeline.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_presets.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_raw_dataset_integration.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_reading_modes.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_sampling.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_stages_dataset.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_enhancements.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_fixes.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_trainer_integration.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/test_wake_trimming.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/train/test_logger.py +0 -0
- {physioex-2.0.3 → physioex-2.0.4}/tests/train/test_progress.py +0 -0
- {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
|
+
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
+
labels = self._parse_allscore(path)
|
|
203
205
|
elif suffix.endswith(".stg.txt"):
|
|
204
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
339
|
+
current = max(off, current)
|
|
305
340
|
|
|
306
|
-
#
|
|
307
|
-
if
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
328
|
-
".allscore.txt"
|
|
329
|
-
):
|
|
355
|
+
if spec.label_path is None:
|
|
330
356
|
return []
|
|
331
|
-
|
|
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
|
-
|
|
348
|
-
|
|
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=[
|
|
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 =
|
|
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
|
|
270
|
-
#
|
|
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
|