Skip to content

autoDetectWorkouts: bypass motion gate on genuine HR onset (edge#170) - #32

Merged
abdulsaheel merged 2 commits into
mainfrom
fix/issue-170-low-motion-cardio-onset
Jul 31, 2026
Merged

autoDetectWorkouts: bypass motion gate on genuine HR onset (edge#170)#32
abdulsaheel merged 2 commits into
mainfrom
fix/issue-170-low-motion-cardio-onset

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • edge#170: a real 1h50m cycling session (avg ~117W, HR 100-140, peak 157 bpm) produced no suggested workout — AutoWorkoutDetector's motion-confirmation gate (motionConfirmMean = 0.15) is tuned for arm-swing activities and unconditionally drops any window where the wrist stays still, which is exactly what happens gripping a handlebar/oar during cycling/rowing, regardless of HR signal strength.
  • A raw %HRR-based bypass was tried first and rejected: it reopens the exact false positive the motion gate exists to catch — fever/anxiety/heat plateaus would qualify too, since raw HR level doesn't distinguish "just started exercising" from "already elevated."
  • Fix: bypass the motion gate only on a genuine HR onset — mean bpm over the first 3 min of the candidate span must rise ≥25 bpm vs. the 3 min immediately preceding it (Whipp & Wasserman 1972 phase-II kinetics: real exertion produces a fast HR rise right at onset; slow drifts don't). No usable pre-window → abstain, motion gate stays in force.

Test plan

  • dart test — 386 passed (was 383; +3 net from new/restructured workout tests), no failures.
  • New test: sharp HR step + low motion (cycling-shaped) → now suggested.
  • New test: slow HR ramp (fever/anxiety-shaped) + low motion → still dropped.
  • New test: no pre-window (data starts mid-elevation) + low motion → still dropped (can't evaluate onset, abstain).
  • Existing high-motion path unaffected (unchanged assertion, split into its own test).

Summary by CodeRabbit

  • New Features

    • Improved workout detection for low-motion cycling and rowing by recognizing sharp heart-rate increases at workout onset.
  • Bug Fixes

    • Low-motion workouts are now retained when heart-rate evidence is strong and complete.
    • Gradual heart-rate elevation and data without a preceding resting period continue to be rejected.

edge#170: a real 1h50m cycling session (avg ~117W, HR 100-140, peak 157)
produced no suggested workout at all. The motion-confirmation gate
(motionConfirmMean = 0.15 mean L2 gravity-delta/s) is tuned for
arm-swing activities (walking/running) and unconditionally drops any
window where the wrist stays still — which is exactly what happens on
a handlebar/oar during cycling/rowing, regardless of how strong the HR
signal is.

A raw %HRR bypass (skip motion above some higher HR-reserve fraction)
was considered and rejected: it re-opens the exact false positive the
gate exists to catch — any sustained elevation (fever, anxiety, heat)
would qualify, since level alone doesn't distinguish "started
exercising" from "already elevated."

Instead, bypass on HR ONSET kinetics: mean bpm over the first 3 min of
the candidate span must rise by >=25 bpm versus the 3 min immediately
before it. Real aerobic effort shows a fast phase-II HR kinetic
response right as exertion begins (Whipp & Wasserman 1972); a
slow-drifting plateau (fever/heat/anxiety climbing over many minutes,
or data that starts mid-elevation with no visible onset) shows no such
rise and stays gated. No usable pre-window -> abstain (never fabricate
an onset that isn't there).
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@abdulsaheel, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7cd3fc34-4d66-4e31-bb8a-143b3d954b25

📥 Commits

Reviewing files that changed from the base of the PR and between 422c8b8 and cbbe06a.

📒 Files selected for processing (1)
  • lib/src/onehz/workout/auto_detect.dart
📝 Walkthrough

Walkthrough

The workout detector now evaluates HR onset when motion confirmation fails. It compares pre-span and initial-span mean BPM values and accepts only spans with sufficient HR rise and complete sample coverage. Tests cover accepted and rejected low-motion cases.

Changes

Workout HR-onset detection

Layer / File(s) Summary
HR-onset detection logic
lib/src/onehz/workout/auto_detect.dart
The detector defines HR lookback, evaluation-window, and rise thresholds. It computes half-open-range mean BPM values and uses them to evaluate low-motion spans.
Onset detection test coverage
test/onehz/workout_test.dart
Tests cover high-motion retention, sharp HR-onset acceptance, gradual elevation rejection, and rejection when no preceding samples exist.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: bypassing the motion gate when genuine heart-rate onset is detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/src/onehz/workout/auto_detect.dart`:
- Around line 297-298: Update the earlyMean calculation in the auto-detection
flow to pass the exclusive upper bound for exactly the first three minutes,
removing the extra one-second increment from the _meanBpmInRange call. Preserve
the existing end and onsetWindowS bounds while ensuring its lo <= ts < hi range
covers only 180 seconds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4886f6ce-b2d4-478b-9251-8404ccc4f8fc

📥 Commits

Reviewing files that changed from the base of the PR and between 5d42191 and 422c8b8.

📒 Files selected for processing (2)
  • lib/src/onehz/workout/auto_detect.dart
  • test/onehz/workout_test.dart

Comment thread lib/src/onehz/workout/auto_detect.dart Outdated
Comment on lines +297 to +298
final earlyMean = _meanBpmInRange(
ts, bpm, start, math.min(end, start + onsetWindowS) + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use exactly the first three minutes for earlyMean.

_meanBpmInRange uses lo <= ts < hi. Line 298 adds one second, so it evaluates 181 seconds. This can change a result at the 25 BPM boundary.

Proposed fix
 final earlyMean = _meanBpmInRange(
-    ts, bpm, start, math.min(end, start + onsetWindowS) + 1);
+    ts, bpm, start, start + onsetWindowS);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/onehz/workout/auto_detect.dart` around lines 297 - 298, Update the
earlyMean calculation in the auto-detection flow to pass the exclusive upper
bound for exactly the first three minutes, removing the extra one-second
increment from the _meanBpmInRange call. Preserve the existing end and
onsetWindowS bounds while ensuring its lo <= ts < hi range covers only 180
seconds.

CodeRabbit caught this on #32: _meanBpmInRange uses a half-open
[lo, hi) range. preMean correctly used [start-onsetLookbackS, start)
(180 samples), but earlyMean used
[start, min(end, start+onsetWindowS)+1) — the trailing +1 made it
181 samples when the span outlasts the onset window, asymmetric with
the pre-window it's compared against. Fixed to
[start, min(end+1, start+onsetWindowS)): matches the 180-sample width
when the span is long enough, while still including the span's last
sample (end, inclusive elsewhere in this file) when the span itself is
shorter than onsetWindowS.
@abdulsaheel
abdulsaheel merged commit 656b33f into main Jul 31, 2026
3 checks passed
@abdulsaheel
abdulsaheel deleted the fix/issue-170-low-motion-cardio-onset branch July 31, 2026 17:24
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.

1 participant