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
16 changes: 14 additions & 2 deletions lib/src/onehz/human/readiness_glassbox.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

import '../types.dart';
import '../util.dart';
import '../foundations/baseline.dart';
import '../foundations/baseline.dart' show robustBaseline, dispersionBelowQuantum;

/// One readiness input the caller supplies.
class GlassBoxInput {
Expand All @@ -39,12 +39,16 @@ class GlassBoxInput {
final double weight; // relative weight (HRV>RHR>RR>temp)
/// If true, a LOWER value is better-for-you (e.g. RHR, resp, temp deviation).
final bool lowerIsBetter;
/// The input's own measurement quantum (e.g. 1 whole bpm, 1 ADC count).
/// 0 = continuous/not quantized, no guard. See the SWC gate below.
final double quantum;
const GlassBoxInput({
required this.label,
required this.value,
required this.history,
required this.weight,
this.lowerIsBetter = false,
this.quantum = 0,
});
}

Expand Down Expand Up @@ -190,7 +194,15 @@ Metric<GlassBoxReadiness> glassBoxReadiness(
final base = robustBaseline(inp.history, minValid: minHistory);
final scale = base.scale;
final delta = (base.center == null) ? 0.0 : (inp.value - base.center!);
final beyond = scale != null && scale > 0 && delta.abs() >= 0.5 * scale;
// A whole-bpm RHR (or integer skin-temp ADC) baseline alternating between
// two adjacent values can carry a nonzero-but-unresolvable MAD that is
// really quantization noise, not physiology (the same 58/59-bpm case
// readiness_composite.dart and overreaching_conjunction.dart guard against
// on this exact channel). If the baseline's dispersion doesn't clear the
// input's own measurement quantum, don't name it as "beyond usual spread".
final quantized = dispersionBelowQuantum(inp.history, inp.quantum);
final beyond =
!quantized && scale != null && scale > 0 && delta.abs() >= 0.5 * scale;

final contribution = inp.weight * (oriented - 50.0);
items.add(ReadinessBreakdownItem(
Expand Down
21 changes: 21 additions & 0 deletions test/onehz/human_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,27 @@ void main() {
expect(v.narrative.toLowerCase(), isNot(contains('noise')));
});

test('a whole-bpm quantized baseline is not named a driver even past 0.5*scale',
() {
// Alternating 58/59 bpm: MAD=0.5 -> scaled MAD ~0.74, so 0.5*scale ~0.37
// is cleared by essentially every night -- but that's 1-bpm rounding
// noise, not real physiology. quantum:1 must suppress it.
final rhr = List<double>.generate(14, (i) => i.isEven ? 58.0 : 59.0);
final inputs = [
GlassBoxInput(
label: 'rhr',
value: 58.0,
history: rhr,
weight: wRhr,
lowerIsBetter: true,
quantum: 1),
];
// ignore: deprecated_member_use_from_same_package
final m = glassBoxReadiness(inputs);
expect(m.value!.breakdown.single.beyondUsualSpread, isFalse);
expect(m.value!.drivers, isEmpty);
});

test('a mover inside the usual spread is never named as a driver', () {
// All inputs essentially at their median => nothing clears the SWC.
final hist = List<double>.generate(20, (i) => 50.0 + (i % 5) * 0.1);
Expand Down
Loading