Skip to content

fix: HRV harmonic gate, 10s windows, rename to HR variability index#222

Merged
ng merged 2 commits intodevfrom
fix/hrv-harmonic-gate-221
Mar 16, 2026
Merged

fix: HRV harmonic gate, 10s windows, rename to HR variability index#222
ng merged 2 commits intodevfrom
fix/hrv-harmonic-gate-221

Conversation

@ng
Copy link
Copy Markdown
Contributor

@ng ng commented Mar 16, 2026

Summary

The previous compute_hrv() produced inflated, clinically meaningless values (120-195ms) labeled as "RMSSD." Three independent expert reviews (cardiologist, DSP engineer, biostatistician) identified the root causes and recommended fixes.

Root causes

  1. Not RMSSD: Window-level IBI (~19 per 5 min) ≠ beat-to-beat IBI (~300+ per 5 min). The metric measures HR trend, not vagal modulation.
  2. Harmonic contamination: ~20% of windows locked to 2nd harmonic (474ms) or sub-harmonic (1458ms), creating 300-600ms successive diffs.
  3. Insufficient filtering: Hampel filter couldn't catch clustered harmonics (local median shifted toward them).

Fixes applied

  • Rename: "HRV/RMSSD" → "HR variability index" in code, docs, and API descriptions
  • Harmonic gate: Trimmed-mean tracker rejects/corrects 2x/0.5x/3x IBIs (18% tolerance)
  • 10s windows (was 30s): ~59 IBI samples per 5 min instead of ~19
  • Gap-aware RMSSD: Skip diffs across rejected-window gaps
  • Range gate: 5-100ms (was 5-200ms)
  • HRValidator cap: 100ms (was 200ms)

Pod validation (2026-03-16)

Before After
HRV range 120-195 ms 20-70 ms
Values >100ms Frequent Zero
Label "RMSSD" (misleading) "HR variability index" (honest)

Future work (#221)

  • Beat-level J-peak detection for real RMSSD (Bruser et al. 2013)
  • ECG validation study (N=10-20, Polar H10)

Test plan

  • 57 unit tests pass
  • TypeScript compilation clean
  • Live pod: HRV 20-70ms over 5 minutes of streaming data
  • Overnight validation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved HRV measurement for more reliable readings, adding harmonic gating, stricter validation (5–100 ms), and refined IBI filtering/windowing.
  • Documentation

    • Clarified the metric as an HR variability index (non-clinical RMSSD), updated windowing parameters, thresholds, and explanatory guidance.
  • Tests

    • Updated tests and expectations to reflect new HR variability index terminology and tightened range/validation rules.

…ity index

The previous compute_hrv() produced inflated values (120-195ms) because:
1. Window-level IBI is NOT beat-to-beat IBI (not clinical RMSSD)
2. ~20% of windows locked to harmonics (474ms, 1458ms) undetected
3. Hampel filter couldn't catch clustered harmonic errors

Fixes (informed by cardiologist, DSP engineer, and biostatistician review):
- Rename: "HRV/RMSSD" → "HR variability index" (honest labeling)
- Harmonic gate: trimmed-mean tracker rejects/corrects 2x/0.5x/3x IBIs
  before Hampel (18% tolerance per DSP expert)
- 10s windows (was 30s): 3x more IBI samples (~59 vs ~19 per 5 min)
- Gap-aware RMSSD: skip diffs across rejected-window gaps
- Range gate: 5-100ms (was 5-200ms, per cardiologist recommendation)
- HRValidator cap: 100ms (was 200ms)

Pod validation: HRV now 20-70ms (was 120-195ms). Zero values >100ms.

Closes #221 (now fixes)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e35cb20e-3e96-40db-af9c-d47830800133

📥 Commits

Reviewing files that changed from the base of the PR and between 07fc805 and 43e431c.

📒 Files selected for processing (3)
  • modules/common/calibration.py
  • modules/piezo-processor/main.py
  • modules/piezo-processor/test_main.py

📝 Walkthrough

Walkthrough

The PR redefines the HRV field from an RMSSD sub-window metric to a non-clinical "HR variability index" computed from 10s windows (50% overlap) with a harmonic gate, Hampel filtering, gap-aware successive differences, and tightens the valid range to 5–100 ms. Docs and tests updated accordingly.

Changes

Cohort / File(s) Summary
Documentation
\.claude/docs/trpc-api-architecture.md, docs/piezo-processor.md
Rename HRV → "HR variability index"; describe new 10s windowing (50% overlap), harmonic gate, Hampel filtering, gap-aware RMSSD-like step, updated validity (5–100 ms), and explanatory notes on non‑clinical metric.
Validation Constants
modules/common/calibration.py
Lowered HRV_HARD_MAX from 200.0 ms to 100.0 ms; updates affect HRV validation thresholds/documentation.
HRV Computation
modules/piezo-processor/main.py
Rewrote compute_hrv to use 10s SHS autocorrelation windows, introduced _harmonic_gate, Hampel filtering, gap-aware successive-differences pipeline, new constants, and final 5–100 ms range gate; public signature unchanged.
Tests
modules/piezo-processor/test_main.py
Updated test descriptions and assertions to reference "HR variability index" and tightened expected ranges from 5–200 ms to 5–100 ms; no API changes.

Sequence Diagram(s)

sequenceDiagram
    participant Input as Raw Samples
    participant SHS as SHS Autocorrelation
    participant Tracker as IBI Tracker
    participant Gate as Harmonic Gate
    participant Hampel as Hampel Filter
    participant Diff as Gap-Aware Differences
    participant Range as Range Gate (5–100 ms)
    participant Output as HR Variability Index

    Input->>SHS: 10s windows (50% overlap)
    SHS->>Tracker: propose IBI estimates
    Tracker->>Gate: provide tracker IBI
    SHS->>Gate: raw IBI estimates
    Gate->>Hampel: accepted IBIs (or corrected/discarded)
    Hampel->>Diff: cleaned IBIs
    Diff->>Range: compute successive-diff metric
    Range->>Output: emit HR variability index or None
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Ten-second hops through heartbeat song,
Harmonics gate the notes so strong,
Hampel trims the weeds away,
Gaps are watched, the index plays,
A rabbit cheers the new HRV way!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: the introduction of a harmonic gate, reduction of window size to 10s, and the metric rename to HR variability index, which are the core technical improvements addressed in this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/hrv-harmonic-gate-221
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Updates the piezo-processor HRV calculation to a safer, more honest “HR variability index” by adding harmonic rejection/correction and increasing sub-window resolution to reduce inflated artifact-driven readings.

Changes:

  • Reworked compute_hrv() to use 10s overlapping windows, peak-only SHS scoring, harmonic gating, Hampel filtering, and gap-aware successive differences with a 5–100 ms validity gate.
  • Tightened HRV validation caps (200 ms → 100 ms) and updated unit tests to reflect the new upper bound.
  • Updated docs to rename the metric and document the new processing pipeline and constraints.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
modules/piezo-processor/main.py Implements the new HR variability index pipeline (10s windows, harmonic gate, gap-aware diffs, tighter range gate).
modules/piezo-processor/test_main.py Updates HRV-related assertions to the new 100 ms cap.
modules/common/calibration.py Lowers HRValidator.HRV_HARD_MAX to 100 ms.
docs/piezo-processor.md Renames HRV metric and documents the updated algorithm and parameters.
.claude/docs/trpc-api-architecture.md Updates API field description to “HR variability index” (not clinical RMSSD) and range.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +571 to +575
gated.append((win_idx, accepted))
tracker_history.append(accepted)

if len(gated) < _HRV_MIN_IBIS:
return None
raw_ibis: list = [] # (window_index, ibi_ms)
for idx, start in enumerate(
range(0, len(filtered) - sub_window + 1, step)):
chunk = filtered[start:start + sub_window]
Comment thread modules/piezo-processor/main.py Outdated
Comment on lines +610 to +613
rmssd = float(np.sqrt(np.mean(sq_diffs)))
# Range gate: 5-100 ms. Window-level HRV above 100 ms is artifact
# even after harmonic correction (cardiologist recommendation).
return rmssd if 5 <= rmssd <= 100 else None
Comment thread modules/piezo-processor/test_main.py Outdated
Comment on lines +746 to +749
# We expect an RMSSD; it may or may not match the exact input std
# but should be a plausible number
if hrv is not None:
assert 5 <= hrv <= 200
assert 5 <= hrv <= 100
Comment thread modules/piezo-processor/test_main.py Outdated

def test_range_gate(self):
"""RMSSD should be None or within 5-200 ms."""
"""RMSSD should be None or within 5-100 ms."""
Comment on lines 533 to +537
# Hard physiological caps (always enforced)
HR_HARD_MIN = 30.0 # bpm — athletes can drop this low (Circulation, AHA)
HR_HARD_MAX = 100.0 # bpm — above = tachycardia per AHA definition
HRV_HARD_MIN = 8.0 # ms — below = likely artifact
HRV_HARD_MAX = 200.0 # ms — BCG-derived >200 is artifact (Shaffer & Ginsberg 2017)
HRV_HARD_MAX = 100.0 # ms — window-level BCG HRV >100 is artifact (see #221)
Comment on lines +465 to +469
def _harmonic_gate(ibi_ms: float, tracker_ibi_ms: float) -> Optional[float]:
"""Reject or correct IBIs that are harmonic multiples of the expected value.

Returns the accepted/corrected IBI in ms, or None to discard.
"""
Comment thread modules/piezo-processor/main.py Outdated
sq_diffs: list = []
for i in range(1, len(clean_ibis)):
gap = clean_indices[i] - clean_indices[i - 1]
if gap > _HRV_MAX_GAP_WINDOWS:
- Rename local var rmssd → hrv_index for clarity
- Fix gap threshold: >= not > (off-by-one)
- Update HRValidator docstring to reflect 100ms cap
- Rename all test comments from RMSSD to HRV index

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@ng ng merged commit dd2efa8 into dev Mar 16, 2026
0 of 5 checks passed
@ng ng deleted the fix/hrv-harmonic-gate-221 branch March 16, 2026 22:17
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 1.1.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants