OxyDex · PulseDex nodes, Tepna physiological-signal suite
Background. When a sleep-apnea patient starts CPAP, their overnight physiology should step: respiratory events fall and autonomic tone recovers. If a multi-night wearable record can pinpoint when that step happened — and flag that it happened at all — the device becomes a passive therapy-response monitor. We test whether a vanilla change-point detector on the production metrics can do it. Methods. For intervention-arc synthetic patients with a planted CPAP-start night (restricted to ≥2 pre- and ≥2 post-treatment nights), we measured per-night ODI-4 (OxyDex) and rMSSD (PulseDex) with the real detectors. A single change-point (minimum within-segment SSE) localized the planted night for each metric and for a fused respiratory+autonomic index; the step-model R² gave a detection statistic, scored against flat-arc controls by ROC. Results. Across CLAIM trNIntervention = 233 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nIntervention intervention and CLAIM trNFlat = 239 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nFlatControl flat-control patients (records of ≥10 nights), change-point localization recovered the CPAP-start night with a median error of 0 nights and within ±1 night in CLAIM trWithin1Rmssd = 95 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Rmssd–CLAIM trWithin1Fused = 100 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Fused% of patients. Fusing the respiratory and autonomic channels was best — exact CLAIM trExactFused = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactFused%, within ±1 night CLAIM trWithin1Fused2 = 100 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Fused%, detection AUC CLAIM trAucFused = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucFused — at least as good as either channel alone (ODI-4 AUC CLAIM trAucOdi = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucOdi, rMSSD CLAIM trAucRmssd = 0.97 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucRmssd). The reversal this abstract reported is WITHDRAWN by the 2026-09-22 correction below and is not restated: on the current tree the respiratory channel is the stronger localizer again (ODI-4 exact CLAIM trExactOdi = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactOdi% vs rMSSD CLAIM trExactRmssd = 88 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactRmssd%). Conclusion. A treatment response that produces a clean physiological step is recoverable and well-localized by a simple detector, and the two single-signal channels carry partly independent evidence that fusion exploits. This is synthetic ground truth with a known, fairly clean step: it certifies the pipeline and method, not real-world CPAP-response detection, which is noisier, adherence-dependent, and confounded.
Keywords: change-point detection · CPAP · treatment response · obstructive sleep apnea · oximetry · heart-rate variability · sensor fusion · longitudinal monitoring
When someone with sleep apnea starts CPAP therapy, their body changes overnight: breathing interruptions drop and the heart's “rest-and-recover” signal improves. If a wearable worn across several nights could spot which night therapy started — and confirm it started at all — it would act as a passive check that the treatment is working.
We tested this on simulated patients where we planted a known “therapy-start” night, then used the real app's two detectors (a breathing one and a heart-rhythm one) to measure each night. A standard “where did the trend change?” method found the right night almost exactly — typically the correct night, nearly always within one night — and combining the breathing and heart signals worked best of all. One honest caveat we quantified: the method can “find” a fake change even in steady patients, so it must always be compared against people who didn't change. Because the step is clean and known here, this proves the method and software work; real CPAP responses are messier, so it isn't yet a claim about real patients.
Adherence and response to CPAP are usually assessed from the device's own residual-event log, not from the patient's broader physiology. Yet the response is physiologically broad: starting effective therapy lowers the apnea burden (fewer desaturations, lower ODI) and, over the same nights, relieves the autonomic stress of repeated arousals (higher short-term heart-rate variability). A wearable that records several nights spanning the therapy start therefore contains a step in two partly-independent channels — a respiratory one and an autonomic one. The analytic question is a textbook one: given a short, noisy multi-night series, can a change-point detector (i) decide that a step occurred and (ii) localize which night it occurred on? We answer it on synthetic patients where the change-point is planted and known, using the production single-signal detectors to produce the trajectories.
Intervention-arc patients in the 1–12-night longitudinal lane carry a planted CPAP-start night (profile.interventionNight, the 0-based index of the first treated night). On and after that night the generator drops the apnea–hypopnea index sharply and lets rMSSD recover, so ODI-4 steps down and rMSSD steps up at the same boundary. We restricted the analysis to arcs with at least two pre- and two post-treatment nights (and, in this run, to records of at least 10 nights), so a single change-point is in principle recoverable. Flat-arc patients (a stable latent, no step) are the detection null. Per-metric attrition came only from per-night node missingness: a patient was used for a metric only if every night carried a valid value (no interpolation across gaps).
Each night was scored by the unmodified production detector, loaded alone in its own Web-Worker realm (OxyDex and PulseDex collide on bare globals, so each runs in a separate worker pool and the two per-night trajectories are joined by seed): ODI-4 from oxydex-dsp.js → processNight and time-domain rMSSD from pulsedex-dsp.js. Timestamps follow the suite Clock Contract so night ordering is viewer-timezone-independent.
For a per-night series x₀…x₍ₘ₋₁₎ we fit a single change-point by minimizing total within-segment sum-of-squares, requiring at least two nights on each side:
where ĉ is the estimated first post-step night, matching the planted convention. We ran this on ODI-4, on rMSSD, and on a fused index — each series z-scored, ODI-4 sign-flipped (so both step the same direction), then averaged. Localization was scored against the planted night as exact match, within ±1 night, and median absolute error. As a detection statistic we used the step-model coefficient of determination R² = 1 − SSE_split/SSE_total, and computed the rank (Mann–Whitney) AUC of intervention vs flat-control R².
| Detector | Patients | Exact | Within ±1 | Median |err| (nights) | Detection AUC | tx median R² | flat median R² |
|---|---|---|---|---|---|---|---|
| ODI-4 (OxyDex) | 912 | 73% | 85% | 0 | 0.94 | 0.79 | 0.27 |
| rMSSD (PulseDex) | 912 | 87% | 96% | 0 | 0.97 | 0.76 | 0.24 |
| Fused (respiratory + autonomic) | 912 | 91% | 97% | 0 | 0.99 | 0.85 | 0.27 |
🔴 CORRECTION — 2026-09-22 (Table 1 re-cut at what the generator yields; owner ruling 2026-09-21,
OWNER-DECISION-QUEUE D9.2). Table 1’s stated cohort — 912 intervention + 918 flat-control at
≥ 10 nights from “~900” per arm — is not recoverable from the repository: driving
treatment-response-analysis.html at its stated configuration (nSubj 900, minN 10) yields
a few hundred patients per arm, not nine hundred, identically under cohort-gen 1.9 and 2.0 (residues
2026-09-16-treatment-response-config-unreproducible ·
2026-09-17-treatment-response-gap-is-not-the-generator ·
2026-09-17-treatment-response-three-cohort-sizes). The ruling: restate the headline at the n the
generator yields, with the interval that n supports, and do not reconfigure toward the published size.
Table 1 above is left as published; this is the same table on the current tree, every cell a sourced claim
against analysis/published-numbers/treatment-response-2026-09-22.json (run 2026-09-22 by tools/analysis-rerun.mjs --paper-scale, intervals
by tools/treatment-response-recut.mjs: Wilson 95 % for the proportions, Hanley–McNeil 95 % for
the AUC; medians carried as the page publishes them, without an interval).
The n moved again since the ruling. The rows measured 269 / 317 on 2026-09-16/17; the same
configuration on 2026-09-22 yields CLAIM trNIntervention = 233 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nIntervention intervention and
CLAIM trNFlat = 239 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nFlatControl flat-control patients at ≥ CLAIM trMinNights = 10 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/minNights nights.
The page and the generator inline the production detectors, and OxyDex moved between those dates
(#2610 epoch HR as the mean of rates; #2745 computeStats over measured seconds only) — named as
the candidates, not verified as the cause; the ruling’s figure was itself a dated measurement.
| Detector | Patients | Exact | Within ±1 | Median |err| (nights) | Detection AUC | tx median R² | flat median R² |
|---|---|---|---|---|---|---|---|
| ODI-4 (OxyDex) | CLAIM trOdiN = 233 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nOdi | CLAIM trExactOdi = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactOdi % [CLAIM trExactLoOdi = 96.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactLoOdi–CLAIM trExactHiOdi = 99.8 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactHiOdi] | CLAIM trW1Odi = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Odi % [CLAIM trW1LoOdi = 96.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1LoOdi–CLAIM trW1HiOdi = 99.8 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1HiOdi] | CLAIM trMedErrOdi = 0 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/medErrOdi | CLAIM trAucOdi = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucOdi [CLAIM trAucLoOdi = 0.991 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucLoOdi–CLAIM trAucHiOdi = 1.000 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucHiOdi] | CLAIM trTxR2Odi = 0.98 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/txR2Odi | CLAIM trFlatR2Odi = 0.31 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/flatR2Odi |
| rMSSD (PulseDex) | CLAIM trRmssdN = 233 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nRmssd | CLAIM trExactRmssd = 88 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactRmssd % [CLAIM trExactLoRmssd = 83.7 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactLoRmssd–CLAIM trExactHiRmssd = 91.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactHiRmssd] | CLAIM trW1Rmssd = 95 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Rmssd % [CLAIM trW1LoRmssd = 91.7 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1LoRmssd–CLAIM trW1HiRmssd = 97.3 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1HiRmssd] | CLAIM trMedErrRmssd = 0 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/medErrRmssd | CLAIM trAucRmssd = 0.97 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucRmssd [CLAIM trAucLoRmssd = 0.954 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucLoRmssd–CLAIM trAucHiRmssd = 0.986 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucHiRmssd] | CLAIM trTxR2Rmssd = 0.77 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/txR2Rmssd | CLAIM trFlatR2Rmssd = 0.23 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/flatR2Rmssd |
| Fused (respiratory + autonomic) | CLAIM trFusedN = 233 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/nFused | CLAIM trExactFused = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactFused % [CLAIM trExactLoFused = 96.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactLoFused–CLAIM trExactHiFused = 99.8 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactHiFused] | CLAIM trW1Fused = 100 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Fused % [CLAIM trW1LoFused = 98.4 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1LoFused–CLAIM trW1HiFused = 100.0 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1HiFused] | CLAIM trMedErrFused = 0 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/medErrFused | CLAIM trAucFused = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucFused [CLAIM trAucLoFused = 0.994 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucLoFused–CLAIM trAucHiFused = 1.000 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucHiFused] | CLAIM trTxR2Fused = 0.93 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/txR2Fused | CLAIM trFlatR2Fused = 0.28 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/flatR2Fused |
What the re-cut changes. Localization is at or near the ceiling for every detector at this n — median error 0 nights everywhere, fused within ±1 in 100 % [98.4–100] — and the intervals say what 233 patients can: an exact-localization rate of 99 % is known to about ±1.5 points, an AUC of 1.00 to about ±0.006. The revision note’s reversal does not hold on this tree: it reports ODI-4’s exact localization falling to 73 % with rMSSD (87 %) localizing better; here ODI-4 localizes exactly in 99 % [96.9–99.8] and rMSSD in 88 % [83.7–91.9], so the respiratory channel is the stronger localizer again and the sentence that built a point on the reversal is withdrawn rather than restated. The conclusion that survives every version — the fused index is at least as good as either channel and a change-point estimator recovers the CPAP-start night with median error 0 — holds at this n with the intervals shown. Table 2’s “This run” row (912 tx + 918 flat) is superseded by the counts above. Synthetic ground truth, as §4 states; nothing here is a field result.
Every detector localized the planted CPAP-start night with a median error of 0 nights. The fused respiratory+autonomic index was best, recovering the exact night in CLAIM trExactFused2 = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactFused% of patients and landing within ±1 night in CLAIM trWithin1Fused3 = 100 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/within1Fused%, with a detection AUC of CLAIM trAucFused2 = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucFused . The single-channel reversal this paragraph reported is WITHDRAWN by the 2026-09-22 correction below and is not restated. It read "rMSSD localizes better than ODI-4" (exact 87 % vs 73 %) against the pre-correction cut; on the current tree ODI-4 localizes exactly in CLAIM trExactOdi2 = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactOdi% [CLAIM trExactLoOdi = 96.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactLoOdi–CLAIM trExactHiOdi = 99.8 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactHiOdi] and rMSSD in CLAIM trExactRmssd2 = 88 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactRmssd% [CLAIM trExactLoRmssd = 83.7 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactLoRmssd–CLAIM trExactHiRmssd = 91.9 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactHiRmssd], so the respiratory channel is the stronger localizer again. The mechanism the paragraph offered for the reversal (a shallower, noisier respiratory step degrading ODI-4's AUC) explained an effect that is not present on this tree and is withdrawn with it. Because the two channels carry partly independent evidence of the same response, fusing them still beats either alone on both localization and detection.
treatment-response-analysis.html, synth-gen 2.1 / cohort-gen 1.9). Top: one intervention patient — ODI-4 (blue) collapses and rMSSD (amber) rises at the planted CPAP-start night (green band); the fused detector's estimate (teal dashed) coincides. Bottom-left: localization accuracy — solid = exact night, light = within ±1 night, for ODI-4 / rMSSD / fused (rMSSD now leads the single channels). Bottom-right: detection ROC (intervention vs flat controls) by step-R²; all three curves hug the top-left, fused highest (AUC CLAIM trAucFused3 = 1.00 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/aucFused (current tree; the figure image is the pre-correction render)). Dark theme is the tool's native rendering.The detection statistic must be read against the null, not absolutely. Fitting a free single change-point to a flat control series still "explained" a median ~24–27% of its variance (R²≈0.24–0.27) purely by overfitting the best of many candidate splits to noise. Intervention patients sat far above this (median R²≈0.76–0.85), which is why detection separates so cleanly — but it also means a fixed R² cutoff read in isolation would over-call change in stable patients. The flat-control comparison (or an equivalent permutation/penalized threshold) is therefore not optional; it is what turns the inflated raw statistic into a usable decision.
When a treatment produces a genuine physiological step, recovering it is easy: a one-line change-point estimator localizes the CPAP-start night to the correct night (median error 0) and usually within a single night, and detects the response with high AUC. The practical messages are three. First, the claim that autonomic recovery is more informative than the respiratory drop is WITHDRAWN by the 2026-09-22 correction below — it rested on the reversal, and on this tree ODI-4 is the stronger localizer (exact CLAIM trExactOdi3 = 99 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactOdi% vs CLAIM trExactRmssd3 = 88 FROM analysis/published-numbers/treatment-response-2026-09-22.json#claims/exactRmssd%). What survives every version is that the fused index is at least as good as either channel alone here (exact 87% vs 73%), so a device without oximetry is not only not blind to therapy response, it may be the stronger single channel. Second, fusion helps: the two channels are not redundant, and combining them is the most accurate and most robust option (exact 91%, AUC 0.99). Third, the detection threshold must account for the null, because the best-split R² is inflated by the search over candidate change-points.
treatment-response-analysis.html → set patients/arm and minimum nights → "Run cohort". The example trajectory, accuracy bars, ROC, and Table 1 populate live. Export treatment-response-results.csv, treatment-response-stats.json, treatment-response-figures.png.oxydex-dsp.js (ODI-4 = processNight().odi4.rate) and pulsedex-dsp.js (rMSSD), each run in its own cohort-worker.js Web-Worker realm (two pools, joined by seed).cohort-gen.js + synth-gen.js; planted CPAP-start night = profile.interventionNight.Patients are independent, so the two arms can be grown to any size; accuracy estimates (exact %, within-±1 %, AUC) are proportions whose standard error scales as √(p(1−p)/N), and the detection AUC's SE falls as ~1/√N. The qualitative result — near-perfect localization, fusion best, null-inflated R² — is visible at small N; large N tightens the percentages and the ROC.
| Tier | Patients/arm | What it buys |
|---|---|---|
| Minimum (acceptable) | ~300 | Localization %s stable to ≈±3%, AUC to ≈±0.02; ordering (fused > ODI-4 > rMSSD) and the null-inflation point are already clear. Below ~100/arm the AUC and exact-% wobble. |
| Recommended | ~3,000 | Percentages to ≈±1%, AUC to the third decimal, clean ROC and accuracy bars. |
| This run (SUPERSEDED — see the 2026-09-22 correction above) | ~900 | |
| Diminishing returns | > ~5,000 | Past here, extra patients barely move accuracies already pinned near their ceilings; what limits realism is the deliberately clean planted step, which more patients cannot make more realistic. |
Practical reading: ~300/arm is enough to see the effect, ~3,000/arm gives publication-quality precision, and beyond ~5,000/arm the gains are cosmetic — the binding limitation is the idealized synthetic step, not sample size.
CLAUDE.md (Clock Contract, evidence-grade system), COHORT-VALIDATION-BRIEF.md, CROSSNIGHT-ENVELOPE-SPEC.md, Tepna suite.