psgscoring 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. psgscoring-0.2.4/LICENSE +28 -0
  2. psgscoring-0.2.4/PKG-INFO +113 -0
  3. psgscoring-0.2.4/README.md +74 -0
  4. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/__init__.py +6 -4
  5. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/classify.py +29 -0
  6. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/constants.py +8 -0
  7. psgscoring-0.2.4/psgscoring/ecg_effort.py +390 -0
  8. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/pipeline.py +4 -0
  9. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/respiratory.py +299 -94
  10. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/signal_quality.py +1 -1
  11. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/spo2.py +14 -3
  12. psgscoring-0.2.4/psgscoring/tests/conftest.py +6 -0
  13. {psgscoring-0.2.2 → psgscoring-0.2.4/psgscoring}/tests/test_psgscoring.py +0 -2
  14. psgscoring-0.2.4/psgscoring.egg-info/PKG-INFO +113 -0
  15. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring.egg-info/SOURCES.txt +3 -3
  16. psgscoring-0.2.4/psgscoring.egg-info/requires.txt +11 -0
  17. psgscoring-0.2.4/pyproject.toml +60 -0
  18. psgscoring-0.2.2/LICENSE +0 -28
  19. psgscoring-0.2.2/PKG-INFO +0 -308
  20. psgscoring-0.2.2/README.md +0 -272
  21. psgscoring-0.2.2/psgscoring/_arousal.py +0 -891
  22. psgscoring-0.2.2/psgscoring/_core.py +0 -2404
  23. psgscoring-0.2.2/psgscoring.egg-info/PKG-INFO +0 -308
  24. psgscoring-0.2.2/psgscoring.egg-info/requires.txt +0 -12
  25. psgscoring-0.2.2/pyproject.toml +0 -48
  26. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/ancillary.py +0 -0
  27. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/breath.py +0 -0
  28. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/plm.py +0 -0
  29. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/signal.py +0 -0
  30. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring/utils.py +0 -0
  31. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring.egg-info/dependency_links.txt +0 -0
  32. {psgscoring-0.2.2 → psgscoring-0.2.4}/psgscoring.egg-info/top_level.txt +0 -0
  33. {psgscoring-0.2.2 → psgscoring-0.2.4}/setup.cfg +0 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024-2026, Bart Rombaut, Briek Rombaut, Cedric Rombaut
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: psgscoring
3
+ Version: 0.2.4
4
+ Summary: Open-source AASM 2.6-compliant respiratory scoring for polysomnography
5
+ Author: Briek Rombaut, Cedric Rombaut
6
+ Author-email: Bart Rombaut <bart.rombaut@azorg.be>
7
+ License: BSD-3-Clause
8
+ Project-URL: Homepage, https://github.com/bartromb/psgscoring
9
+ Project-URL: Documentation, https://github.com/bartromb/psgscoring#readme
10
+ Project-URL: Repository, https://github.com/bartromb/psgscoring
11
+ Project-URL: Issues, https://github.com/bartromb/psgscoring/issues
12
+ Project-URL: Changelog, https://github.com/bartromb/psgscoring/blob/main/CHANGES.md
13
+ Keywords: sleep,polysomnography,PSG,apnea,hypopnea,AHI,AASM,respiratory,scoring,sleep-staging,YASA,EDF,clinical,neuroscience
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Intended Audience :: Healthcare Industry
17
+ Classifier: License :: OSI Approved :: BSD License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
25
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.22
30
+ Requires-Dist: scipy>=1.9
31
+ Requires-Dist: mne>=1.3
32
+ Provides-Extra: full
33
+ Requires-Dist: lightgbm>=3.3; extra == "full"
34
+ Requires-Dist: yasa>=0.6.4; extra == "full"
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest>=7.0; extra == "dev"
37
+ Requires-Dist: pytest-cov; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ # psgscoring
41
+
42
+ **Open-source AASM 2.6-compliant respiratory scoring for polysomnography.**
43
+
44
+ `psgscoring` is a standalone Python library that implements the full AASM 2.6 respiratory scoring algorithm for polysomnographic (PSG) recordings. It is the scoring engine behind [YASAFlaskified](https://github.com/bartromb/YASAFlaskified) and is designed to work with [YASA](https://github.com/raphaelvallat/yasa) and [MNE-Python](https://mne.tools/).
45
+
46
+ ## Features
47
+
48
+ - **Apnea detection**: ≥90% flow reduction on oronasal thermistor, ≥10 s
49
+ - **Hypopnea detection**: ≥30% flow reduction on nasal pressure, ≥10 s, with ≥3% desaturation or arousal
50
+ - **Apnea type classification**: 7-rule obstructive/central/mixed classification with Hilbert phase-angle analysis
51
+ - **ECG-derived effort** (v0.2.4): Transformed ECG (TECG) method (Berry et al., JCSM 2019) + spectral classifier for improved central vs. obstructive differentiation
52
+ - **Six over-counting corrections**: post-apnea baseline inflation, SpO₂ cross-contamination, Cheyne-Stokes flagging, borderline classification, artefact-flank exclusion, local baseline validation
53
+ - **Six under-counting corrections**: peak-based hypopnea detection, SpO₂ de-blocking, extended nadir window, flow smoothing, position auto-mapping, configurable scoring profiles
54
+ - **SpO₂ analysis**: ODI at 3% and 4% thresholds, baseline (P90), T90
55
+ - **PLM scoring**: AASM 2.6 + WASM criteria
56
+ - **RERA and RDI**: flow limitation (flattening index) and FRI-RERA detection
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pip install psgscoring
62
+ ```
63
+
64
+ With optional dependencies (LightGBM confidence calibration):
65
+
66
+ ```bash
67
+ pip install psgscoring[full]
68
+ ```
69
+
70
+ ## Quick start
71
+
72
+ ```python
73
+ import mne
74
+ from psgscoring import run_pneumo_analysis
75
+
76
+ raw = mne.io.read_raw_edf("recording.edf", preload=True)
77
+ hypno = ["W", "N1", "N2", "N2", "N3", ...] # 30-s epoch labels
78
+
79
+ results = run_pneumo_analysis(raw, hypno)
80
+
81
+ ahi = results["respiratory"]["summary"]["ahi_total"]
82
+ print(f"AHI: {ahi}")
83
+ ```
84
+
85
+ ## Scoring profiles
86
+
87
+ Three configurable profiles balance sensitivity vs. specificity:
88
+
89
+ | Profile | Use case | Hypopnea threshold |
90
+ |---------|----------|-------------------|
91
+ | `strict` | Minimise false positives | 40% flow reduction |
92
+ | `standard` | AASM 2.6 default | 30% flow reduction |
93
+ | `sensitive` | Maximise detection | 25% flow reduction |
94
+
95
+ ## References
96
+
97
+ - Berry RB et al. The AASM Manual for the Scoring of Sleep and Associated Events, Version 2.6. AASM, 2020.
98
+ - Berry RB et al. Use of a Transformed ECG Signal to Detect Respiratory Effort During Apnea. *J Clin Sleep Med.* 2019;15(11):1653–1660.
99
+ - Vallat R, Walker MP. An open-source, high-performance tool for automated sleep staging. *eLife.* 2021;10:e70092.
100
+
101
+ ## Disclaimer
102
+
103
+ `psgscoring` is **research software**, not a cleared medical device. All computed indices are research-grade estimates that must be reviewed by a qualified clinician. See [DISCLAIMER.md](https://github.com/bartromb/psgscoring/blob/main/DISCLAIMER.md).
104
+
105
+ ## License
106
+
107
+ BSD-3-Clause. See [LICENSE](LICENSE).
108
+
109
+ ## Citation
110
+
111
+ If you use `psgscoring` in your research, please cite:
112
+
113
+ > Rombaut B, Rombaut B, Rombaut C, Vallat R. YASAFlaskified: an open-source platform for automated PSG analysis with systematic over-counting and under-counting corrections. *In preparation*, 2026.
@@ -0,0 +1,74 @@
1
+ # psgscoring
2
+
3
+ **Open-source AASM 2.6-compliant respiratory scoring for polysomnography.**
4
+
5
+ `psgscoring` is a standalone Python library that implements the full AASM 2.6 respiratory scoring algorithm for polysomnographic (PSG) recordings. It is the scoring engine behind [YASAFlaskified](https://github.com/bartromb/YASAFlaskified) and is designed to work with [YASA](https://github.com/raphaelvallat/yasa) and [MNE-Python](https://mne.tools/).
6
+
7
+ ## Features
8
+
9
+ - **Apnea detection**: ≥90% flow reduction on oronasal thermistor, ≥10 s
10
+ - **Hypopnea detection**: ≥30% flow reduction on nasal pressure, ≥10 s, with ≥3% desaturation or arousal
11
+ - **Apnea type classification**: 7-rule obstructive/central/mixed classification with Hilbert phase-angle analysis
12
+ - **ECG-derived effort** (v0.2.4): Transformed ECG (TECG) method (Berry et al., JCSM 2019) + spectral classifier for improved central vs. obstructive differentiation
13
+ - **Six over-counting corrections**: post-apnea baseline inflation, SpO₂ cross-contamination, Cheyne-Stokes flagging, borderline classification, artefact-flank exclusion, local baseline validation
14
+ - **Six under-counting corrections**: peak-based hypopnea detection, SpO₂ de-blocking, extended nadir window, flow smoothing, position auto-mapping, configurable scoring profiles
15
+ - **SpO₂ analysis**: ODI at 3% and 4% thresholds, baseline (P90), T90
16
+ - **PLM scoring**: AASM 2.6 + WASM criteria
17
+ - **RERA and RDI**: flow limitation (flattening index) and FRI-RERA detection
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install psgscoring
23
+ ```
24
+
25
+ With optional dependencies (LightGBM confidence calibration):
26
+
27
+ ```bash
28
+ pip install psgscoring[full]
29
+ ```
30
+
31
+ ## Quick start
32
+
33
+ ```python
34
+ import mne
35
+ from psgscoring import run_pneumo_analysis
36
+
37
+ raw = mne.io.read_raw_edf("recording.edf", preload=True)
38
+ hypno = ["W", "N1", "N2", "N2", "N3", ...] # 30-s epoch labels
39
+
40
+ results = run_pneumo_analysis(raw, hypno)
41
+
42
+ ahi = results["respiratory"]["summary"]["ahi_total"]
43
+ print(f"AHI: {ahi}")
44
+ ```
45
+
46
+ ## Scoring profiles
47
+
48
+ Three configurable profiles balance sensitivity vs. specificity:
49
+
50
+ | Profile | Use case | Hypopnea threshold |
51
+ |---------|----------|-------------------|
52
+ | `strict` | Minimise false positives | 40% flow reduction |
53
+ | `standard` | AASM 2.6 default | 30% flow reduction |
54
+ | `sensitive` | Maximise detection | 25% flow reduction |
55
+
56
+ ## References
57
+
58
+ - Berry RB et al. The AASM Manual for the Scoring of Sleep and Associated Events, Version 2.6. AASM, 2020.
59
+ - Berry RB et al. Use of a Transformed ECG Signal to Detect Respiratory Effort During Apnea. *J Clin Sleep Med.* 2019;15(11):1653–1660.
60
+ - Vallat R, Walker MP. An open-source, high-performance tool for automated sleep staging. *eLife.* 2021;10:e70092.
61
+
62
+ ## Disclaimer
63
+
64
+ `psgscoring` is **research software**, not a cleared medical device. All computed indices are research-grade estimates that must be reviewed by a qualified clinician. See [DISCLAIMER.md](https://github.com/bartromb/psgscoring/blob/main/DISCLAIMER.md).
65
+
66
+ ## License
67
+
68
+ BSD-3-Clause. See [LICENSE](LICENSE).
69
+
70
+ ## Citation
71
+
72
+ If you use `psgscoring` in your research, please cite:
73
+
74
+ > Rombaut B, Rombaut B, Rombaut C, Vallat R. YASAFlaskified: an open-source platform for automated PSG analysis with systematic over-counting and under-counting corrections. *In preparation*, 2026.
@@ -20,6 +20,7 @@ spo2 – SpO2 coupling (Rule 1A) and full SpO2 analysis
20
20
  plm – PLM detection (AASM 2.6)
21
21
  ancillary – position, heart rate, snore, Cheyne-Stokes
22
22
  respiratory – apnea/hypopnea detection, Rule 1B, summary statistics
23
+ ecg_effort – ECG-derived effort (TECG, spectral classifier) for central/obstructive differentiation
23
24
  pipeline – MNE-facing master function (run_pneumo_analysis)
24
25
  """
25
26
 
@@ -49,6 +50,7 @@ from .breath import (
49
50
  detect_breath_events,
50
51
  )
51
52
  from .classify import classify_apnea_type
53
+ from .ecg_effort import ecg_effort_assessment, compute_tecg
52
54
  from .spo2 import analyze_spo2, detect_desaturations, get_desaturation
53
55
  from .plm import analyze_plm
54
56
  from .ancillary import (
@@ -65,9 +67,8 @@ from .utils import (
65
67
  is_nrem, is_rem, is_sleep,
66
68
  safe_r,
67
69
  )
68
- from .signal_quality import assess_signal_quality
69
70
 
70
- __version__ = "0.2.2"
71
+ __version__ = "0.2.4"
71
72
  __all__ = [
72
73
  # Master
73
74
  "run_pneumo_analysis",
@@ -92,6 +93,9 @@ __all__ = [
92
93
  "detect_breath_events",
93
94
  # Classify
94
95
  "classify_apnea_type",
96
+ # ECG effort
97
+ "ecg_effort_assessment",
98
+ "compute_tecg",
95
99
  # SpO2
96
100
  "analyze_spo2",
97
101
  "detect_desaturations",
@@ -103,8 +107,6 @@ __all__ = [
103
107
  "analyze_heart_rate",
104
108
  "analyze_snore",
105
109
  "detect_cheyne_stokes",
106
- # Signal quality (v0.2.2)
107
- "assess_signal_quality",
108
110
  # Utils
109
111
  "detect_channels",
110
112
  "channel_map_from_user",
@@ -112,6 +112,7 @@ def classify_apnea_type(
112
112
  abdomen_raw: np.ndarray | None,
113
113
  effort_baseline: float,
114
114
  sf: float,
115
+ ecg_assessment: dict | None = None,
115
116
  ) -> tuple[str, float, dict]:
116
117
  """
117
118
  Classify an apnea event as ``"obstructive"``, ``"central"``, or
@@ -126,9 +127,19 @@ def classify_apnea_type(
126
127
  3. **Mixed pattern** – absent effort first half, present second half
127
128
  4. **Effort clearly present** → obstructive
128
129
  5. **Truly flat** – no raw movement, low envelope → central
130
+ 5b. **ECG-derived reclassification** (v0.8.23) – TECG + spectral
131
+ analysis overrides RIP-based obstructive if cardiac artefact only.
129
132
  6. **Borderline default** → obstructive (low confidence)
130
133
  If LightGBM model available, confidence is calibrated by model.
131
134
 
135
+ Parameters
136
+ ----------
137
+ ecg_assessment : dict, optional
138
+ Output of ``ecg_effort.ecg_effort_assessment()``.
139
+ If provided and ``reclassify_as_central`` is True, events that
140
+ would otherwise be classified as obstructive (rules 4, 6) are
141
+ reclassified as central.
142
+
132
143
  Returns
133
144
  -------
134
145
  (type_str, confidence_0_to_1, detail_dict)
@@ -239,6 +250,24 @@ def classify_apnea_type(
239
250
  )
240
251
  return "central", safe_r(_conf(conf, 5), 2), detail
241
252
 
253
+ # ── Rule 5b (v0.8.23): ECG-derived reclassification ──────────────────
254
+ # If TECG shows no inspiratory bursts AND spectral analysis shows
255
+ # cardiac dominance, reclassify borderline/effort-present as central.
256
+ if ecg_assessment is not None and ecg_assessment.get("reclassify_as_central"):
257
+ detail["ecg_assessment"] = {
258
+ k: v for k, v in ecg_assessment.items()
259
+ if k not in ("tecg_detail", "spectral_detail")
260
+ }
261
+ # Only reclassify if effort is ambiguous (ratio < clear threshold)
262
+ if effort_ratio < EFFORT_PRESENT_RATIO * 1.5:
263
+ conf = 0.75
264
+ if ecg_assessment.get("ecg_effort_present") is False:
265
+ conf = 0.85 # both TECG and spectral agree
266
+ detail["decision_reason"] = (
267
+ f"ecg_reclassified_central_effort={safe_r(effort_ratio,3)}"
268
+ )
269
+ return "central", safe_r(_conf(conf, 5), 2), detail
270
+
242
271
  # ── Rule 6: Borderline default ────────────────────────────────────────
243
272
  detail["decision_reason"] = (
244
273
  f"borderline_default_var={safe_r(raw_var_ratio,3)}_effort={safe_r(effort_ratio,3)}"
@@ -16,6 +16,8 @@ HYPOPNEA_SMOOTH_S = 3.0 # v0.8.12: smooth flow before thresholding (seco
16
16
  # Mimics human visual averaging; reduces false negatives
17
17
  APNEA_MIN_DUR_S = 10.0 # seconds
18
18
  HYPOPNEA_MIN_DUR_S = 10.0 # seconds
19
+ APNEA_MAX_DUR_S = 90.0 # v0.8.22: events >90s are split at partial recovery
20
+ HYPOPNEA_MAX_DUR_S = 60.0 # v0.8.22: events >60s are split at partial recovery
19
21
  DESATURATION_DROP_PCT = 3.0 # >= 3% SpO2 drop required for Rule 1A
20
22
  EFFORT_ABSENT_RATIO = 0.20 # < 20% of baseline effort -> absent
21
23
  EFFORT_PRESENT_RATIO = 0.40 # > 40% of baseline effort -> present
@@ -45,6 +47,8 @@ SCORING_PROFILES: dict[str, dict] = {
45
47
  "HYPOPNEA_SMOOTH_S": 0.0, # no smoothing
46
48
  "CROSS_CONTAM_WINDOW_S": 15.0,
47
49
  "USE_PEAK_DETECTION": False, # envelope only
50
+ "HYPOPNEA_MAX_DUR_S": 60.0, # v0.8.22
51
+ "APNEA_MAX_DUR_S": 90.0, # v0.8.22
48
52
  },
49
53
  "standard": {
50
54
  "label": "Standard (AASM 2.6)",
@@ -54,6 +58,8 @@ SCORING_PROFILES: dict[str, dict] = {
54
58
  "HYPOPNEA_SMOOTH_S": 3.0,
55
59
  "CROSS_CONTAM_WINDOW_S": 15.0,
56
60
  "USE_PEAK_DETECTION": True, # peak + envelope
61
+ "HYPOPNEA_MAX_DUR_S": 60.0, # v0.8.22
62
+ "APNEA_MAX_DUR_S": 90.0, # v0.8.22
57
63
  },
58
64
  "sensitive": {
59
65
  "label": "Sensitive (RPSGT-like)",
@@ -63,6 +69,8 @@ SCORING_PROFILES: dict[str, dict] = {
63
69
  "HYPOPNEA_SMOOTH_S": 5.0, # more smoothing
64
70
  "CROSS_CONTAM_WINDOW_S": 0.0, # no cross-contam check
65
71
  "USE_PEAK_DETECTION": True,
72
+ "HYPOPNEA_MAX_DUR_S": 90.0, # v0.8.22: meer tolerant
73
+ "APNEA_MAX_DUR_S": 120.0, # v0.8.22
66
74
  },
67
75
  }
68
76