-
Notifications
You must be signed in to change notification settings - Fork 20
sleep: end an auto night at the band's last SLEEP (gen5/MG envelope) #80
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
Open
DropTabl
wants to merge
4
commits into
OpenStrap:main
Choose a base branch
from
DropTabl:feat/band-sleep-offset-trim
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| // BAND-STATE NIGHT END — corroboration, never a source. | ||
| // | ||
| // Gen5/MG R18 body 60 bits 4-5 carry the band's own coarse envelope: | ||
| // 0 wake, 1 still, 2 sleep, 3 up ("up" only after sleep, "still" only between | ||
| // wake and sleep). No stages, lags onset, so it never creates or stages a night. | ||
| // One use: when the band reports SLEEP for the LAST time inside our window and | ||
| // then stays awake (UP/WAKE), continuously observed, for the rest of it, the | ||
| // remainder was a lie-in. Never "first UP": mid-night UP runs of 16-28 min that | ||
| // return to SLEEP are normal. STILL in the tail means re-settling — refuse. | ||
| // | ||
| // Two shapes exist for the same signal in this package: this POSITIONAL | ||
| // List<int> (1:1 with the 1 Hz arrays, -1 absent) and | ||
| // AdvancedSleepStager.detectSleep's [ts, state] pairs. Same codes. | ||
|
|
||
| const int kBandStateSleep = 2; | ||
| const int kBandStateStill = 1; | ||
| const int kMinBandStateCoveragePct = 80; | ||
| const int kMinBandTailCoveragePct = 95; | ||
| const int kMaxBandTailGapSec = 300; | ||
| const int kMaxBandTailStillSec = 60; | ||
| const int kMinBandOffsetTrimSec = 600; | ||
|
|
||
| /// The night's new end (same convention as the chosen group's `end`), or null. | ||
| /// | ||
| /// [tsSec] ascending, 1:1 with [bandState]; a state outside 0..3 is ABSENT. | ||
| int? bandTrimmedOffsetSec({ | ||
| required int startSec, | ||
| required int endSec, | ||
| required List<int> tsSec, | ||
| required List<int> bandState, | ||
| int minNightSec = 3 * 3600, | ||
| }) { | ||
| if (endSec <= startSec || tsSec.length != bandState.length) return null; | ||
| bool known(int s) => s >= 0 && s <= 3; | ||
|
|
||
| var lo = 0, hi = tsSec.length; | ||
| while (lo < hi) { | ||
| final mid = (lo + hi) >> 1; | ||
| if (tsSec[mid] < startSec) { | ||
| lo = mid + 1; | ||
| } else { | ||
| hi = mid; | ||
| } | ||
| } | ||
| final first = lo; | ||
|
|
||
| var knownSec = 0; | ||
| int? lastSleepSec; | ||
| int? lastCounted; // tsSec is ascending: count each distinct second once | ||
| for (var k = first; k < tsSec.length && tsSec[k] < endSec; k++) { | ||
| if (!known(bandState[k])) continue; | ||
| if (tsSec[k] != lastCounted) { | ||
| knownSec++; | ||
| lastCounted = tsSec[k]; | ||
| } | ||
| if (bandState[k] == kBandStateSleep) lastSleepSec = tsSec[k]; | ||
| } | ||
| if (lastSleepSec == null) return null; | ||
| if (knownSec * 100 < (endSec - startSec) * kMinBandStateCoveragePct) return null; | ||
|
|
||
| final newEnd = lastSleepSec + 1; | ||
| final tailSec = endSec - newEnd; | ||
| if (tailSec < kMinBandOffsetTrimSec) return null; | ||
| if (newEnd - startSec < minNightSec) return null; | ||
|
|
||
| // The tail must be continuously observed awake: coverage, longest hole | ||
| // (leading, internal and trailing all count) and no re-settling. | ||
| var tailKnown = 0, stillSec = 0, maxGap = 0; | ||
| var prevKnownTs = lastSleepSec; // the last SLEEP second itself was observed | ||
| int? lastTailCounted, lastStillCounted; | ||
| for (var k = first; k < tsSec.length && tsSec[k] < endSec; k++) { | ||
| final t = tsSec[k]; | ||
| if (t < newEnd || !known(bandState[k])) continue; | ||
| if (bandState[k] == kBandStateStill && t != lastStillCounted) { | ||
| stillSec++; | ||
| lastStillCounted = t; | ||
| } | ||
| if (t == lastTailCounted) continue; | ||
| lastTailCounted = t; | ||
| tailKnown++; | ||
| final gap = t - prevKnownTs - 1; | ||
| if (gap > maxGap) maxGap = gap; | ||
| prevKnownTs = t; | ||
| } | ||
| final trailingGap = endSec - 1 - prevKnownTs; | ||
| if (trailingGap > maxGap) maxGap = trailingGap; | ||
| if (tailKnown * 100 < tailSec * kMinBandTailCoveragePct) return null; | ||
| if (maxGap > kMaxBandTailGapSec) return null; | ||
| if (stillSec > kMaxBandTailStillSec) return null; | ||
| return newEnd; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| import 'package:test/test.dart'; | ||
| import 'package:openstrap_analytics/onehz.dart'; | ||
|
|
||
| const int _t0 = 1700000000; | ||
|
|
||
| /// Positional ts/band arrays from (state, seconds) runs. state -1 = row present | ||
| /// but band state absent; state null = no row at all (a hole in tsSec). | ||
| ({List<int> ts, List<int> band, int end}) _night(List<(int?, int)> runs) { | ||
| final ts = <int>[], band = <int>[]; | ||
| var t = _t0; | ||
| for (final (s, n) in runs) { | ||
| for (var i = 0; i < n; i++, t++) { | ||
| if (s == null) continue; | ||
| ts.add(t); | ||
| band.add(s); | ||
| } | ||
| } | ||
| return (ts: ts, band: band, end: t); | ||
| } | ||
|
|
||
| int? _trim(({List<int> ts, List<int> band, int end}) n) => bandTrimmedOffsetSec( | ||
| startSec: _t0, endSec: n.end, tsSec: n.ts, bandState: n.band); | ||
|
|
||
| void main() { | ||
| test('lie-in after waking: end moves to last SLEEP second + 1', () { | ||
| final n = _night([(2, 8 * 3600), (3, 70 * 60)]); // lie-in after waking | ||
| expect(_trim(n), _t0 + 8 * 3600); | ||
| }); | ||
|
|
||
| test('UP then WAKE tail also trims', () { | ||
| final n = _night([(2, 8 * 3600), (3, 30 * 60), (0, 20 * 60)]); | ||
| expect(_trim(n), _t0 + 8 * 3600); | ||
| }); | ||
|
|
||
| test('mid-night UP then SLEEP again: only the trailing UP is trimmed', () { | ||
| final n = _night([(2, 4 * 3600), (3, 19 * 60), (2, 3 * 3600), (3, 48 * 60)]); | ||
| expect(_trim(n), _t0 + 4 * 3600 + 19 * 60 + 3 * 3600); | ||
| }); | ||
|
|
||
| test('mid-night UP then SLEEP to the end: no trim', () { | ||
| expect(_trim(_night([(2, 4 * 3600), (3, 28 * 60), (2, 3 * 3600)])), isNull); | ||
| }); | ||
|
|
||
| test('tail shorter than 10 min: no trim', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (3, 9 * 60)])), isNull); | ||
| }); | ||
|
|
||
| test('band never SLEEP in the window: no trim (no veto in this plan)', () { | ||
| expect(_trim(_night([(0, 2 * 3600), (1, 3600), (3, 5 * 3600)])), isNull); | ||
| }); | ||
|
|
||
| // Each gap case below has EXACTLY 95 % tail coverage (114 of 120 min), so it | ||
| // passes the coverage gate and fails only on the 6-min (> 5 min) hole. | ||
| test('trailing gap > 5 min: no trim (could hide a return to SLEEP)', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (3, 114 * 60), (null, 6 * 60)])), isNull); | ||
| }); | ||
|
|
||
| test('leading gap > 5 min right after last SLEEP: no trim', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (null, 6 * 60), (3, 114 * 60)])), isNull); | ||
| }); | ||
|
|
||
| test('internal gap > 5 min: no trim', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (3, 60 * 60), (-1, 6 * 60), (3, 54 * 60)])), | ||
| isNull); | ||
| }); | ||
|
|
||
| test('control: same 2 h tail without the hole trims', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (3, 120 * 60)])), _t0 + 7 * 3600); | ||
| }); | ||
|
|
||
| test('short gaps (<= 5 min) at >= 95 % tail coverage still trim', () { | ||
| final n = _night([(2, 7 * 3600), (3, 30 * 60), (null, 2 * 60), (3, 38 * 60)]); | ||
| expect(_trim(n), _t0 + 7 * 3600); | ||
| }); | ||
|
|
||
| test('STILL in the tail (re-settling) > 60 s: no trim', () { | ||
| expect(_trim(_night([(2, 7 * 3600), (3, 30 * 60), (1, 10 * 60), (0, 5 * 60)])), | ||
| isNull); | ||
| }); | ||
|
|
||
| test('whole-window coverage below 80 %: no trim', () { | ||
| expect(_trim(_night([(-1, 6 * 3600), (2, 2 * 3600), (3, 60 * 60)])), isNull); | ||
| }); | ||
|
|
||
| test('trim would drop below 3 h: no trim', () { | ||
| expect(_trim(_night([(2, 2 * 3600 + 50 * 60), (3, 60 * 60)])), isNull); | ||
| }); | ||
|
|
||
| test('all absent (-1, gen4): no trim', () { | ||
| expect(_trim(_night([(-1, 9 * 3600)])), isNull); | ||
| }); | ||
|
|
||
| test('length mismatch or empty window: no trim', () { | ||
| expect(bandTrimmedOffsetSec( | ||
| startSec: _t0, endSec: _t0 + 10, tsSec: const [1, 2], bandState: const [2]), | ||
| isNull); | ||
| expect(bandTrimmedOffsetSec( | ||
| startSec: _t0, endSec: _t0, tsSec: const [], bandState: const []), | ||
| isNull); | ||
| }); | ||
|
|
||
| // Duplicate seconds (callers derive tsSec with tsMs ~/ 1000) must count once. | ||
| ({List<int> ts, List<int> band, int end}) _dupTail( | ||
| int tailSec, bool Function(int i) present) { | ||
| final ts = <int>[], band = <int>[]; | ||
| for (var i = 0; i < 7 * 3600; i++) { | ||
| ts.add(_t0 + i); | ||
| band.add(2); | ||
| } | ||
| for (var i = 0; i < tailSec; i++) { | ||
| if (!present(i)) continue; | ||
| for (var r = 0; r < 2; r++) { | ||
| ts.add(_t0 + 7 * 3600 + i); | ||
| band.add(3); | ||
| } | ||
| } | ||
| return (ts: ts, band: band, end: _t0 + 7 * 3600 + tailSec); | ||
| } | ||
|
|
||
| test('duplicate rows cannot fake tail coverage', () { | ||
| final n = _dupTail(120 * 60, (i) => i.isEven); // 50 % distinct coverage | ||
| expect(_trim(n), isNull); | ||
| }); | ||
|
|
||
| test('duplicate rows in an otherwise complete tail still trim', () { | ||
| final n = _dupTail(60 * 60, (_) => true); | ||
| expect(_trim(n), _t0 + 7 * 3600); | ||
| }); | ||
|
|
||
| test('stager and rule share one "sleep" code', () { | ||
| expect(AdvancedSleepStager.bandStateAsleep, kBandStateSleep); | ||
| }); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.