From c8dd777bab1a036b5a3ee1833784e758c00f7261 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:11:46 +0530 Subject: [PATCH 1/2] fix: vanHeesSleepWindow no longer fabricates epoch-0 for unstamped onset/offset hasTs only checked accel.first.tsMs (the every() branch was dead code, implied by the first check). if the sample at bestStart or the min(bestEnd,n-1) sample was unstamped while the first sample wasn't, offsetMs/onsetMs got set to 0.0 instead of null. now each field checks its own source sample independently. --- lib/src/onehz/sleep/van_hees.dart | 7 +-- test/onehz/sleep_test.dart | 75 +++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/lib/src/onehz/sleep/van_hees.dart b/lib/src/onehz/sleep/van_hees.dart index be16176..532a52a 100644 --- a/lib/src/onehz/sleep/van_hees.dart +++ b/lib/src/onehz/sleep/van_hees.dart @@ -398,9 +398,10 @@ Metric vanHeesSleepWindow( ); } - final hasTs = accel.every((a) => a.tsMs != 0) || accel.first.tsMs != 0; - final onsetMs = hasTs ? accel[bestStart].tsMs : null; - final offsetMs = hasTs ? accel[math.min(bestEnd, n - 1)].tsMs : null; + final onsetSample = accel[bestStart]; + final offsetSample = accel[math.min(bestEnd, n - 1)]; + final onsetMs = onsetSample.tsMs != 0 ? onsetSample.tsMs : null; + final offsetMs = offsetSample.tsMs != 0 ? offsetSample.tsMs : null; var unresolved = 0; for (final u in immobileUnknown) { diff --git a/test/onehz/sleep_test.dart b/test/onehz/sleep_test.dart index 418f6b6..885e343 100644 --- a/test/onehz/sleep_test.dart +++ b/test/onehz/sleep_test.dart @@ -133,6 +133,81 @@ void main() { expect(m.present, isFalse); expect(m.confidence, 0); }); + + test('offset sample unstamped (tsMs==0) → offsetMs null, onsetMs kept', () { + // Same day/night/day shape as above, but only the LAST sample in the + // still block carries tsMs==0 (a placeholder/unstamped sample) while + // sample[0] is stamped — regression for the redundant hasTs check that + // only looked at accel.first.tsMs and could fabricate offsetMs=0.0. + final accel = []; + var t = 1000.0; + for (var i = 0; i < 12 * 3600; i++) { + final phase = math.sin(i * 0.5); + accel.add(AccelSample(t, 0.3 * phase, 0.3, 0.9 * (1 - 0.2 * phase))); + t += 1000.0; + } + for (var i = 0; i < 7 * 3600; i++) { + accel.add(AccelSample(t, 0.02, 0.02, 1.0)); + t += 1000.0; + } + for (var i = 0; i < 5 * 3600; i++) { + final phase = math.sin(i * 0.5); + accel.add(AccelSample(t, 0.3 * phase, 0.3, 0.9 * (1 - 0.2 * phase))); + t += 1000.0; + } + // Zero out the very last sample's timestamp only. + accel[accel.length - 1] = AccelSample( + 0, + accel.last.x, + accel.last.y, + accel.last.z, + ); + + final m = vanHeesSleepWindow(accel); + expect(m.present, isTrue); + final w = m.value!; + expect(w.onsetMs, isNotNull); + if (w.offsetIdx >= accel.length - 1) { + expect(w.offsetMs, isNull, + reason: 'unstamped offset sample must not fabricate epoch 0'); + } + }); + + test('onset sample unstamped (tsMs==0) → onsetMs null, offsetMs kept', () { + final accel = []; + var t = 1000.0; + for (var i = 0; i < 12 * 3600; i++) { + final phase = math.sin(i * 0.5); + accel.add(AccelSample(t, 0.3 * phase, 0.3, 0.9 * (1 - 0.2 * phase))); + t += 1000.0; + } + final nightStart = accel.length; + for (var i = 0; i < 7 * 3600; i++) { + accel.add(AccelSample(t, 0.02, 0.02, 1.0)); + t += 1000.0; + } + for (var i = 0; i < 5 * 3600; i++) { + final phase = math.sin(i * 0.5); + accel.add(AccelSample(t, 0.3 * phase, 0.3, 0.9 * (1 - 0.2 * phase))); + t += 1000.0; + } + // Zero out only the first still-block sample's timestamp. + accel[nightStart] = AccelSample( + 0, + accel[nightStart].x, + accel[nightStart].y, + accel[nightStart].z, + ); + + final m = vanHeesSleepWindow(accel); + expect(m.present, isTrue); + final w = m.value!; + if (w.onsetIdx == nightStart) { + expect(w.onsetMs, isNull, + reason: 'unstamped onset sample must not fabricate epoch 0'); + } + expect(w.offsetMs, isNotNull); + }); }); // ---------------------------------------------------------------------- SRI From 70b5073a9d09b607161f89413c5666f511375991 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Sat, 19 Sep 2026 14:17:02 +0530 Subject: [PATCH 2/2] tighten van hees regression tests per coderabbit the offset test was zeroing accel.length-1's timestamp but bestEnd doesn't land on the true block boundary (same edge tolerance the square-wave test already accepts), so the old assert was gated behind an if that never fired. zero a window near the boundary instead and assert the detector actually landed inside it before checking offsetMs. same tightening on the onset test (exact index assert instead of an if-guard). --- test/onehz/sleep_test.dart | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/test/onehz/sleep_test.dart b/test/onehz/sleep_test.dart index 885e343..422f5aa 100644 --- a/test/onehz/sleep_test.dart +++ b/test/onehz/sleep_test.dart @@ -135,8 +135,8 @@ void main() { }); test('offset sample unstamped (tsMs==0) → offsetMs null, onsetMs kept', () { - // Same day/night/day shape as above, but only the LAST sample in the - // still block carries tsMs==0 (a placeholder/unstamped sample) while + // Same day/night/day shape as above, but only the LAST sample of the + // still block (i.e. the actual offsetIdx sample) carries tsMs==0 while // sample[0] is stamped — regression for the redundant hasTs check that // only looked at accel.first.tsMs and could fabricate offsetMs=0.0. final accel = []; @@ -150,27 +150,31 @@ void main() { accel.add(AccelSample(t, 0.02, 0.02, 1.0)); t += 1000.0; } + final nightEnd = accel.length; // last still index + 1 for (var i = 0; i < 5 * 3600; i++) { final phase = math.sin(i * 0.5); accel.add(AccelSample(t, 0.3 * phase, 0.3, 0.9 * (1 - 0.2 * phase))); t += 1000.0; } - // Zero out the very last sample's timestamp only. - accel[accel.length - 1] = AccelSample( - 0, - accel.last.x, - accel.last.y, - accel.last.z, - ); + // Zero out the timestamps of the still block's last 10 min — the + // detector's edge tolerance means bestEnd can land a bit before the + // true boundary (mirrors the ±10min tolerance the square-wave test + // above already accepts), so cover a window instead of one index. + final zeroWindowStart = nightEnd - 600; + for (var idx = zeroWindowStart; idx < nightEnd; idx++) { + accel[idx] = AccelSample(0, accel[idx].x, accel[idx].y, accel[idx].z); + } final m = vanHeesSleepWindow(accel); expect(m.present, isTrue); final w = m.value!; + // Sanity: the detector actually landed inside the zeroed window, or + // this test proves nothing. + expect(w.offsetIdx, inInclusiveRange(zeroWindowStart, nightEnd), + reason: 'test setup must target the sample the code actually reads'); expect(w.onsetMs, isNotNull); - if (w.offsetIdx >= accel.length - 1) { - expect(w.offsetMs, isNull, - reason: 'unstamped offset sample must not fabricate epoch 0'); - } + expect(w.offsetMs, isNull, + reason: 'unstamped offset sample must not fabricate epoch 0'); }); test('onset sample unstamped (tsMs==0) → onsetMs null, offsetMs kept', () { @@ -202,10 +206,10 @@ void main() { final m = vanHeesSleepWindow(accel); expect(m.present, isTrue); final w = m.value!; - if (w.onsetIdx == nightStart) { - expect(w.onsetMs, isNull, - reason: 'unstamped onset sample must not fabricate epoch 0'); - } + expect(w.onsetIdx, nightStart, + reason: 'test setup must target the sample the code actually reads'); + expect(w.onsetMs, isNull, + reason: 'unstamped onset sample must not fabricate epoch 0'); expect(w.offsetMs, isNotNull); }); });