Repository navigation
strain: score against a personal quiet level, never let quiet time debit exercise - #91
Conversation
…bit exercise Headline strain subtracts the cost of being awake as a lump, wakeMinutes * Q*y(Q). Two things were wrong with it once Q became an argument (MOT-03): - There was no way to turn dailyQuietWakingHrr into the trailing trait its own doc asks for, so callers kept passing the 0.20 reference. - The lump is not one-sided. Every quiet minute below Q nets a negative term against the whole day, exercise included. A user who sits at 0.10 HRR has a 45-min run at 145 bpm (RHR 50, HRmax 187) cancelled to 0.00/21 against Q = 0.20, and the cumulative curve climbs through the run and falls back to 0 by bedtime. Even with a perfectly personal Q, a day whose quiet stretch sits 0.03 HRR under it halves that run. Changes: - strainFromNetTrimp: the 0-21 map factored out of strainScore. Pure refactor; strainScore's output is unchanged. - netTrimpAboveQuiet / NetTrimp: minutes at or above 40 % HRR (ACSM's moderate floor, the same boundary as maxQuietHrr) are exercise and are never offset. Every other minute nets among itself and floors at 0 as a block. With no exercise minute this equals the lump form, so the honest zeros MOT-05 protected stay zeros. A full per-sample floor is deliberately not used: alternating 0.17/0.23 HRR around 0.20 scores 3.86 that way vs 0.46 here. - strainScoreFromSeries / strainCurveFromSeries: headline and cumulative curve from one shared accumulator, so curve.last equals the headline and banked exercise never falls back out. - personalQuietWakingHrr / QuietLevel: median of the trailing 28 prior daily levels. Abstains with need_baseline below 3 days and never falls back to 0.20. 3-6 days scores at lower confidence with a "calibrating" note. The curve is deliberately not monotone. Positive living banked before a run (a busy morning above Q) can be netted back out by a quiet evening below Q: 120 min at 0.30 HRR, 45 min at 145 bpm, then 735 min at 0.10 HRR reads 10.74 at the end of the run and 9.38 at the end of the day. What is guaranteed, documented and tested: the exercise contribution never decreases after the last exercise minute, the curve never drops below what the exercise alone scores, and curve.last is the headline. A running-max ratchet would hide the dip but bias every quiet day upward (the running max of a random walk around Q drifts up), which is the MOT-05 inflation the exercise/living split avoids. Before/after, low-quiet user (0.10 HRR) + 45 min @145, Q = 0.20: 0.00 -> 9.38; at their own Q = 0.10: 9.77. Anchor profile, guarded at Q = 0.20: inactive 0.00, walk 2.70, run 8.55 -> 8.88, hard 16.54 -> 16.65, 195 min @160 still 21.00. strainScore / strainScoreMetric (lump) stay for rescaling a stored TRIMP that has no per-minute series left.
Reviewer's GuideThe PR introduces personal quiet-level calibration and series-based strain scoring that separates exercise from ordinary living, preventing quiet time from offsetting exercise while preserving the legacy lump scorer for data without per-minute HR; a shared accumulator now drives both headline and cumulative results, with extensive calibration and edge-case tests. Sequence diagram for personal quiet-level strain scoringsequenceDiagram
participant Caller
participant QuietCalibration as personalQuietWakingHrr
participant SeriesScorer as strainScoreFromSeries
participant Accumulator as netTrimpAboveQuiet
participant Mapper as strainFromNetTrimp
Caller->>QuietCalibration: personalQuietWakingHrr(priorDailyLevels)
QuietCalibration-->>Caller: Metric<QuietLevel>
Caller->>SeriesScorer: strainScoreFromSeries(hrPerMin, restingHr, maxHr, quietHrr, quietSettled)
SeriesScorer->>Accumulator: netTrimpAboveQuiet(hrPerMin, restingHr, maxHr, quietHrr, sex)
Accumulator-->>SeriesScorer: NetTrimp(exercise, living, minutes)
SeriesScorer->>Mapper: strainFromNetTrimp(net)
Mapper-->>SeriesScorer: headline strain 0-21
SeriesScorer-->>Caller: Metric<double>
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 45 minutes. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configuration
📒 Files selected for processing (4)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="lib/src/onehz/clinical/load_trimp.dart" line_range="571" />
<code_context>
+
+/// Wake minutes a day needs before its own median may enter the trait series.
+/// A 1-hour window measures that hour, not ordinary living.
+const int quietHrrTraitMinMinutes = 360;
+
+/// A personal quiet-waking level and how many prior days back it.
</code_context>
<issue_to_address>
**Short recordings enter traits**
When a caller builds personal history using `dailyQuietWakingHrr`’s default 60-minute minimum, `personalQuietWakingHrr` receives only HRR values and cannot verify their coverage, so a 60–359-minute day enters the trait despite the 360-minute eligibility rule and can skew later strain scores.
Carry each daily level’s waking-minute coverage into personal history and exclude levels below 360 minutes.
Also at `lib/src/onehz/clinical/load_trimp.dart:597`.
</issue_to_address>
### Comment 2
<location path="lib/src/onehz/clinical/load_trimp.dart" line_range="299-300" />
<code_context>
/// Headline 0–21 strain as a Metric, alongside the raw TRIMP (EST tier).
///
+/// LUMP FORM — see [strainScore]. Kept for rescaling a stored TRIMP that has no
+/// per-minute series; anything holding the series uses [strainScoreFromSeries].
+///
/// [trimp] the raw Banister TRIMP for the day/session, [wakeMinutes] the wake
/// window it was accumulated over, [quietHrr] this user's quiet-waking level
</code_context>
<issue_to_address>
**Calibrating baseline appears settled**
When a caller uses `strainScoreMetric` to rescale stored TRIMP with a quiet level from fewer than seven prior days, when a caller passes the HRR from a 3–6-day `QuietLevel`, `strainScoreMetric` still emits confidence 0.6 and a generic note, so the stored-TRIMP score appears settled despite using a calibrating baseline.
Pass the baseline's settled state into `strainScoreMetric` and return calibrating confidence and a note when it is false.
</issue_to_address>Sourcery assessment
Approval pending. 2 findings to address first.
Blocking findings: lib/src/onehz/clinical/load_trimp.dart:571, lib/src/onehz/clinical/load_trimp.dart:300
strainScoreMetric rescales a stored TRIMP against the same personal quiet level the live scorer uses, but always reported confidence 0.6 and a generic note, so a level backed by 3-6 days looked settled. It now takes the same required quietSettled flag as strainScoreFromSeries: false lowers confidence to 0.45 and says "calibrating", and the value is unchanged. personalQuietWakingHrr's doc now states that each prior level must already be an eligible day (minMinutes: quietHrrTraitMinMinutes), which the caller that persists the daily levels enforces.
Why
Headline strain subtracts the cost of being awake as a lump,
wakeMinutes · Q·y(Q). MOT-03 made the quiet levelQa required argument so callers would pass the user's own, but nothing turneddailyQuietWakingHrrinto the trailing trait its doc asks for, so callers still pass the 0.20 reference. And the lump is not one-sided: every quiet minute belowQnets a negative term against the whole day, exercise included. For a user who sits at 0.10 HRR, a 45-min run at 145 bpm (RHR 50, HRmax 187) scores 0.00/21 againstQ = 0.20, and the cumulative curve climbs through the run and falls back to 0 by bedtime. Even with a perfectly personalQ, a day whose quiet stretch sits 0.03 HRR under it halves that run.What changed
strainFromNetTrimp: the 0–21 map factored out ofstrainScore(pure refactor, output unchanged).netTrimpAboveQuiet/NetTrimp: minutes at or above 40 % HRR (ACSM's moderate floor, the same boundary asmaxQuietHrr) are exercise and are never offset; every other minute nets among itself and floors at 0 as a block. With no exercise minute this equals the lump form, so the honest zeros MOT-05 protected stay zeros. A full per-sample floor is deliberately not used (alternating 0.17/0.23 HRR around 0.20 scores 3.86 that way vs 0.46 here).strainScoreFromSeries/strainCurveFromSeries: headline and cumulative curve from one shared accumulator, socurve.last == headlineand banked exercise never falls back out.quietSettledis required, so a calibrating level can never be reported at settled confidence by omission.personalQuietWakingHrr/QuietLevel: median of the trailing 28 prior daily levels; abstains withneed_baselinebelow 3 days and never falls back to 0.20; 3–6 days scores at lower confidence with a "calibrating" note.strainScore/strainScoreMetric(lump) stay, documented as the form for rescaling a stored TRIMP that has no per-minute series left.ALGORITHMS.mdrows for the new functions.The curve is deliberately not monotone: positive living banked before a run (a busy morning above
Q) can be netted back out by a quiet evening below it (120 min at 0.30 HRR, 45 min at 145 bpm, then 735 min at 0.10 HRR reads 10.74 at the end of the run and 9.38 at the end of the day). Guaranteed and tested: the exercise contribution never decreases after the last exercise minute, the curve never drops below what exercise alone scores, andcurve.lastis the headline. A running-max ratchet would bias every quiet day upward.Before / after
Tests
test/onehz/strain_calibration_test.dart: guarded net vs lump, nothing-day jitter, the anchor table, the exercise floor, the cumulative curve (incl. the non-monotone case),personalQuietWakingHrr(cold start, calibrating, settled, trailing window, invalid levels), and the absent envelope.dart analyze --fatal-infosclean;dart testgreen.Dependencies
The edge PR OpenStrap/edge#551 consumes these APIs and needs this merged and pinned first.
API note:
strainScoreMetricnow takes a requiredquietSettled(same asstrainScoreFromSeries). Edge’s existing callers are replaced by OpenStrap/edge#551, so repin analytics together with that PR.🤖 Generated with Claude Code
Summary by Sourcery
Score per-minute strain against a personal quiet-waking baseline while ensuring quiet time cannot debit exercise load.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Tests: