Skip to content

feat(biometrics): let the HR estimator report nothing instead of a weak-signal guess - #840

Merged
ng merged 3 commits into
devfrom
feat/hr-estimator-guard
Oct 18, 2026
Merged

ng merged 3 commits into
devfrom
feat/hr-estimator-guard

Conversation

@ng

@ng ng commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Why

The local runner (#837) showed that tuning the heart-rate tracker can't bring pod HR in line with the Apple Watch.

Across the 8 nights with Watch HR (10-08 to 10-16, 526 paired readings):

  • Most readings are close: the median error is 0.94 bpm.
  • A few are far off: 19% of readings miss by more than 8 bpm and account for 72% of the total error.
  • The tracker settings can't fix it. Every tolerance and history combination moved error by at most 0.006 bpm on the new nights.

Cause. I logged the subharmonic-summation candidates on night 10-14. In 15 of 18 big misses, the Watch's true rate was not a candidate at all: the autocorrelation had a single peak between 45 and 120 bpm, at the wrong rate (Watch 52, pod 62–65). The tracker passed 95 of 98 misses straight through, because its consistency rule accepts jumps of up to about 1.55× the tolerance.

  • What separates the misses is signal strength. The winning peak score has a median of 0.34 on misses versus 0.60 on good windows, but the estimator accepted anything at or above a hard-coded 0.1.
  • Ruled out: pump or other-side coupling. The other channel matched the wrong value in only 2 of 9 misses.

What

  • New profile parameter heart_rate.minimumPeakScore, range 0.1–0.8, shipped default 0.4. It replaces the hard-coded 0.1 floor in subharmonic_summation_hr. Windows scoring below it report no heart rate instead of a guess, which changes default output on weak-signal windows for every pod. A profile can set 0.1 to restore the old behaviour.
  • Manifest bumped to 1.1.0 with an updated sourceHashes entry for signal.py. The calibration card gets a slider row for the parameter, and i18n catalogs are re-extracted.
  • Local runner:
    • Ranks by each candidate's own HR error among candidates keeping at least --min-coverage (default 75%) of the defaults' Watch coverage. The old common-support statistic scores any abstaining change as exactly 0.000, so it couldn't see this change.
    • Adds --save-readings for error analysis.

Evidence (local replay of the real code through the pod's evaluator; equal-night means)

minimumPeakScore Dev MAE Dev p95 Dev coverage Held-back MAE Held-back p95 Held-back coverage
0.1 (default) 5.21 16.5 100% 4.34 16.6 100%
0.35 2.91 10.0 83% 3.47 14.7 82%
0.4 1.75 6.8 77% 3.10 14.1 74%
0.45 1.17 4.5 70% 2.80 13.8 66%
0.5 1.17 4.7 64% 2.02 10.4 51%
  • Dev nights: 10-08, 10-10, 10-11, 10-14, 10-15. Held-back nights: 10-12, 10-13, 10-16.
  • Rejected: a stricter tracker that drops uncorrectable jumps changed nothing. A 3-window median helped little and lost to the score floor.
  • Not fixed: a +2 to +3 bpm high bias remains on the held-back nights, even with the gate.

Deploy note

The manifest hash changes. Existing profile revisions pin the old manifest, so after deploying, those revisions (including the left side's active one) report PROFILE_INCOMPATIBLE: pinned engine is not available in this build and must be re-proposed against the new manifest. This is the existing versioning contract; no migration is added here.

Test plan

  • pytest modules/common (419) and modules/piezo-processor (204), plus new tests: abstention below the threshold, unchanged default, range validation
  • vitest: runner ranking (abstention credited, coverage floor enforced), calibration UI (231), registry source-hash pin, demo evaluator bundle re-pinned
  • Full vitest suite: two failures under full load, updateDeploy and the waveform publication integration test, both pass alone
  • tsc --noEmit, eslint
  • After deploy: re-propose the left profile (it inherits 0.4), apply it, then compare the next nights against Watch HR

🤖 Generated with Claude Code

🛏️ Beam this PR onto a sleepypod ✨

Pick the snippet that matches your pod. Already-installed pods can't use the curl bootstraps below — sleepypod's egress firewall DROPs github.com, and the script that knows how to unblock WAN is the very file curl is trying to fetch. Use sp-update instead; it's on disk and opens WAN as its first step.

🔄 Already on a sleepypod (most common — review a PR on a running pod):

sudo sp-update feat/hr-estimator-guard

🚀 Fresh pod / first install — bootstraps from GitHub, rebuilds every push:

curl -fsSL https://raw.githubusercontent.com/sleepypod/core/feat/hr-estimator-guard/scripts/install \
  | sudo bash -s -- --branch feat/hr-estimator-guard

🎯 Pin to this exact build (run 39088122122 · 54ea17c) — fresh-pod path, locked to one CI artifact for reproducible review:

curl -fsSL https://raw.githubusercontent.com/sleepypod/core/54ea17cb16378b044ded72a8b980894af70b78e3/scripts/install \
  | sudo bash -s -- \
      --branch feat/hr-estimator-guard \
      --artifact-url 'https://nightly.link/sleepypod/core/actions/runs/39088122122/sleepypod-core.zip'

🧊 Artifact: sleepypod-core · self-destructs in 30 days 💥

@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sleepypod-demo Ignored Ignored Preview Oct 18, 2026 1:53am UTC

@coderabbitai

coderabbitai Bot commented Oct 17, 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 32 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: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 217a6dfe-9007-42a5-b2f1-29a248847d88
📥 Commits

Reviewing files that changed from the base of the PR and between 36f1dc3 and 54ea17c.

📒 Files selected for processing: 16
  • modules/common/biometrics/manifest.v1.json
  • modules/common/biometrics/signal.py
  • modules/piezo-processor/test_shared_signal.py
  • scripts/biometrics-local-fit/README.md
  • scripts/biometrics-local-fit/cli.ts
  • scripts/biometrics-local-fit/fit.ts
  • scripts/biometrics-local-fit/report.test.ts
  • scripts/biometrics-local-fit/report.ts
  • src/biometrics/algorithms/registry-core.ts
  • src/components/biometrics/Calibration/CalibrationProfileCard.tsx
  • src/components/biometrics/Calibration/calibration-model.ts
  • src/components/biometrics/Calibration/tests/BiometricCalibration.test.tsx
  • src/demo/biometrics/worker-build.json
  • src/lib/i18n/locales/en.po
  • src/lib/i18n/locales/es.po
  • src/lib/i18n/locales/pseudo.po
  • Autofix · 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.

@codecov

codecov Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

@ng
ng force-pushed the feat/biometrics-local-fit branch from 4d3359c to 8319f8b Compare October 18, 2026 01:18
@ng
ng force-pushed the feat/hr-estimator-guard branch from 1e3d2f7 to 8dfef8b Compare October 18, 2026 01:21
@ng

ng commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit (mirror ng#25) findings, addressed in 8dfef8b:

  • Fixed: --min-coverage must be a finite fraction from 0 to 1. It's checked before any extraction or replay, and rankCandidates enforces the same range.
  • Fixed: added Spanish translations for the beat-score slider and its label.

Rebased onto the updated #837.

Base automatically changed from feat/biometrics-local-fit to dev October 18, 2026 01:33
@ng
ng force-pushed the feat/hr-estimator-guard branch from 8dfef8b to 54ea17c Compare October 18, 2026 01:53
@ng
ng enabled auto-merge October 18, 2026 01:54
@ng
ng added this pull request to the merge queue Oct 18, 2026
Merged via the queue into dev with commit 928d7ec Oct 18, 2026
17 of 18 checks passed
@ng
ng deleted the feat/hr-estimator-guard branch October 18, 2026 02:07
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