-
Notifications
You must be signed in to change notification settings - Fork 12
A sedentary day is not eight naps: keep sedentary wake in the awake baseline #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -147,6 +147,50 @@ void main() { | |
| reason: 'the rejection reason must be visible, not silent'); | ||
| }); | ||
|
|
||
| test('a SEDENTARY DAY is not eight naps — sedentary wake stays in the ' | ||
| 'awake baseline', () { | ||
| // THE REGRESSION. The baseline once excluded every detected bout, which | ||
| // removed all of the day's still time and left the AMBULATORY HR median | ||
| // in its place. Every motionless block then cleared `medHr > baseline * | ||
| // napRestingHrMult` on nothing but the ordinary sit/walk HR difference: | ||
| // this exact fixture returned EIGHT naps totalling 199 minutes, each at | ||
| // confidence 0.85 — the cap — for a day nobody napped on. | ||
| // | ||
| // The test above cannot catch it: it uses bpm 80 for BOTH its active and | ||
| // still segments, so active and sedentary HR are identical and the | ||
| // baseline cannot be inflated. The contrast IS the bug, so it has to be | ||
| // in the fixture. | ||
| final d = _Day(); | ||
| for (var b = 0; b < 8; b++) { | ||
| d | ||
| ..active(6, bpm: 96) // walking | ||
| ..still(25, bpm: 72); // at the desk — awake, just not moving | ||
| } | ||
| d.active(60, bpm: 96); | ||
|
|
||
| final m = detectNaps(d.accel, d.hr); | ||
|
|
||
| expect(m.value, isEmpty, | ||
| reason: 'sitting still at 72 bpm on a day you walk at 96 is not a ' | ||
| 'nap; keeping the still seconds puts the median at 72, and ' | ||
| '0.95 x 72 = 68.4 < 72 rejects every block'); | ||
| expect(m.note, contains('no HR dip')); | ||
| }); | ||
|
|
||
| test('the same day at a 10% HR contrast is also not a nap', () { | ||
| // The failure did not need a dramatic difference — 84 vs 76 bpm, which is | ||
| // an unremarkable sit-versus-walk gap, produced all eight at 0.84. | ||
| final d = _Day(); | ||
| for (var b = 0; b < 8; b++) { | ||
| d | ||
| ..active(6, bpm: 84) | ||
| ..still(25, bpm: 76); | ||
| } | ||
| d.active(60, bpm: 84); | ||
|
|
||
| expect(detectNaps(d.accel, d.hr).value, isEmpty); | ||
| }); | ||
|
|
||
| test('an off-wrist span is rejected even though it is perfectly still', () { | ||
| final d = _Day() | ||
| ..active(90) | ||
|
|
@@ -377,6 +421,33 @@ void main() { | |
| reason: 'the awake baseline is ~78 bpm; a 60 bpm nap clears it'); | ||
| expect(m.value!.single.tstSec, closeTo(30 * 60, 90)); | ||
| }); | ||
|
|
||
| test('a real nap is still detected on a SEDENTARY day', () { | ||
| // The other side of the same coin, and the reason the fix keeps sedentary | ||
| // wake in the pool instead of dropping every bout: a day that is mostly | ||
| // sitting still must still be able to report the one block that was | ||
| // actually sleep. Baseline lands at desk HR (~72), and 56 clears | ||
| // 0.95 x 72 = 68.4 comfortably. | ||
| final d = _Day(); | ||
| for (var b = 0; b < 4; b++) { | ||
| d | ||
| ..active(6, bpm: 96) | ||
| ..still(25, bpm: 72); | ||
| } | ||
| d.still(50, bpm: 56); // the genuine nap, a real autonomic dip | ||
| for (var b = 0; b < 4; b++) { | ||
| d | ||
| ..active(6, bpm: 96) | ||
| ..still(25, bpm: 72); | ||
| } | ||
| d.active(60, bpm: 96); // end awake: nothing to defer at the record end | ||
|
|
||
| final m = detectNaps(d.accel, d.hr); | ||
|
|
||
| expect(m.value, hasLength(1), | ||
| reason: 'the deep-dip block is a nap even though the day around it ' | ||
| 'is sedentary'); | ||
| }); | ||
|
Comment on lines
+425
to
+450
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Separate the 50-minute nap from adjacent sedentary bouts. The preceding Insert an active interval longer than the bout-chain bridge before the nap. Assert that Proposed test correction for (var b = 0; b < 4; b++) {
d
..active(6, bpm: 96)
..still(25, bpm: 72);
}
+ d.active(6, bpm: 96);
d.still(50, bpm: 56); // the genuine nap, a real autonomic dip
for (var b = 0; b < 4; b++) {
d
..active(6, bpm: 96)
..still(25, bpm: 72);
}
@@
expect(m.value, hasLength(1),
reason: 'the deep-dip block is a nap even though the day around it '
'is sedentary');
+ expect(m.value!.single.tstSec, closeTo(50 * 60, 90));🤖 Prompt for AI Agents |
||
| }); | ||
|
|
||
| group('detectNaps — recording holes are not stillness', () { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add a regression test for the per-candidate baseline shortage path.
test/onehz/nap_test.dartLines 322-336 leave the day-wideawakeIdxpool belowminAwakeHrSamples. That test returns at Line 268. It does not execute the new check at Line 342.Add a fixture where
awakeIdx.length >= minAwakeHrSamples, but removing the candidate leaves fewer samples. Assert thatdetectNapsreturnsMetric.absentwhen no other nap is emitted.🤖 Prompt for AI Agents