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.
+[](https://colab.research.google.com/github/Q9gJYx/StageGuard/blob/main/notebooks/demo_stageguard.ipynb)
+[](https://doi.org/10.1145/3770855.3818916)
+[](https://doi.org/10.1145/3770855.3818916)
+[](LICENSE)
+[](https://www.python.org)
+[](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):
+
+[](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",
+ "[](https://colab.research.google.com/github/Q9gJYx/StageGuard/blob/main/notebooks/demo_stageguard.ipynb)\n",
+ "[](https://doi.org/10.1145/3770855.3818916)\n",
+ "[](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)