Skip to content

Sleep consistency reports a typical bedtime no night is near #41

Description

@adidshaft

The defect

Sleep consistency reports a typical bedtime that no night is near. On the current 14-night window it renders typical bedtime 20:48 while 0 of 14 nights fall within an hour of it. The percentage built on top of that number is equally meaningless.

Two independent bugs stack, both in AtriaSleepConsistency.result(from:) in Atria/Atria/Sessions.swift.

1. The noon anchor splits a single cluster in half

// Anchor both timings at the night that starts in the evening.
return (night.id, night.day,
        bedtime < 12 * 60 ? bedtime + 24 * 60 : bedtime,
        wake    < 12 * 60 ? wake    + 24 * 60 : wake)

The anchor assumes sleep begins in the evening. For any wearer whose main sleep starts near midday, the cluster straddles the anchor:

Real bedtime Anchored minutes
11:28 2,128 (= 11:28 + 24 h) pushed a full day forward
12:08 728 left alone

Two nights 40 minutes apart become 23 h 20 m apart. The anchor is not wrong in general — it is wrong for this wearer, and it fails silently.

2. Arithmetic mean over a bimodal set

let typicalBedtime = Int((Double(bedtimes.reduce(0, +)) / Double(bedtimes.count)).rounded())

Once the anchor has scattered the nights to both ends of the axis, the mean lands in the empty middle — which is how a wearer who reliably sleeps in the early afternoon is told their typical bedtime is 20:48. recommendedWindow already uses medianWake; the two typicals do not.

Why the fix is coordinated, not a one-liner

The anchored minute values are load-bearing beyond this struct. NightDeviation.bedtimeMinutes / .wakeMinutes feed the schedule visual, which maps them through AtriaSleepConsistencyStrip.fraction(_:) against axisStartMinutes (AtriaHealthScreen.swift:3323). Changing the anchor without moving the axis moves every night off the strip. AtriaFitnessAge also consumes combinedPercent as a scored factor, so the number changing is a user-visible scoring change, not just a label fix.

Suggested direction

A circular mean on a 24 h clock is the standard fix and is byte-identical for a conventional evening sleeper — worth checking, because it means the change can be validated as a no-op on normal data before it is trusted on shifted data. It alone does not fix this case (still 0% here); the anchor has to stop being a fixed noon constant and become derived from the wearer's own cluster.

Done when

  • A wearer sleeping ~13:15→19:15 gets a typical bedtime within an hour of their actual nights.
  • A conventional 23:00→07:00 sleeper's output is unchanged (regression pin).
  • A genuinely biphasic/irregular set reports low consistency rather than a confident wrong centre.
  • The schedule strip and AtriaFitnessAge factor stay correct under the new anchor.
  • The metric fails closed rather than printing a centre no night supports.

Related: the wearer's shifted schedule is the same condition that made #25 hard.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Important, not blocking releasearea: sleepSleep detection, staging, and settlementarea: uiScreens, charts, and presentation truthtype: defectA wrong behaviour observed on device or in code

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions