From 0c19d863dc7e68263df0c349dbe84822045b1319 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Mon, 21 Sep 2026 20:45:07 +0530 Subject: [PATCH] fix: temp_circadian withholds IS/IV instead of fabricating 0.0 on a flat series MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _nonparam divided by varTot/diffN without the zero-variance guard that circadian_np.dart already has, so a flat or heavily-quantized skin-temp window reported interdailyStability=0.0 / intradailyVariability=0.0 as if measured, instead of null. Matches the sibling file's existing `if (ssTot == 0) return absent` discipline. Also marked the dormant zero-baseline ratio fallback in ewma_baselines.dart with a ponytail comment — unreachable today since every metricCfg has minVal > 0, but worth flagging before a future metric legitimately crosses zero. --- lib/src/onehz/foundations/ewma_baselines.dart | 4 ++++ lib/src/onehz/wellness/temp_circadian.dart | 9 +++++++-- test/onehz/wellness_test.dart | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/src/onehz/foundations/ewma_baselines.dart b/lib/src/onehz/foundations/ewma_baselines.dart index fe5d2fa..c53fc99 100644 --- a/lib/src/onehz/foundations/ewma_baselines.dart +++ b/lib/src/onehz/foundations/ewma_baselines.dart @@ -325,6 +325,10 @@ class Baselines { final sigma = math.max(1.253 * state.spread, 1e-9); final z = (value - state.baseline) / sigma; final delta = value - state.baseline; + // ponytail: silent 0.0 fallback if baseline is ever exactly 0 — every + // metricCfg today has minVal > 0 so update() can never seed a zero + // baseline; revisit (make ratio nullable) before adding a metric whose + // range legitimately crosses zero. final ratio = state.baseline != 0 ? (value / state.baseline - 1.0) : 0.0; return Deviation( z: z, delta: delta, ratio: ratio, inNormalRange: z.abs() <= 1.0); diff --git a/lib/src/onehz/wellness/temp_circadian.dart b/lib/src/onehz/wellness/temp_circadian.dart index 89f33d1..8294d8d 100644 --- a/lib/src/onehz/wellness/temp_circadian.dart +++ b/lib/src/onehz/wellness/temp_circadian.dart @@ -356,8 +356,13 @@ CircadianNonparam? _nonparam( final d = v - grand; varTot += d * d; } + // A flat/quantized-ADC window has zero total variance and zero + // consecutive-difference energy — IS and IV are both undefined divisions + // by zero there, not "perfectly stable" 0.0 readings (matches + // circadian_np.dart's `if (ssTot == 0) return absent` discipline). + if (varTot == 0 || diffN == 0) return null; final p = present.length; - final iv = (diffN > 0 && varTot > 0) ? (diffSq / diffN) / (varTot / p) : 0.0; + final iv = (diffSq / diffN) / (varTot / p); // IS: between-day stability. Average each within-day epoch-of-day across days, // then variance-of-the-24h-profile / total variance. @@ -393,7 +398,7 @@ CircadianNonparam? _nonparam( // circadian_np.dart's `p = epochsPerDay` normalization. profVar /= epochsPerDay; } - final double is_ = varTot > 0 ? (profVar / (varTot / p)).clamp(0, 1) : 0.0; + final double is_ = (profVar / (varTot / p)).clamp(0, 1); // M10 / L5 / RA are DELIBERATELY NOT COMPUTED. See the file header: the temp // series that survives retention is median-centred, so it is signed, and diff --git a/test/onehz/wellness_test.dart b/test/onehz/wellness_test.dart index 1b10623..8304b56 100644 --- a/test/onehz/wellness_test.dart +++ b/test/onehz/wellness_test.dart @@ -176,6 +176,23 @@ void main() { expect(m.present, isFalse); expect(m.confidence, 0); }); + + test( + 'nonparam is withheld (not a fabricated 0.0) on a flat/' + 'zero-variance series', () { + // 3 days of an identical ADC reading -> varTot == 0 and diffN's + // squared-difference sum == 0. IS/IV are both undefined divisions by + // zero here, not "perfectly stable / perfectly regular" measurements. + // The cosinor half can still fit a (degenerate) phase on flat input, + // so the overall Metric stays present — it's specifically `nonparam` + // that must come back null instead of IS=0.0/IV=0.0. + final samples = [ + for (var i = 0; i < 3 * 24 * 6; i++) + AdcSample(i * 10 * 60 * 1000.0, 2000.0), + ]; + final m = tempCircadian(samples, deviceFamily: 'gen4', epochMin: 60); + expect(m.value!.nonparam, isNull); + }); }); // -------------------------------------------------------------------------