diff --git a/.gitignore b/.gitignore index b7faf40..2a2f5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -98,7 +98,7 @@ ipython_config.py # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. -#uv.lock +uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. @@ -205,3 +205,12 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ + +# Agent scratch +.claude/ + +# Data: ship ONLY the curated demo artifact; never raw downloads or transient outputs. +data/_raw_labels/ +data/* +!data/README.md +!data/sleepaccel_demo.npz diff --git a/.zenodo.json b/.zenodo.json new file mode 100644 index 0000000..e6fa712 --- /dev/null +++ b/.zenodo.json @@ -0,0 +1,28 @@ +{ + "title": "StageGuard: Physiologically Constrained Sleep Staging", + "description": "Official implementation of the KDD 2026 AI4Sciences paper 'StageGuard: Physiologically Constrained Sleep Staging'. StageGuard is a plug-and-play, backbone-agnostic structured-inference framework that wraps any neural sleep-staging backbone with physiology-informed priors: a differentiable soft transition penalty during training and a semi-Markov constrained decoder at inference. The repository bundles a small subset of Sleep-Accel PSG hypnograms (labels only) under the ODC-By license for the demo notebook.", + "license": "MIT", + "upload_type": "software", + "access_right": "open", + "creators": [ + {"name": "Wang, Juntang", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"}, + {"name": "Wang, Yihan", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"}, + {"name": "Wu, Hao", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"}, + {"name": "Gao, Jiayu", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"}, + {"name": "Xu, Shixin", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"}, + {"name": "Zou, Dongmian", "affiliation": "Zu Chongzhi Center, Duke Kunshan University"} + ], + "keywords": [ + "sleep staging", + "physiological constraints", + "semi-Markov model", + "temporal consistency", + "constrained decoding", + "deep learning", + "KDD 2026" + ], + "related_identifiers": [ + {"identifier": "10.1145/3770855.3818916", "relation": "isSupplementTo", "scheme": "doi"}, + {"identifier": "10.13026/hmhs-py35", "relation": "references", "scheme": "doi"} + ] +} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..1b4d0c0 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,67 @@ +cff-version: 1.2.0 +message: "If you use StageGuard in your research, please cite the KDD 2026 paper below." +title: "StageGuard: Physiologically Constrained Sleep Staging" +type: software +license: MIT +repository-code: "https://github.com/Q9gJYx/StageGuard" +url: "https://doi.org/10.1145/3770855.3818916" +# doi: 10.5281/zenodo.XXXXXXXX # add the archival Zenodo DOI after the release is deposited +abstract: >- + StageGuard is a plug-and-play, backbone-agnostic structured-inference framework that wraps any neural + sleep-staging model with physiology-informed priors: a differentiable soft transition penalty during + training and a semi-Markov constrained decoder at inference. +keywords: + - sleep staging + - physiological constraints + - semi-Markov model + - temporal consistency + - constrained decoding + - deep learning +authors: + - family-names: Wang + given-names: Juntang + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "jw853@duke.edu" + - family-names: Wang + given-names: Yihan + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "yw572@duke.edu" + - family-names: Wu + given-names: Hao + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "hw412@duke.edu" + - family-names: Gao + given-names: Jiayu + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "jg563@duke.edu" + - family-names: Xu + given-names: Shixin + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "shixin.xu@duke.edu" + - family-names: Zou + given-names: Dongmian + affiliation: "Zu Chongzhi Center, Duke Kunshan University" + email: "dongmian.zou@duke.edu" +preferred-citation: + type: conference-paper + title: "StageGuard: Physiologically Constrained Sleep Staging" + year: 2026 + collection-title: "Proceedings of the 32nd ACM SIGKDD Conference on Knowledge Discovery and Data Mining V.2 (KDD '26)" + doi: "10.1145/3770855.3818916" + conference: + name: "ACM SIGKDD Conference on Knowledge Discovery and Data Mining (KDD)" + city: "Jeju Island" + country: "KR" + authors: + - family-names: Wang + given-names: Juntang + - family-names: Wang + given-names: Yihan + - family-names: Wu + given-names: Hao + - family-names: Gao + given-names: Jiayu + - family-names: Xu + given-names: Shixin + - family-names: Zou + given-names: Dongmian diff --git a/LICENSE b/LICENSE index bc70771..916c585 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Juntang +Copyright (c) 2026 Juntang Wang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 021ca38..0acf60f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,45 @@ -# StageGuard +# [KDD 2026 AI4Sci] StageGuard: Physiologically Constrained Sleep Staging **Backbone-agnostic physiological constraints for neural sleep staging.** -StageGuard improves any neural sleep staging model by enforcing physiological constraints during both training (soft transition penalty) and inference (Semi-Markov constrained decoding). It works across modalities—EEG, actigraphy, cardiorespiratory signals, and bioradar—with modality-specific configurations. +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Q9gJYx/StageGuard/blob/main/notebooks/demo_stageguard.ipynb) +[![KDD 2026](https://img.shields.io/badge/KDD-2026%20AI4Sci-1d4ed8.svg)](https://doi.org/10.1145/3770855.3818916) +[![DOI](https://img.shields.io/badge/DOI-10.1145%2F3770855.3818916-blue.svg)](https://doi.org/10.1145/3770855.3818916) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org) +[![PyTorch](https://img.shields.io/badge/PyTorch-2.0%2B-ee4c2c.svg)](https://pytorch.org) + + +Official implementation of the **KDD 2026 AI4Sciences** paper **"StageGuard: Physiologically Constrained +Sleep Staging"** by Juntang Wang, Yihan Wang, Hao Wu, Jiayu Gao, Shixin Xu, and Dongmian Zou (Zu Chongzhi +Center, Duke Kunshan University). Equal contribution: Juntang Wang, Yihan Wang, Hao Wu, Jiayu Gao. +Corresponding author: Dongmian Zou (`dongmian.zou@duke.edu`). + +> Automated sleep staging is increasingly used in large-scale studies and downstream scientific analyses to +> derive sleep-architecture endpoints, including total sleep time, REM latency, sleep efficiency, and +> bout-duration statistics. Deep learning models achieve epoch-level accuracy approaching inter-rater +> agreement, yet often produce hypnograms that violate physiological invariants, such as rare state +> transitions (e.g., direct Wake to REM) or excessively fragmented sequences. Such violations can bias +> downstream sleep metrics, regardless of overall accuracy. We propose StageGuard, a plug-and-play, +> backbone-agnostic structured-inference framework that wraps any neural sleep-staging backbone with +> physiology-informed priors. StageGuard combines (1) a differentiable soft transition penalty that +> discourages physiologically rare transitions during training, and (2) a semi-Markov constrained decoder +> with a duration-augmented state space that jointly enforces transition penalties and minimum bout durations +> at inference. Across six backbones and four datasets, StageGuard reduces the transition-violation rate to +> physiologically plausible levels and lowers the fragmentation index by 56 to 62%, while maintaining or +> slightly improving classification accuracy, and improves the accuracy of derived sleep-architecture +> statistics that are not directly optimized by the method. + +## News + +- **2026** Accepted to KDD 2026 (AI4Sciences track), Jeju Island, Republic of Korea (Aug 9 to 13, 2026). ## Key Contributions -1. **Soft Transition Penalty** — A differentiable loss term that discourages physiologically rare stage transitions (e.g., Wake↔REM) during training. -2. **Semi-Markov Decoder** — An augmented Viterbi decoder that enforces minimum bout durations, penalizes rare transitions, and suppresses flip-flop artifacts at inference time. -3. **Signal Quality Integration** — Per-epoch signal quality scores modulate decoder confidence, gracefully degrading predictions for noisy segments. +1. **Soft Transition Penalty** - A differentiable loss term that discourages physiologically rare stage transitions (e.g., Wake to/from REM) during training. +2. **Semi-Markov Decoder** - An augmented Viterbi decoder that enforces minimum bout durations, penalizes rare transitions, and suppresses flip-flop artifacts at inference time. +3. **Signal Quality Integration** - Per-epoch signal quality scores modulate decoder confidence, gracefully degrading predictions for noisy segments. ## Installation @@ -19,8 +50,22 @@ conda activate stageguard # Or with pip pip install -e . + +# Optional: notebook/plotting extras +pip install -e ".[demo]" ``` +## Demo + +Run the interactive demo on Colab (no install, a few seconds on CPU): + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Q9gJYx/StageGuard/blob/main/notebooks/demo_stageguard.ipynb) + +It loads real Sleep-Accel PSG hypnograms (shipped under ODC-By), simulates a noisy backbone, and shows the +semi-Markov decoder driving the transition-violation rate (TVR) and fragmentation index (FI) down while +holding or improving accuracy, with a before/after hypnogram plot. See +[`notebooks/demo_stageguard.ipynb`](notebooks/demo_stageguard.ipynb). + ## Quick Start ```python @@ -60,55 +105,64 @@ class MyBackbone(nn.Module): model = StageGuardWrapper(MyBackbone(), config) ``` +See [`docs/backbones.md`](docs/backbones.md) for the full interface contract and the `ModalityConfig` schema. + ## Repository Structure ``` StageGuard/ -├── configs/ # Modality-specific YAML configurations -│ ├── mouse_eeg.yaml # AccuSleep: 3-state, 4s epochs -│ ├── actigraphy.yaml # Sleep-Accel: 2-state, 30s epochs -│ ├── cardiorespiratory.yaml # SHHS: 3-state, 30s epochs -│ └── bioradar.yaml # SLEEPBRL: 3-state, 30s epochs +├── configs/ # Modality-specific YAML configurations +│ ├── mouse_eeg.yaml # AccuSleep: 3-state, 4s epochs +│ ├── actigraphy.yaml # Sleep-Accel: 2-state, 30s epochs +│ ├── cardiorespiratory.yaml # SHHS: 3-state, 30s epochs +│ ├── bioradar.yaml # SLEEPBRL: 3-state, 30s epochs +│ └── sleepaccel_demo.yaml # 3-state Wake/NREM/REM config used by the demo ├── stageguard/ -│ ├── config.py # ModalityConfig dataclass + YAML loader -│ ├── losses.py # SoftTransitionPenalty + stageguard_loss -│ ├── decoder.py # SemiMarkovDecoder (augmented Viterbi) -│ ├── wrapper.py # StageGuardWrapper (backbone + loss + decoder) -│ ├── metrics.py # TVR, FI, accuracy, kappa, F1, sleep architecture -│ ├── sqi.py # Signal quality index per modality -│ ├── backbones/ # Backbone implementations + registry -│ │ ├── base.py # BackboneBase ABC -│ │ ├── accusleep.py # 2-layer CNN -│ │ └── usleep.py # U-Net encoder-decoder -│ └── data/ # Dataset loaders (expect pre-downloaded data) -│ ├── base.py # BaseSleepDataset ABC -│ ├── mouse_eeg.py # AccuSleep mouse EEG/EMG -│ ├── actigraphy.py # Sleep-Accel wrist actigraphy -│ ├── cardiorespiratory.py # SHHS cardiorespiratory -│ └── bioradar.py # SLEEPBRL bioradar -├── tests/ # Comprehensive test suite -└── examples/ - └── demo.py # End-to-end synthetic data demo +│ ├── config.py # ModalityConfig dataclass + YAML loader +│ ├── losses.py # SoftTransitionPenalty + stageguard_loss +│ ├── decoder.py # SemiMarkovDecoder (augmented Viterbi) +│ ├── wrapper.py # StageGuardWrapper (backbone + loss + decoder) +│ ├── metrics.py # TVR, FI, accuracy, kappa, F1, sleep architecture +│ ├── sqi.py # Signal quality index per modality +│ ├── backbones/ # Backbone implementations + registry +│ └── data/ # Dataset loaders (expect pre-downloaded data) +├── notebooks/ +│ └── demo_stageguard.ipynb # Colab-runnable demo +├── data/ +│ ├── sleepaccel_demo.npz # small Sleep-Accel PSG hypnogram subset (ODC-By) +│ └── README.md # provenance, license, stage-collapse mapping +├── docs/ +│ ├── datasets.md # dataset access + loader formats +│ └── backbones.md # interface contract + config schema +├── examples/demo.py # end-to-end synthetic data demo +├── tests/ # test suite +├── ROADMAP.md +├── CITATION.cff +└── .zenodo.json ``` ## Datasets -| Dataset | Modality | Classes | Epoch | URL | -|---------|----------|---------|-------|-----| -| AccuSleep | Mouse EEG/EMG | 3 (Wake, NREM, REM) | 4s | [Zenodo](https://zenodo.org/records/4079563) | -| Sleep-Accel | Wrist actigraphy | 2 (Wake, Sleep) | 30s | [PhysioNet](https://physionet.org/content/sleep-accel/1.0.0/) | -| SHHS | Cardiorespiratory | 3 (Wake, Light, Deep) | 30s | [NSRR](https://sleepdata.org/datasets/shhs) | +| Dataset | Modality | Classes | Epoch | Access | +|---------|----------|---------|-------|--------| +| AccuSleep | Mouse EEG/EMG | 3 (Wake, NREM, REM) | 4s | Open ([Zenodo](https://zenodo.org/records/4079563)) | +| Sleep-Accel | Wrist actigraphy | 2 (Wake, Sleep) | 30s | Open, ODC-By ([PhysioNet](https://physionet.org/content/sleep-accel/1.0.0/)) | +| SHHS | Cardiorespiratory | 3 (Wake, Light, Deep) | 30s | Data-use agreement ([NSRR](https://sleepdata.org/datasets/shhs)) | | SLEEPBRL | Bioradar | 3 (Wake, Light, Deep) | 30s | Contact authors | -> **Note:** Datasets must be downloaded separately. Some require data use agreements. Each loader provides download instructions via `Dataset.download_instructions()`. +Datasets must be downloaded separately; some require data use agreements. Each loader provides download +instructions via `Dataset.download_instructions()`. See [`docs/datasets.md`](docs/datasets.md) for the +expected on-disk formats. The demo notebook uses a small, redistributable subset of Sleep-Accel PSG +hypnograms shipped at [`data/sleepaccel_demo.npz`](data/sleepaccel_demo.npz) (see +[`data/README.md`](data/README.md)). ## Hyperparameters | Parameter | Symbol | Default | Description | |-----------|--------|---------|-------------| -| `lambda_trans` | λ | 1.0 | Transition penalty weight | -| `epsilon` | ε | 5.0 | Rare transition penalty in decoder | -| `gamma` | γ | 2.0 | Anti-flip-flop penalty | +| `lambda_trans` | lambda | 1.0 | Transition penalty weight | +| `epsilon` | epsilon | 5.0 | Rare transition penalty in decoder | +| `gamma` | gamma | 2.0 | Anti-flip-flop penalty | | `k` | k | 5 | Flip-flop lookback window (epochs) | | `d_min` | d_min | per-stage | Minimum bout duration (epochs) | | `d_max` | d_max | 30 | Maximum tracked duration (epochs) | @@ -127,6 +181,30 @@ pytest tests/test_decoder.py # Single module python examples/demo.py ``` +## Acknowledgments + +We thank the authors of the datasets used in this work: AccuSleep (Barger et al., 2019), Sleep-Accel (Walch +et al., 2019, distributed via PhysioNet under ODC-By), SHHS (Quan et al., 1997, via the NSRR), and SLEEPBRL +(Tataraidze et al., 2015). The reference backbones reimplement architectures from AccuSleep (Barger et al., +2019) and U-Sleep (Perslev et al., 2021). + +## Citation + +If you use StageGuard in your research, please cite: + +```bibtex +@inproceedings{wang2026stageguard, + title = {StageGuard: Physiologically Constrained Sleep Staging}, + author = {Wang, Juntang and Wang, Yihan and Wu, Hao and Gao, Jiayu and Xu, Shixin and Zou, Dongmian}, + booktitle = {Proceedings of the 32nd ACM SIGKDD Conference on Knowledge Discovery and Data Mining V.2 (KDD '26)}, + year = {2026}, + doi = {10.1145/3770855.3818916} +} +``` + +A machine-readable [CITATION.cff](CITATION.cff) is also provided, which GitHub renders into a "Cite this +repository" button on the project page. + ## License -MIT +Released under the MIT License. See [LICENSE](LICENSE) for details. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..748d1a7 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,33 @@ +# Roadmap + +This file tracks planned work beyond the KDD 2026 camera-ready snapshot (`v1.0.0-kdd2026`). The released +library is the backbone-agnostic StageGuard core (soft transition penalty + semi-Markov decoder + metrics + +SQI) with two reference backbones and a runnable demo. + +## Additional backbone adapters + +The paper evaluates six backbones; two ship here (`accusleep`, `usleep`). Reference adapters for the +remaining four are planned: + +- DeepSleepNet (Supratak et al., 2017) +- SeqSleepNet (Phan et al., 2019) +- AttnSleep (Eldele et al., 2021) +- SleepTransformer (Phan et al., 2022) + +Any of these already works today by wrapping the author's own implementation in `StageGuardWrapper`; the only +requirement is a `(B, T, C)` logits output (see [docs/backbones.md](docs/backbones.md)). + +## Reproduction harness + +An `experiments/` package with per-dataset entry points (`--mode base / +stageguard`) to reproduce the +paper's tables, plus exact-version environment locks and per-run seeds. + +## Packaging and distribution + +- Publish to PyPI (`pip install stageguard`). +- Archive each tagged release on Zenodo for a citable DOI. + +## More modalities and configs + +- Additional `configs/*.yaml` for further datasets and stage schemes. +- Optional helper utilities for converting common PSG label formats into the loaders' expected layouts. diff --git a/configs/sleepaccel_demo.yaml b/configs/sleepaccel_demo.yaml new file mode 100644 index 0000000..7de77b0 --- /dev/null +++ b/configs/sleepaccel_demo.yaml @@ -0,0 +1,33 @@ +# Sleep-Accel (demo) -- wrist-PSG hypnograms, 3-state collapse +# Dataset: https://physionet.org/content/sleep-accel/1.0.0/ (Walch et al. 2019, ODC-By) +# Demo config for notebooks/demo_stageguard.ipynb. Uses the PSG gold-standard +# hypnograms collapsed to 3 states (Wake / NREM / REM) at 30-second epochs. +# NOTE: distinct from configs/actigraphy.yaml (2-state Wake/Sleep) -- this 3-state +# scheme showcases the rare-transition and minimum-duration constraints. + +dataset_name: Sleep-Accel (demo, 3-state) +dataset_url: https://physionet.org/content/sleep-accel/1.0.0/ + +stage_names: [Wake, NREM, REM] +num_classes: 3 +epoch_sec: 30.0 + +# Direct Wake<->REM is physiologically rare in humans +rare_transitions: + - [0, 2] # Wake -> REM + - [2, 0] # REM -> Wake + +lambda_trans: 1.0 + +# Semi-Markov decoder +d_max: 30 +epsilon: 5.0 +gamma: 2.0 +k: 5 +d_min: [1, 2, 2] # Wake >= 1 epoch (30s), NREM >= 2 (60s), REM >= 2 (60s); + # chosen so < 2% of true epochs fall in sub-d_min bouts across + # the shipped demo subjects (see data/README.md) + +# Signal quality +sqi_method: acceleration_variance +sqi_threshold: 0.3 diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..fc620af --- /dev/null +++ b/data/README.md @@ -0,0 +1,109 @@ +# Demo data + +This directory ships a single small artifact, **`sleepaccel_demo.npz`**, used by +[`notebooks/demo_stageguard.ipynb`](../notebooks/demo_stageguard.ipynb). It contains **labels only** +(sleep-stage hypnograms) for 5 subjects — **no raw accelerometer or photoplethysmography signals are +redistributed.** It exists so the notebook can demonstrate StageGuard's constrained decoder end-to-end on +real PSG hypnograms without any download or data-use agreement. + +> StageGuard's full datasets (AccuSleep, Sleep-Accel, SHHS, SLEEPBRL) are described in +> [`docs/datasets.md`](../docs/datasets.md); most must be obtained separately. + +## Provenance + +Derived from the PhysioNet dataset **"Motion and heart rate from a wrist-worn wearable and labeled sleep from +polysomnography"** v1.0.0 (Walch et al.), DOI [`10.13026/hmhs-py35`](https://doi.org/10.13026/hmhs-py35), +. Only the per-subject PSG label files +(`labels/_labeled_sleep.txt`) were used. + +Subjects shipped (PhysioNet IDs): **4426783, 781756, 9961348, 759667, 1066528**. + +## License & attribution + +The source dataset is released under the **Open Data Commons Attribution License v1.0 (ODC-By)**. Per its +terms, this redistributed subset carries the following attribution: + +> Contains information from *Motion and heart rate from a wrist-worn wearable and labeled sleep from +> polysomnography* (PhysioNet, DOI 10.13026/hmhs-py35), which is made available under the +> [ODC Attribution License](https://opendatacommons.org/licenses/by/1.0/). + +Please cite the original work if you use this artifact: + +```bibtex +@article{walch2019sleep, + author = {Walch, Olivia and Huang, Yitong and Forger, Daniel and Goldstein, Cathy}, + title = {Sleep stage prediction with raw acceleration and photoplethysmography heart rate + data derived from a consumer wearable device}, + journal = {Sleep}, + volume = {42}, + number = {12}, + pages = {zsz180}, + year = {2019}, + doi = {10.1093/sleep/zsz180} +} + +@misc{physionet_sleepaccel, + author = {Walch, Olivia}, + title = {Motion and heart rate from a wrist-worn wearable and labeled sleep from polysomnography + (version 1.0.0)}, + year = {2019}, + doi = {10.13026/hmhs-py35}, + note = {PhysioNet} +} + +@article{goldberger2000physionet, + author = {Goldberger, A. L. and Amaral, L. A. N. and Glass, L. and others}, + title = {{PhysioBank, PhysioToolkit, and PhysioNet}}, + journal = {Circulation}, + volume = {101}, + number = {23}, + pages = {e215--e220}, + year = {2000}, + doi = {10.1161/01.CIR.101.23.e215} +} +``` + +## Stage collapse (5-stage -> 3-state) + +The source labels follow AASM/R&K codes. They are collapsed to the 3-state scheme of +[`configs/sleepaccel_demo.yaml`](../configs/sleepaccel_demo.yaml): + +| Source code | Meaning | StageGuard 3-state | +|---|---|---| +| 0 | Wake | `0` Wake | +| 1 | N1 | `1` NREM | +| 2 | N2 | `1` NREM | +| 3 | N3 | `1` NREM | +| 4 | (R&K stage 4, deep) | `1` NREM | +| 5 | REM | `2` REM | +| -1 | unscored / artifact | `0` Wake | + +`-1` is mapped to Wake (not dropped) so that no spurious cross-gap transitions are introduced into the +ground-truth hypnogram. The shipped subjects happen to contain only codes `{0,1,2,3,5}`, but the mapping +handles `4` and `-1` for reproducibility. + +## File schema (`sleepaccel_demo.npz`) + +Saved with per-subject keys (no `allow_pickle` required on load): + +| key | dtype | shape | meaning | +|---|---|---|---| +| `hypno_0` … `hypno_4` | `int64` | `(T_i,)` | collapsed hypnogram per subject; values in `{0,1,2}` | +| `subject_ids` | ` (B, T) +``` + +### Bundled backbones + +| Name (registry key) | Architecture | Reference | +|---|---|---| +| `accusleep` | 2-layer 1D CNN | Barger et al. (2019) | +| `usleep` | U-Net encoder-decoder | Perslev et al. (2021) | + +```python +from stageguard.backbones import get_backbone, register_backbone +backbone = get_backbone("accusleep", num_classes=3, in_channels=1) +register_backbone("my_net", MyBackbone) # add your own to the registry +``` + +The paper evaluates six backbones; the other four (DeepSleepNet, SeqSleepNet, AttnSleep, SleepTransformer) +are on the [roadmap](../ROADMAP.md). Any of them can already be used today by wrapping the author's own +implementation in `StageGuardWrapper`, since the only requirement is `(B, T, C)` output. + +## ModalityConfig schema + +`ModalityConfig` (see `stageguard/config.py`) holds the stage definitions and constraint parameters. Load +from YAML with `ModalityConfig.from_yaml(path)`. Fields: + +| Field | Type | Default | Meaning | +|---|---|---|---| +| `stage_names` | list[str] | (required) | stage labels; `len` must equal `num_classes` | +| `num_classes` | int | (required) | number of stages `C` | +| `rare_transitions` | list[[int,int]] | (required) | `(source, target)` pairs penalized as physiologically rare | +| `lambda_trans` | float | 1.0 | weight of the soft transition penalty in training | +| `d_max` | int | 30 | maximum tracked bout duration (augmented-state size is `C*C*d_max`) | +| `epsilon` | float | 5.0 | decoder penalty for a rare transition | +| `gamma` | float | 2.0 | anti-flip-flop penalty for returning to the previous stage within `k` epochs | +| `k` | int | 5 | flip-flop look-back window (epochs) | +| `d_min` | list[int] | [] | minimum bout duration per stage (epochs); `len` must equal `num_classes` if set | +| `sqi_method` | str | "spectral_entropy" | SQI function name (see `stageguard/sqi.py`) | +| `sqi_threshold` | float | 0.5 | documentary threshold for the modality | +| `epoch_sec` | float | 30.0 | epoch length in seconds | +| `dataset_name`, `dataset_url` | str / None | None | optional metadata | + +## Using the components standalone + +The decoder, metrics, and SQI functions work without the wrapper (they are imported from submodules, not the +top-level package): + +```python +import numpy as np +from stageguard.decoder import SemiMarkovDecoder +from stageguard.metrics import (transition_violation_rate, fragmentation_index, + classification_metrics, sleep_architecture) +from stageguard.sqi import compute_sqi + +decoder = SemiMarkovDecoder(config) +stages = decoder.decode(log_probs) # log_probs: (T, C) LOG-probabilities +stages = decoder.decode(log_probs, sqi_scores) # optional (T,) SQI in [0, 1] +``` + +Notes: + +- `decode` expects **log-probabilities** (e.g. `torch.log_softmax(logits, -1)`), not raw logits. +- `fragmentation_index` returns transitions **per epoch**; multiply by `3600 / epoch_sec` for per-hour. +- `sleep_architecture` reports TST, sleep efficiency, WASO, awakenings, and per-stage mean bout durations. +- `compute_sqi(signal, method=config.sqi_method)` returns a scalar quality score in `[0, 1]`. + +See [`notebooks/demo_stageguard.ipynb`](../notebooks/demo_stageguard.ipynb) for an end-to-end example. diff --git a/docs/datasets.md b/docs/datasets.md new file mode 100644 index 0000000..35d5e75 --- /dev/null +++ b/docs/datasets.md @@ -0,0 +1,53 @@ +# Datasets + +StageGuard is evaluated on four sleep datasets spanning four modalities. The data loaders in +`stageguard/data/` expect the data to be **pre-downloaded**; each loader exposes a `DOWNLOAD_URL` and a +`download_instructions()` helper: + +```python +from stageguard.data.mouse_eeg import AccuSleepDataset +print(AccuSleepDataset.download_instructions()) +ds = AccuSleepDataset("path/to/accusleep_dir", sequence_length=100) +``` + +## Overview + +| Dataset | Modality | Classes | Epoch | Access | Loader | +|---|---|---|---|---|---| +| AccuSleep | Mouse EEG/EMG | 3 (Wake, NREM, REM) | 4 s | Open ([Zenodo](https://zenodo.org/records/4079563)) | `data/mouse_eeg.py` | +| Sleep-Accel | Wrist actigraphy | 2 (Wake, Sleep) | 30 s | Open, ODC-By ([PhysioNet](https://physionet.org/content/sleep-accel/1.0.0/)) | `data/actigraphy.py` | +| SHHS | Cardiorespiratory | 3 (Wake, Light, Deep) | 30 s | Data-use agreement ([NSRR](https://sleepdata.org/datasets/shhs)) | `data/cardiorespiratory.py` | +| SLEEPBRL | Bioradar | 3 (Wake, Light, Deep) | 30 s | Contact authors | `data/bioradar.py` | + +Each loader concatenates per-recording files found in `data_dir` and returns `(signals, labels)`. + +## On-disk formats expected by each loader + +- **AccuSleep** (`AccuSleepDataset`): one or more `*.npz` files, each with `eeg` of shape + `(n_epochs, samples_per_epoch)` and integer `labels` of shape `(n_epochs,)` (Wake=0, NREM=1, REM=2). + Defaults: `fs=256`, `epoch_sec=4.0`. +- **Sleep-Accel** (`SleepAccelDataset`): per-subject CSV with columns `timestamp, x, y, z, label`, one row per + 30 s epoch, binary `label` (Wake=0, Sleep=1). +- **SHHS** (`SHHSDataset`): `*.h5` files, each with HDF5 datasets `features` and `labels` (Wake=0, Light=1, + Deep=2). Access requires an NSRR data-use agreement. +- **SLEEPBRL** (`SleepBRLDataset`): `*.npz` files, each with `signals` and `labels` (Wake=0, Light=1, Deep=2). + Not publicly redistributable; contact the authors. + +Modality-specific physiological constraints live in `configs/*.yaml` (one per dataset); load them with +`ModalityConfig.from_yaml(...)`. + +## Demo data subset + +The notebook [`notebooks/demo_stageguard.ipynb`](../notebooks/demo_stageguard.ipynb) does **not** require any of +the above. It uses a small, redistributable subset of **Sleep-Accel PSG hypnograms** (labels only, collapsed +to 3 states) shipped at [`data/sleepaccel_demo.npz`](../data/sleepaccel_demo.npz). See +[`data/README.md`](../data/README.md) for provenance, the ODC-By attribution, and the stage-collapse mapping. + +## Citations + +Cite the dataset you use: + +- AccuSleep: Barger et al. (2019). +- Sleep-Accel: Walch et al. (2019), `10.1093/sleep/zsz180`; PhysioNet record `10.13026/hmhs-py35`. +- SHHS: Quan et al. (1997); see the NSRR for current citation requirements. +- SLEEPBRL: Tataraidze et al. (2015). diff --git a/environment.yml b/environment.yml index 1f65d89..34f3b5f 100644 --- a/environment.yml +++ b/environment.yml @@ -12,5 +12,8 @@ dependencies: - pyyaml>=6.0 - h5py>=3.8 - pandas>=2.0 + - matplotlib>=3.7 + - jupyter + - ipykernel - pytest>=7.3 - pip diff --git a/notebooks/demo_stageguard.ipynb b/notebooks/demo_stageguard.ipynb new file mode 100644 index 0000000..a54b5e1 --- /dev/null +++ b/notebooks/demo_stageguard.ipynb @@ -0,0 +1,615 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "f5871133", + "metadata": {}, + "source": [ + "# [KDD 2026 AI4Sci] StageGuard - interactive demo\n", + "\n", + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/Q9gJYx/StageGuard/blob/main/notebooks/demo_stageguard.ipynb)\n", + "[![KDD 2026](https://img.shields.io/badge/KDD-2026%20AI4Sci-1d4ed8.svg)](https://doi.org/10.1145/3770855.3818916)\n", + "[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Q9gJYx/StageGuard/blob/main/LICENSE)\n", + "\n", + "**StageGuard** wraps any neural sleep-staging backbone with physiology-informed priors: a soft\n", + "transition penalty at training time and a **semi-Markov constrained decoder** at inference time.\n", + "\n", + "This notebook demonstrates the inference-time decoder on **real PSG hypnograms** from the open\n", + "[Sleep-Accel](https://physionet.org/content/sleep-accel/1.0.0/) dataset (ODC-By), comparing:\n", + "\n", + "- **Unconstrained** backbone output (`argmax` of per-epoch probabilities), vs\n", + "- **StageGuard** (semi-Markov constrained decode).\n", + "\n", + "It shows StageGuard drives the **transition-violation rate (TVR)** and **fragmentation index (FI)** down\n", + "while **holding or improving accuracy** - then visualizes the three hypnograms.\n", + "\n", + "> **Note.** No trained weights are shipped. A small, clearly-labeled *simulated* noisy backbone fabricates\n", + "> per-epoch probabilities from the ground-truth hypnogram (fixed seed) purely to exercise the decoder.\n", + "> Runtime: a few seconds on CPU. No GPU, no downloads beyond the repo." + ] + }, + { + "cell_type": "markdown", + "id": "da904e9a", + "metadata": {}, + "source": [ + "## Setup\n", + "On Colab this clones the repo and installs the package. Locally (run from the repo root) it is a no-op." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "987bf7c5", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:17.715874Z", + "iopub.status.busy": "2026-06-18T09:52:17.715874Z", + "iopub.status.idle": "2026-06-18T09:52:17.725417Z", + "shell.execute_reply": "2026-06-18T09:52:17.725417Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running in Colab: False\n" + ] + } + ], + "source": [ + "import os, sys\n", + "IN_COLAB = \"google.colab\" in sys.modules\n", + "if IN_COLAB:\n", + " if not os.path.exists(\"StageGuard\"):\n", + " !git clone -q https://github.com/Q9gJYx/StageGuard.git\n", + " %cd StageGuard\n", + " !pip install -q -e .\n", + "# Ensure cwd is the repo root so configs/ and data/ resolve when run locally.\n", + "if not os.path.exists(\"configs\") and os.path.basename(os.getcwd()) == \"notebooks\":\n", + " os.chdir(\"..\")\n", + "print(\"Running in Colab:\", IN_COLAB)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9cc1fa48", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:17.727425Z", + "iopub.status.busy": "2026-06-18T09:52:17.727425Z", + "iopub.status.idle": "2026-06-18T09:52:21.219131Z", + "shell.execute_reply": "2026-06-18T09:52:21.219131Z" + } + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from stageguard.config import ModalityConfig\n", + "from stageguard.decoder import SemiMarkovDecoder\n", + "from stageguard.metrics import (\n", + " transition_violation_rate, fragmentation_index,\n", + " classification_metrics, sleep_architecture,\n", + ")\n", + "SEED = 20260618" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "988b19b4", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.221293Z", + "iopub.status.busy": "2026-06-18T09:52:21.221293Z", + "iopub.status.idle": "2026-06-18T09:52:21.228566Z", + "shell.execute_reply": "2026-06-18T09:52:21.228566Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sleep-Accel (demo, 3-state) | stages=['Wake', 'NREM', 'REM'] | rare transitions=[(0, 2), (2, 0)]\n", + "primary subject 4426783 | T=980 epochs (8.2 h)\n" + ] + } + ], + "source": [ + "config = ModalityConfig.from_yaml(\"configs/sleepaccel_demo.yaml\")\n", + "data = np.load(\"data/sleepaccel_demo.npz\")\n", + "C = config.num_classes\n", + "epoch_sec = float(data[\"epoch_sec\"])\n", + "stage_names = list(config.stage_names)\n", + "y_true = data[\"hypno_0\"].astype(int) # primary subject\n", + "T = len(y_true)\n", + "print(f\"{config.dataset_name} | stages={stage_names} | rare transitions={config.rare_transitions}\")\n", + "print(f\"primary subject {data['subject_ids'][0]} | T={T} epochs ({T * epoch_sec / 3600:.1f} h)\")" + ] + }, + { + "cell_type": "markdown", + "id": "73045342", + "metadata": {}, + "source": [ + "## A simulated noisy backbone\n", + "\n", + "The function below is **not** a trained model. It fabricates `(T, C)` **log-probabilities** around the true\n", + "hypnogram (so a backbone-like `argmax` is mostly correct), then injects two realistic error modes:\n", + "\n", + "1. **Rare Wake↔REM violations** - a few REM epochs flipped to a confident *Wake* (raises TVR).\n", + "2. **Flip-flop fragmentation** - a few NREM epochs flipped to a confident wrong stage for one epoch (raises FI).\n", + "\n", + "`SemiMarkovDecoder.decode` then has to recover the clean hypnogram from these corrupted emissions." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3510c439", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.230575Z", + "iopub.status.busy": "2026-06-18T09:52:21.230575Z", + "iopub.status.idle": "2026-06-18T09:52:21.238355Z", + "shell.execute_reply": "2026-06-18T09:52:21.238355Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "log_probs: (980, 3) | rows normalized: True\n" + ] + } + ], + "source": [ + "def simulate_backbone_logprobs(y_true, C, rng, *, base_conf=2.5, noise=0.8,\n", + " n_rare=12, n_flip=18, spike=5.0, flip=4.0):\n", + " \"\"\"Deterministic SIMULATED (T, C) log-probabilities - NOT a trained model.\n", + "\n", + " Confident-but-noisy logits around the true hypnogram, plus injected\n", + " Wake<->REM violations (TVR) and single-epoch flip-flops (FI). Returns\n", + " log-probabilities because SemiMarkovDecoder.decode expects log-prob input.\n", + " \"\"\"\n", + " T = len(y_true)\n", + " logits = rng.normal(0.0, noise, size=(T, C))\n", + " logits[np.arange(T), y_true] += base_conf\n", + "\n", + " def mids(stage): # interior epochs sitting inside a >=3-epoch bout of `stage`\n", + " return [t for t in range(2, T - 2)\n", + " if y_true[t - 1] == y_true[t] == y_true[t + 1] == stage]\n", + "\n", + " # (1) rare Wake<->REM: flip REM-bout interiors to a confident Wake -> REM,Wake,REM\n", + " rem_mid = mids(2)\n", + " for t in rng.choice(rem_mid, size=min(n_rare, len(rem_mid)), replace=False):\n", + " logits[t, :] -= 1.0\n", + " logits[t, 0] += spike\n", + " # (2) flip-flops: flip NREM-bout interiors to a non-rare wrong stage (Wake or REM)\n", + " nrem_mid = mids(1)\n", + " for i, t in enumerate(rng.choice(nrem_mid, size=min(n_flip, len(nrem_mid)), replace=False)):\n", + " logits[t, :] -= 1.0\n", + " logits[t, 0 if i % 2 else 2] += flip\n", + "\n", + " logits -= logits.max(axis=1, keepdims=True) # -> log-softmax (log-probs)\n", + " return (logits - np.log(np.exp(logits).sum(axis=1, keepdims=True))).astype(np.float64)\n", + "\n", + "\n", + "rng = np.random.default_rng(SEED)\n", + "log_probs = simulate_backbone_logprobs(y_true, C, rng)\n", + "print(\"log_probs:\", log_probs.shape,\n", + " \"| rows normalized:\", np.allclose(np.exp(log_probs).sum(1), 1.0))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c4e4ed29", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.240367Z", + "iopub.status.busy": "2026-06-18T09:52:21.240367Z", + "iopub.status.idle": "2026-06-18T09:52:21.500373Z", + "shell.execute_reply": "2026-06-18T09:52:21.500373Z" + } + }, + "outputs": [], + "source": [ + "decoder = SemiMarkovDecoder(config)\n", + "argmax_pred = log_probs.argmax(axis=1) # unconstrained backbone output\n", + "sg_pred = decoder.decode(log_probs) # StageGuard constrained decode" + ] + }, + { + "cell_type": "markdown", + "id": "f3a4f050", + "metadata": {}, + "source": [ + "## Metrics: constraints satisfied, accuracy preserved" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "733a6d59", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.503395Z", + "iopub.status.busy": "2026-06-18T09:52:21.502391Z", + "iopub.status.idle": "2026-06-18T09:52:21.523970Z", + "shell.execute_reply": "2026-06-18T09:52:21.523467Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "method TVR FI/h acc kappa REMlat/m\n", + "Unconstrained (argmax) 0.028 17.0 0.945 0.896 28.0\n", + "StageGuard decode 0.000 4.9 0.984 0.969 73.5\n", + "Ground truth (PSG) 0.000 4.9 1.000 1.000 73.5\n" + ] + } + ], + "source": [ + "def rem_latency_min(seq):\n", + " rem = stage_names.index(\"REM\")\n", + " idx = np.where(seq == rem)[0]\n", + " return idx[0] * epoch_sec / 60.0 if len(idx) else float(\"nan\")\n", + "\n", + "def summarize(name, pred):\n", + " cm = classification_metrics(y_true, pred, stage_names)\n", + " return dict(name=name,\n", + " TVR=transition_violation_rate(pred, config.rare_transitions),\n", + " FI_per_h=fragmentation_index(pred) * 3600.0 / epoch_sec,\n", + " acc=cm[\"accuracy\"], kappa=cm[\"kappa\"], REM_lat=rem_latency_min(pred))\n", + "\n", + "rows = [summarize(\"Unconstrained (argmax)\", argmax_pred),\n", + " summarize(\"StageGuard decode\", sg_pred),\n", + " summarize(\"Ground truth (PSG)\", y_true)]\n", + "print(f\"{'method':24s} {'TVR':>7} {'FI/h':>7} {'acc':>7} {'kappa':>7} {'REMlat/m':>9}\")\n", + "for r in rows:\n", + " print(f\"{r['name']:24s} {r['TVR']:7.3f} {r['FI_per_h']:7.1f} \"\n", + " f\"{r['acc']:7.3f} {r['kappa']:7.3f} {r['REM_lat']:9.1f}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dee1cdb3", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.525976Z", + "iopub.status.busy": "2026-06-18T09:52:21.524973Z", + "iopub.status.idle": "2026-06-18T09:52:21.531579Z", + "shell.execute_reply": "2026-06-18T09:52:21.530931Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "argmax {'tst_min': 440.5, 'sleep_efficiency': 0.9, 'waso_min': 46.0, 'awakenings': 40.0}\n", + "StageGuard {'tst_min': 452.0, 'sleep_efficiency': 0.92, 'waso_min': 25.0, 'awakenings': 12.0}\n", + "truth {'tst_min': 455.0, 'sleep_efficiency': 0.93, 'waso_min': 22.0, 'awakenings': 11.0}\n" + ] + } + ], + "source": [ + "# Derived sleep-architecture statistics (not directly optimized by the decoder)\n", + "def arch(pred):\n", + " a = sleep_architecture(pred, epoch_sec=epoch_sec, wake_label=0, stage_names=stage_names)\n", + " return {k: round(a[k], 2) for k in (\"tst_min\", \"sleep_efficiency\", \"waso_min\", \"awakenings\")}\n", + "\n", + "for name, pred in [(\"argmax\", argmax_pred), (\"StageGuard\", sg_pred), (\"truth\", y_true)]:\n", + " print(f\"{name:11s}\", arch(pred))" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2c28a566", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.533114Z", + "iopub.status.busy": "2026-06-18T09:52:21.533114Z", + "iopub.status.idle": "2026-06-18T09:52:21.537715Z", + "shell.execute_reply": "2026-06-18T09:52:21.537203Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "guard OK: TVR down, fragmentation down, accuracy held/improved\n" + ] + } + ], + "source": [ + "# Guard: fail loudly if the demo's story regresses.\n", + "tvr_a, tvr_s = rows[0][\"TVR\"], rows[1][\"TVR\"]\n", + "fi_a, fi_s = rows[0][\"FI_per_h\"], rows[1][\"FI_per_h\"]\n", + "acc_a, acc_s = rows[0][\"acc\"], rows[1][\"acc\"]\n", + "assert tvr_a > 0.01 and tvr_s <= tvr_a / 4, (tvr_a, tvr_s)\n", + "assert fi_a > fi_s, (fi_a, fi_s)\n", + "assert acc_s >= acc_a - 0.01, (acc_a, acc_s)\n", + "print(\"guard OK: TVR down, fragmentation down, accuracy held/improved\")" + ] + }, + { + "cell_type": "markdown", + "id": "8ca529d0", + "metadata": {}, + "source": [ + "## The three hypnograms" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "a1697476", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.539755Z", + "iopub.status.busy": "2026-06-18T09:52:21.539244Z", + "iopub.status.idle": "2026-06-18T09:52:21.726007Z", + "shell.execute_reply": "2026-06-18T09:52:21.725504Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "t_h = np.arange(T) * epoch_sec / 3600.0\n", + "fig, axes = plt.subplots(3, 1, figsize=(11, 6), sharex=True)\n", + "for ax, (title, seq) in zip(axes, [(\"Unconstrained argmax\", argmax_pred),\n", + " (\"StageGuard decode\", sg_pred),\n", + " (\"Ground truth (PSG)\", y_true)]):\n", + " ax.step(t_h, seq, where=\"post\", lw=1.0)\n", + " ax.set_yticks(range(C)); ax.set_yticklabels(stage_names)\n", + " ax.set_ylabel(title, fontsize=9); ax.invert_yaxis() # Wake on top (clinical convention)\n", + "axes[-1].set_xlabel(\"Time (hours)\")\n", + "fig.suptitle(\"StageGuard removes illegal Wake<->REM jumps and flip-flop fragmentation\")\n", + "fig.tight_layout(); plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6d56e21a", + "metadata": {}, + "source": [ + "## The same holds across all shipped subjects" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "75e6d982", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:21.728025Z", + "iopub.status.busy": "2026-06-18T09:52:21.727010Z", + "iopub.status.idle": "2026-06-18T09:52:22.855951Z", + "shell.execute_reply": "2026-06-18T09:52:22.855951Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " subject TVR (am->sg) FI/h (am->sg) acc (am->sg)\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 4426783 0.028 -> 0.000 17.0 -> 4.9 0.945 -> 0.984\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 781756 0.039 -> 0.000 19.1 -> 5.1 0.942 -> 0.982\n", + " 9961348 0.042 -> 0.001 18.4 -> 4.2 0.939 -> 0.993\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 759667 0.070 -> 0.000 25.3 -> 3.3 0.909 -> 0.981\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1066528 0.037 -> 0.000 17.5 -> 5.6 0.957 -> 0.979\n" + ] + } + ], + "source": [ + "print(f\"{'subject':>9} {'TVR (am->sg)':>14} {'FI/h (am->sg)':>16} {'acc (am->sg)':>14}\")\n", + "for i in range(len(data[\"subject_ids\"])):\n", + " y = data[f\"hypno_{i}\"].astype(int)\n", + " lp = simulate_backbone_logprobs(y, C, np.random.default_rng(SEED + i))\n", + " am, sg = lp.argmax(1), decoder.decode(lp)\n", + " tvr = (transition_violation_rate(am, config.rare_transitions),\n", + " transition_violation_rate(sg, config.rare_transitions))\n", + " fih = (fragmentation_index(am) * 3600 / epoch_sec,\n", + " fragmentation_index(sg) * 3600 / epoch_sec)\n", + " acc = (classification_metrics(y, am)[\"accuracy\"],\n", + " classification_metrics(y, sg)[\"accuracy\"])\n", + " print(f\"{str(data['subject_ids'][i]):>9} {tvr[0]:5.3f} -> {tvr[1]:<5.3f} \"\n", + " f\"{fih[0]:6.1f} -> {fih[1]:<6.1f} {acc[0]:.3f} -> {acc[1]:.3f}\")" + ] + }, + { + "cell_type": "markdown", + "id": "9b7511b2", + "metadata": {}, + "source": [ + "## Signal-quality integration\n", + "\n", + "Per-epoch signal-quality (SQI) scores let the decoder discount low-quality epochs: emissions are damped\n", + "toward uniform where SQI is low, so the physiological priors take over. Below, a short window inside a REM\n", + "bout is corrupted with confident-but-wrong \"Wake\" emissions (true REM left as the runner-up). With full trust\n", + "(no SQI) the decoder follows the corrupt signal; with low SQI over that window, the rare Wake<->REM prior\n", + "correctly overrides it and REM is preserved." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "506ab535", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:22.858058Z", + "iopub.status.busy": "2026-06-18T09:52:22.858058Z", + "iopub.status.idle": "2026-06-18T09:52:23.393875Z", + "shell.execute_reply": "2026-06-18T09:52:23.393875Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "REM-bout artifact window [568:576] REM accuracy -> no SQI: 0.00 with SQI: 1.00\n" + ] + } + ], + "source": [ + "def runs_of(seq):\n", + " out, i = [], 0\n", + " while i < len(seq):\n", + " j = i + 1\n", + " while j < len(seq) and seq[j] == seq[i]:\n", + " j += 1\n", + " out.append((i, j, int(seq[i]))); i = j\n", + " return out\n", + "\n", + "# corrupt a short window inside the longest REM bout: confident \"Wake\", true REM as runner-up\n", + "rem = stage_names.index(\"REM\")\n", + "i0, j0, _ = max((r for r in runs_of(y_true) if r[2] == rem), key=lambda r: r[1] - r[0])\n", + "L = 8\n", + "a = i0 + ((j0 - i0) - L) // 2; b = a + L\n", + "\n", + "lp_bad = log_probs.copy()\n", + "lp_bad[a:b, :] = np.log([0.90, 0.02, 0.08]) # [Wake, NREM, REM]: confident-wrong Wake\n", + "sqi = np.ones(T); sqi[a:b] = 0.05 # mark the window low-quality (scores in [0, 1])\n", + "\n", + "pred_no_sqi = decoder.decode(lp_bad) # full trust -> follows the corrupt Wake\n", + "pred_sqi = decoder.decode(lp_bad, sqi) # low SQI -> rare-transition prior preserves REM\n", + "acc_win = lambda p: float((p[a:b] == y_true[a:b]).mean())\n", + "print(f\"REM-bout artifact window [{a}:{b}] REM accuracy -> \"\n", + " f\"no SQI: {acc_win(pred_no_sqi):.2f} with SQI: {acc_win(pred_sqi):.2f}\")\n", + "assert acc_win(pred_sqi) > acc_win(pred_no_sqi), \"SQI should recover the corrupt window\"" + ] + }, + { + "cell_type": "markdown", + "id": "97eaebb8", + "metadata": {}, + "source": [ + "## Training-side: the differentiable soft transition penalty" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "9f566e0f", + "metadata": { + "execution": { + "iopub.execute_input": "2026-06-18T09:52:23.395883Z", + "iopub.status.busy": "2026-06-18T09:52:23.395883Z", + "iopub.status.idle": "2026-06-18T09:52:23.418789Z", + "shell.execute_reply": "2026-06-18T09:52:23.418286Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "total=1.3299 ce=1.0993 trans=0.2306 (random net -> small trans term)\n", + "backward OK: the soft transition penalty is differentiable\n" + ] + } + ], + "source": [ + "import torch\n", + "from stageguard.backbones import get_backbone\n", + "from stageguard.wrapper import StageGuardWrapper\n", + "\n", + "backbone = get_backbone(\"accusleep\", num_classes=C, in_channels=1)\n", + "model = StageGuardWrapper(backbone, config)\n", + "B, Tt, samples = 2, 40, 128\n", + "x = torch.randn(B, Tt, 1, samples) # (B, T, in_channels, epoch_samples)\n", + "targets = torch.randint(0, C, (B, Tt))\n", + "loss, details = model.training_step(x, targets)\n", + "print(f\"total={loss.item():.4f} ce={details['ce_loss'].item():.4f} \"\n", + " f\"trans={details['trans_loss'].item():.4f} (random net -> small trans term)\")\n", + "loss.backward()\n", + "print(\"backward OK: the soft transition penalty is differentiable\")" + ] + }, + { + "cell_type": "markdown", + "id": "59a0c6e6", + "metadata": {}, + "source": [ + "## Next steps\n", + "\n", + "- Wrap your own backbone (any `nn.Module` returning `(B, T, C)` logits) - see\n", + " [`docs/backbones.md`](https://github.com/Q9gJYx/StageGuard/blob/main/docs/backbones.md).\n", + "- Real datasets and access notes - see\n", + " [`docs/datasets.md`](https://github.com/Q9gJYx/StageGuard/blob/main/docs/datasets.md).\n", + "- Paper: *StageGuard: Physiologically Constrained Sleep Staging*, KDD 2026 AI4Sciences,\n", + " DOI [10.1145/3770855.3818916](https://doi.org/10.1145/3770855.3818916). Archival DOI (Zenodo): _pending_.\n", + "- Please cite via [`CITATION.cff`](https://github.com/Q9gJYx/StageGuard/blob/main/CITATION.cff)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.15" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index e8ef467..7107c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,27 @@ build-backend = "setuptools.build_meta" [project] name = "stageguard" -version = "0.1.0" +version = "1.0.0" description = "Backbone-agnostic physiological constraints for neural sleep staging" readme = "README.md" license = "MIT" requires-python = ">=3.10" +authors = [ + {name = "Juntang Wang", email = "jw853@duke.edu"}, + {name = "Yihan Wang", email = "yw572@duke.edu"}, + {name = "Hao Wu", email = "hw412@duke.edu"}, + {name = "Jiayu Gao", email = "jg563@duke.edu"}, + {name = "Shixin Xu", email = "shixin.xu@duke.edu"}, + {name = "Dongmian Zou", email = "dongmian.zou@duke.edu"}, +] +keywords = [ + "sleep staging", + "physiological constraints", + "semi-Markov model", + "temporal consistency", + "constrained decoding", + "deep learning", +] dependencies = [ "torch>=2.0", "numpy>=1.24", @@ -21,6 +37,12 @@ dependencies = [ [project.optional-dependencies] dev = ["pytest>=7.3"] +demo = ["matplotlib>=3.7", "jupyter"] + +[project.urls] +Homepage = "https://github.com/Q9gJYx/StageGuard" +Repository = "https://github.com/Q9gJYx/StageGuard" +Paper = "https://doi.org/10.1145/3770855.3818916" [tool.setuptools.packages.find] include = ["stageguard*"] diff --git a/stageguard/__init__.py b/stageguard/__init__.py index fd5ad13..198bcf3 100644 --- a/stageguard/__init__.py +++ b/stageguard/__init__.py @@ -1,6 +1,6 @@ """StageGuard: backbone-agnostic physiological constraints for neural sleep staging.""" -__version__ = "0.1.0" +__version__ = "1.0.0" from .config import ModalityConfig from .decoder import SemiMarkovDecoder diff --git a/stageguard/config.py b/stageguard/config.py index 9e92326..8b36bf2 100644 --- a/stageguard/config.py +++ b/stageguard/config.py @@ -61,6 +61,6 @@ def __post_init__(self) -> None: def from_yaml(cls, path: str | Path) -> "ModalityConfig": """Load configuration from a YAML file.""" path = Path(path) - with open(path) as f: + with open(path, encoding="utf-8") as f: data = yaml.safe_load(f) return cls(**data)