Skip to content

Stop dropping REM sleep, and let the coach say so - #120

Open
ak710 wants to merge 1 commit into
saksham2001:mainfrom
ak710:fix/rem-sleep-plumbing
Open

Stop dropping REM sleep, and let the coach say so#120
ak710 wants to merge 1 commit into
saksham2001:mainfrom
ak710:fix/rem-sleep-plumbing

Conversation

@ak710

@ak710 ak710 commented Aug 3, 2026

Copy link
Copy Markdown

Closes #111

Merge order: 1 of 9. Branches off main, depends on nothing.

What

The Colmi big-data timeline (stage 0x04) and the YCBT timeline (tag 3) both report REM, and both decoders have always stored it. It just never got any further — SleepSummary carried light/deep/awake only, so REM never reached the sleep score, the Sleep tab, or the coach, which was hard-coded to tell the model "light/deep/awake only, no REM" on every ring.

Also fixes a scoring bug

hasAwakeSignal falls back to asking whether the stage timeline accounted for essentially the whole night. REM was excluded from that sum, so a fully-described REM night looked ~80% covered, failed the 0.95 gate, and had its awake reading thrown away as "no signal" — costing it 45% of the awake sub-score.

Test: testFullyDescribedRemNightKeepsItsAwakeSignal, with testPartiallyDescribedNightStillWithholdsAwakeSignal as the complement so the fix didn't just make the gate unconditionally true.

Design notes

  • Nil, not zero, throughout. A jring's 0x11 timeline genuinely has no REM stage; "absent" and "you slept no REM" are different claims. remPct, SleepContext.remMin and AverageStages.rem are all optional.
  • The coach's caveat is chosen per night from that night's own blocks, not from the connected ring's capabilities. Stored nights outlive the ring that recorded them, so switching rings must not retro-actively disclaim last week's REM.
  • REM is measured but not yet scored. The light band (ideal 50–60%) is calibrated for a no-REM decoder that lumps REM into light, so re-weighting belongs in its own change — noted in SleepScore.calculate, and done in [Feature]: Sleep score scores time in bed as if it were time asleep #115.
  • averageStages became a struct rather than a 4-tuple, which trips SwiftLint's large_tuple. That's a net −1 on the lint baseline.

Testing

PulseLoopTests/SleepRemStageTests.swift — 10 new tests. Full suite green (881). SwiftLint introduces no new violation types.

Demo data already generates REM blocks, so -seedDemo YES exercises this without hardware.

The Colmi big-data timeline (stage 0x04) and the YCBT timeline (tag 3) both
report REM, and both decoders have always stored it as a SleepStageBlock. It
just never got any further: SleepSummary carried light/deep/awake only, so REM
never reached the sleep score, the Sleep tab, or the coach — which was still
hard-coded to tell the model "light/deep/awake only, no REM" on every ring.

Carry remMinutes through SleepSummary, collapseByDay and averageStages, and
report it as remPct. Nil rather than zero throughout when the ring reported no
REM stage at all: a jring's 0x11 timeline genuinely has none, and "absent" and
"you slept no REM" are different claims.

This also fixes a scoring side-effect. hasAwakeSignal falls back to asking
whether the stage timeline accounted for essentially the whole night, and REM
was excluded from that sum — so a fully described REM night looked only ~80%
covered, failed the 0.95 gate, and had its awake reading thrown away as "no
signal", costing it 45% of the awake sub-score. REM now counts toward coverage.

REM is measured but still not scored: the light band (ideal 50-60%) is
calibrated for a no-REM decoder that lumps REM into light, so re-weighting the
score would move every stored night and needs the versioned recompute readiness
does. Noted in SleepScore.calculate for the sleep-score rework.

The coach's caveat is now chosen per night from that night's own blocks rather
than from the connected ring's capabilities — stored nights outlive the ring
that recorded them, so switching rings must not retro-actively disclaim last
week's REM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Sleep stages decode REM but nothing downstream uses it

1 participant