Skip to content

feat(analytics): sleep-vs-wake HR contrast (Swift + Kotlin) - #1424

Merged
ryanbr merged 1 commit into
mainfrom
analytics-sleep-hr-contrast
Aug 18, 2026
Merged

ryanbr merged 1 commit into
mainfrom
analytics-sleep-hr-contrast

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What

A pure, deterministic engine — mirrored Swift/Kotlin — that compares mean HR over an explicitly supplied primary-sleep window against mean HR over an explicitly supplied wake window. Reports sleepMinusWakeBpm, sleepReductionPercent = 100·(W−S)/W, and per-window valid/total/coverage.

Deliberately not a new RHR, not a dipping classifier

Contract

Fixed-grid epoch arrays: nil = unobserved, out-of-[30,220]-bpm = invalid; missing/invalid epochs are never imputed and only lower coverage. Unweighted mean over valid epochs (a time-weighted mean only because the grid is fixed). The 30-valid-sample gate is parameterized (default mirrors the primary-session RHR experiment) and applied independently to each window; evaluate returns nil unless both sides clear it.

Tests / validation

  • Android testFullDebugUnitTest: SleepHeartRateContrastTest 7/7 green locally (both reduction signs, independent gating, coverage math, range-edge inclusivity, fail-closed).
  • Swift StrandAnalyticsTests: same suite, validated by swift-packages CI (StrandAnalytics needs macOS).
  • Pure package + Android analytics only — no app-target Swift, no schema/migration.

Status

Complete + tested but not yet surfaced — nothing builds the fixed-grid wake/primary-sleep HR windows yet; that window-construction is deliberately out of scope. Idea from @jonbiro.

Adds SleepHeartRateContrast, a pure engine that compares mean HR over an
explicitly supplied primary-sleep window against a wake window, mirrored
Swift/Kotlin. Reports sleepMinusWakeBpm, sleepReductionPercent, and per-window
valid/total/coverage.

Deliberately NOT a new RHR definition: the shipped floor RHR and the
primary-session mean candidate (#1174/#1188, evidence #1169) are untouched, and
it adds no third definition. Emits NO dipper/non-dipper/riser/risk/diagnostic
label - descriptive wellness context only, matching NOOP's non-diagnostic
boundary. Says "sleep vs wake", not "night vs day"; window selection is the
caller's responsibility.

Fixed-grid input contract: nil = unobserved, out-of-range (30..220 bpm) =
invalid; missing/invalid epochs are never imputed and only lower coverage. The
30-valid-sample gate is parameterized (default mirrors the primary-session RHR
experiment) and applied independently to each window.

Idea from @jonbiro. Not yet surfaced - no consumer builds the windows yet.
@ryanbr
ryanbr merged commit 6cb3666 into main Aug 18, 2026
14 checks passed
@ryanbr
ryanbr deleted the analytics-sleep-hr-contrast branch August 18, 2026 01:09
ryanbr added a commit that referenced this pull request Aug 18, 2026
Build numbers only. MARKETING_VERSION / versionName stay 10.1.1 — the staging
line is one above the latest release (v10.1.0) and holds there until 10.1.1
actually ships.

versionCode 341 -> 342 and CURRENT_PROJECT_VERSION 223 -> 224, both +1 past the
previous staging build so a sideloaded update installs in place rather than
being refused as a downgrade.

Picks up since the last testing build (f2271cd): backup build provenance
(#1422), the CTL/ATL/TSB training-load engine (#1423) and its Trends card
(#1425), sleep-vs-wake HR contrast (#1424), the v20 optical decoder + CRC gate
(#895), the Sleep UI split (#1426), the WHOOP4 clock-correlation fix (#1427),
the settings waist-row layout + copy fix (#1428), hourly Apple step import
(#1429), Low refresh (#1430), and Power saving moving into More with a 35%
ceiling (#1431).
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.

1 participant