Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
94877e4
feat(sleep-train): fit a sleep stager to open polysomnography
ayiskakov Sep 24, 2026
cd88346
feat(sleep): SleepStagerV3, a stager fitted to polysomnography
ayiskakov Sep 24, 2026
839a032
feat(sleep-psg): score V2 and V3 over Tools/SleepTrain cohorts
ayiskakov Sep 24, 2026
6ed6b96
feat(sleep): V3 stages nights by default; V2 and V1 stay selectable
ayiskakov Sep 24, 2026
f66ea74
docs(sleep): describe SleepStagerV3 in ANALYTICS.md
ayiskakov Sep 24, 2026
58b8e1e
fix(sleep): re-stage an edited night through the same funnel detectio…
ayiskakov Sep 24, 2026
9acac05
fix(engine): stage every day of a pass with the choice its cache sign…
ayiskakov Sep 24, 2026
b4393be
fix(sleep-train): leave an undefined night kappa out of the mean, as …
ayiskakov Sep 24, 2026
816558e
fix(engine): name every day-cache config field beside its value
ayiskakov Sep 24, 2026
3a64c31
fix(diagnostics): the REM funnel line names the staging setting, not …
ayiskakov Sep 24, 2026
1329567
fix(sleep-ui): a nap edit names the nap's start and end, not Bedtime …
ayiskakov Sep 24, 2026
b8c71f7
fix(sleep): judge the session's cut-short last epoch by the grid epoc…
ayiskakov Sep 24, 2026
1229c34
fix(sleep): V3 fills a column missing all night with the head's mean,…
ayiskakov Sep 24, 2026
20bc5f0
fix(sleep): stage a night with no motion with the chosen recipe, and …
ayiskakov Sep 24, 2026
c34c55e
docs(sleep): V2's cycle prior keeps the session clock inside a band w…
ayiskakov Sep 24, 2026
05119fc
refactor(sleep): one copy of the stream plumbing V2 and V3 stage behind
ayiskakov Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -364,18 +364,17 @@ public enum AnalyticsEngine {
// pure-function callers/tests free of it; IntelligenceEngine threads the
// night window's persisted band state. (#531 / H8 consume)
bandSleepState: [(ts: Int, state: Int)] = [],
// Which sleep-staging recipe runs (V2 vs V1). When true, detected nights
// are staged by `SleepStagerV2` instead of V1. This PARAMETER defaults
// false to keep pure-function callers/tests byte-identical — it is NOT
// the product default. IntelligenceEngine threads
// `PuffinExperiment.experimentalSleepV2Enabled`, which is default ON
// (#277/#351), so the shipped app stages with V2. (7.0.0)
useSleepStagerV2: Bool = false,
// Which sleep-staging recipe stages detected nights. This PARAMETER
// defaults to `.v1` to keep pure-function callers/tests byte-identical —
// it is NOT the product default. IntelligenceEngine threads
// `PuffinExperiment.sleepStager`, which is `.v3` unless the user picked
// another recipe, so the shipped app stages with V3.
stager: SleepStagerVersion = .v1,
// Opt-in motion-aware wake refinement (#364 "Proposal 2" follow-up; density
// gate precedent #345). When true, `WakeMotionRefinement` re-derives each
// detected session's stages, reclassifying a hot-but-still WAKE segment to
// `light` when it shows no locomotion and a stable posture outside isolated
// burst minutes; it only ever runs AFTER V1/V2 staging and self-gates on the
// burst minutes; it only ever runs AFTER staging and self-gates on the
// observed gravity + step density, so it is a no-op on a sparse (e.g. WHOOP
// 4.0) night regardless of this flag. Default false keeps every pure-function
// caller/test byte-identical; IntelligenceEngine threads
Expand Down Expand Up @@ -445,9 +444,9 @@ public enum AnalyticsEngine {
let detectedSessions = SleepStager.detectSleep(hr: hr, rr: rr, resp: resp, gravity: gravity,
tzOffsetSeconds: tzOffsetSeconds, wristOff: wristOff,
bandSleepState: bandSleepState,
useSleepStagerV2: useSleepStagerV2,
stager: stager,
traceSink: traceSink)
// Motion-aware wake refinement (#364 follow-up) runs AFTER V1/V2 staging, over every detected
// Motion-aware wake refinement (#364 follow-up) runs AFTER staging, over every detected
// session (naps included — the same eligibility gates apply). `steps` is the SAME calendar-day/
// night-window stream the caller passed for the rest of this analysis; the pass self-gates on its
// observed density, so an empty/sparse `steps` (e.g. a WHOOP 4.0, which never emits StepSample at
Expand Down Expand Up @@ -594,11 +593,14 @@ public enum AnalyticsEngine {
}
// #319: the motion-coverage + staging context behind the Rest number, so a high score on a poor
// night can be explained from an export (WHOOP 4.0 banks motion coarsely → sparse=true → most
// epochs default to sleep → over-counted duration → high Rest). `stager` says whether V1/V2 ran.
// epochs default to sleep → over-counted duration → high Rest). `stager` says which recipe ran:
// an HR-only night was staged by `hrOnlyRecipe` (V2 where the choice is V1, which cannot stage
// a night without motion), which IntelligenceEngine hands `hrOnlySessions` from the same choice.
let stagedBy = mainGroup.contains { $0.hrOnly } ? stager.hrOnlyRecipe : stager
traceSink(AnalyticsEngine.sleepMotionLine(
day: day, grav: gravity.count, hr: hr.count,
sparse: gravitySparse,
useSleepStagerV2: useSleepStagerV2, family: skinTempFamily))
stager: stagedBy, family: skinTempFamily))
// CAPTURE-C (#799): append the sleep PROVENANCE so an imported row winning the merge is visible
// (not silently swapped for the measured night). hoursAsleep = the scored night's tst in minutes;
// sourceRowId = the main-night's start ts for the measured path (stable per night), else the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ extension AnalyticsEngine {
/// most epochs default to sleep → over-counted duration → high Rest); `stager` says which engine ran;
/// `family` the day's owner. PURE; byte-identical to Android `AnalyticsEngine.sleepMotionLine`.
public static func sleepMotionLine(day: String, grav: Int, hr: Int, sparse: Bool,
useSleepStagerV2: Bool, family: DeviceFamily) -> String {
stager: SleepStagerVersion, family: DeviceFamily) -> String {
"sleep-motion day=\(day) grav=\(grav) hr=\(hr) sparse=\(sparse) "
+ "stager=\(useSleepStagerV2 ? "V2" : "V1") family=\(family.rawValue)"
+ "stager=\(stager.label) family=\(family.rawValue)"
}

/// How long AFTER the detected onset to sample HR for the #271 onset trace (seconds). The first
Expand Down
Loading