Skip to content

strain: score against a personal quiet level, never let quiet time debit exercise - #91

Merged
abdulsaheel merged 2 commits into
OpenStrap:mainfrom
DropTabl:fix/strain-personal-quiet-hrr
Oct 9, 2026
Merged

abdulsaheel merged 2 commits into
OpenStrap:mainfrom
DropTabl:fix/strain-personal-quiet-hrr

Conversation

@DropTabl

@DropTabl DropTabl commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Why

Headline strain subtracts the cost of being awake as a lump, wakeMinutes · Q·y(Q). MOT-03 made the quiet level Q a required argument so callers would pass the user's own, but nothing turned dailyQuietWakingHrr into 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 below Q nets 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 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.

What changed

  • strainFromNetTrimp: the 0–21 map factored out of strainScore (pure refactor, output 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 == headline and banked exercise never falls back out. quietSettled is 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 with need_baseline below 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.md rows 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, and curve.last is the headline. A running-max ratchet would bias every quiet day upward.

Before / after

day before after
quiet 0.10 HRR + 45 min @145, Q = 0.20 0.00 9.38
same, at the user's own Q = 0.10 — 9.77
anchor profile + 45 min @145 (guarded, Q = 0.20) 8.55 8.88
anchor profile + 90 min @165 16.54 16.65
inactive / 60-min walk / 195 min @160 0.00 / 2.70 / 21.00 unchanged

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-infos clean; dart test green.

Dependencies

The edge PR OpenStrap/edge#551 consumes these APIs and needs this merged and pinned first.

API note: strainScoreMetric now takes a required quietSettled (same as strainScoreFromSeries). 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:

  • Add personal quiet-waking calibration using a trailing median with explicit cold-start and calibrating states.
  • Add per-minute strain scoring and cumulative curves that protect exercise load from quiet-time debits.

Bug Fixes:

  • Prevent quiet periods below the reference level from cancelling exercise and preserve honest zero scores on inactive or jitter-only days.
  • Ensure cumulative strain retains banked exercise contribution while allowing ordinary living load to net out.

Enhancements:

  • Share the strain mapping and accumulator across lump, series, and curve scoring while retaining the lump form for stored TRIMP rescaling.
  • Require quiet-level settlement state and expose consistent absent-input validation and confidence disclosure.
  • Document the new strain and quiet-level algorithms.

Documentation:

  • Update algorithm documentation with personal quiet-level, guarded series scoring, cumulative curves, and lump-score semantics.

Tests:

  • Expand strain calibration coverage for protected exercise scoring, personal quiet-level history, cumulative behavior, calibration confidence, and invalid-input handling.

…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.
@sourcery-ai

sourcery-ai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The 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 scoring

sequenceDiagram
    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>
Loading

File-Level Changes

Change Details Files
Replace whole-day quiet-cost subtraction with a guarded per-minute net TRIMP model that protects exercise from quiet-time debits.
  • Extract the shared TRIMP-to-0–21 mapping.
  • Classify minutes at or above 40% HRR as exercise and accumulate them separately.
  • Net sub-threshold living minutes as a block, floor that block at zero, and add exercise in full.
  • Preserve the lump scorer for stored TRIMP rescaling without a per-minute series.
lib/src/onehz/clinical/load_trimp.dart
ALGORITHMS.md
Add series-based headline and cumulative strain APIs backed by one accumulator.
  • Compute headline strain from per-minute HR with required anchors, personal quiet level, and explicit calibration state.
  • Generate a cumulative curve whose final value equals the headline and whose post-exercise values cannot fall below exercise-only strain.
  • Return absent metrics or null curves for invalid anchors, quiet levels, or unworn input.
lib/src/onehz/clinical/load_trimp.dart
Derive a personal quiet-waking baseline from prior daily measurements instead of using the population reference.
  • Use the median of up to 28 valid prior daily levels.
  • Abstain below three valid days and mark three through six days as calibrating with reduced confidence.
  • Expose level metadata and settled status while filtering invalid history.
lib/src/onehz/clinical/load_trimp.dart
ALGORITHMS.md
Expand calibration coverage for guarded scoring, curves, personal baselines, and absence behavior.
  • Verify low-quiet exercise is not erased and nothing-day jitter preserves honest zeros.
  • Pin anchor outputs, exercise-floor guarantees, non-monotone curve behavior, invalid-minute handling, and headline/curve consistency.
  • Test cold-start, calibrating, settled, trailing-window, invalid-level, and missing-input cases.
test/onehz/strain_calibration_test.dart

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You'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.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 05bacd05-bf36-4e4b-8d05-83522bd3b54e
📥 Commits

Reviewing files that changed from the base of the PR and between 27b0ba4 and 4e67d3c.

📒 Files selected for processing (4)
  • ALGORITHMS.md
  • lib/src/onehz/clinical/load_trimp.dart
  • test/onehz/clinical_test.dart
  • test/onehz/strain_calibration_test.dart
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/src/onehz/clinical/load_trimp.dart
Comment thread lib/src/onehz/clinical/load_trimp.dart
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.
@abdulsaheel
abdulsaheel merged commit d378cda into OpenStrap:main Oct 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants