Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/src/onehz/foundations/ewma_baselines.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
9 changes: 7 additions & 2 deletions lib/src/onehz/wellness/temp_circadian.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions test/onehz/wellness_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <AdcSample>[
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);
});
});

// -------------------------------------------------------------------------
Expand Down
Loading