Heart Rate Estimation from Raw PPG

2026/07/20

This is a technical review of the ring’s PPG heart-rate algorithm: what it does, how it is evaluated, how well it agrees with the ring’s own chip HR across every recording we have, and where the remaining headroom is.

1. Introduction

The ring streams two raw PPG channels and a 3-axis accelerometer at 25 Hz, one row per second. The cardio module turns that into a heart rate, plus the quality metadata a consumer needs to decide whether to trust it.

The design has one governing idea: two independent estimators that are cross-checked against each other, and abstention when they disagree. A single spectral estimator on a wrist/ring PPG is easy to fool — motion energy, respiratory harmonics and the dicrotic notch all put peaks in the pulse band. Running a frequency-domain and a time-domain estimator side by side and requiring them to agree converts most of those failures from wrong answers into no answer, which is the safer failure for everything downstream.

The cost of that choice is coverage: the algorithm declines to report on roughly 43 % of referenced windows, rising to ~60 % under motion. Most of this review is about whether that trade is being made at the right point and whether the failures that survive it are the ones we think they are.

2. Workflow of the current algorithm

         PPG1 + PPG2 + ACC · 25 Hz, 1 row/s
                          │
           segment · 12 s window, 1 s hop
      higher-SQI channel · band-pass 0.8-3.5 Hz
                          │
           ┌──────────────┴────────────┐
           ↓                           ↓
  ┌──────────────────┐      ┌──────────────────────┐
  │   TIME DOMAIN    │      │   FREQUENCY DOMAIN   │
  ├──────────────────┤      ├──────────────────────┤
  │ systolic peaks   │      │ Hann window → rFFT   │
  │   → IBIs         │      │ in-band argmax       │
  │ reject artifacts │      │ sub-bin refine       │
  │ HR = 60000 / med │      │ 2nd-harmonic guard   │
  └──────────────────┘      └──────────────────────┘
           │                           │
           └──────────────┬────────────┘
                          ↓
          fuse · agree ≤ 8 bpm ? mean : FFT
                          │
                          ↓
            gate · usable ⟺ agree ≤ 5 bpm
                          │
                          ↓
                  fill gaps ≤ 30 s
      longer gaps stay NaN - the pipeline abstains

2.1 Segmentation and the PPG duty cycle

The loader splits a recording wherever the 1 s timestamp cadence breaks by more than 2 s, and wherever the PPG front-end switches on or off. Only PPG-on runs become segments.

The duty-cycle handling matters more than it sounds. The daily recordings stream PPG continuously, but the sleep and step recordings run it at 10–35 % duty to save power, and the collector zero-fills the off rows. Zeros are not a quiet signal — a window straddling an on/off edge contains a step, which is broadband, and a broadband window produces a confident, wrong HR. Excluding those rows from segments entirely is the only safe treatment.

2.2 The two estimators

Frequency domain. Hann-window, real FFT, take the in-band argmax, then refine it parabolically across neighbouring bins. The refinement is not cosmetic: a 12 s window has ~0.08 Hz bins ≈ 5 bpm, far too coarse to report raw.

The same function returns SQI, a spectral concentration score — the fraction of in-band spectral mass falling within ±0.2 Hz of the peak. A clean pulse concentrates near 1; a motion-corrupted window smears toward 0.

Time domain. Find systolic peaks on the band-passed signal with a 0.4 s refractory period, difference them into inter-beat intervals, reject IBIs outside 300–1500 ms and those more than 20 % from the window median, and report 60000 / median IBI. Because it counts actual beats it fails differently from the FFT, which is precisely what makes the cross-check informative.

2.3 The sub-harmonic guard

A weak-fundamental or dicrotic-heavy pulse can put the spectral peak on the 2nd harmonic, so the FFT reports ~2× the true rate. The guard checks whether f_peak / 2 is a genuine local maximum carrying ≥20 % of the peak’s height, and if so relocates the estimate there.

Frequency alone cannot decide this, and that is the subtle part. A 2× lock of a true 50 bpm pulse presents at ~100 bpm — the same place as genuine tachycardia. No threshold on peak frequency separates the case the guard must fix from the case it must not touch. The guard is therefore arbitrated by the time-domain estimate: it relocates only when the halved rate is the one the beat count actually supports.

2.4 Fusion and the usability gate

When the two estimators agree within 8 bpm the window is high-confidence and they are averaged. When they disagree the window reports the FFT value, but is marked unusable.

Usability is decided by agreement alone: |FFT − peak| ≤ 5 bpm. SQI is not a gate. It remains a strong quality ranking and it still selects the channel, but as a binary override it admitted only noise.

The fill stage then bridges unusable stretches up to 30 s by linear interpolation between usable neighbours, on the reasoning that HR is physiologically smooth over that scale. Longer gaps stay NaN — the pipeline abstains rather than emitting a stale value.

2.5 A concrete 12s example

All of the above on a single real 12 s window: the two channels being scored, the band-pass stripping drift, the beat train and the one inter-beat interval that gets rejected, the spectrum and its sub-bin refinement, and the two estimates meeting at the gate. Every number in it is the pipeline’s own output rather than an illustration, so it moves whenever the thresholds in the sections above do.

Note: ACC is reduced to a per-second magnitude standard deviation and used to label windows rest (< 0.03 g) or active only.


3. Dataset for the accuracy evaluation

3.1 The reference, and what it is worth

The HR column is treated as ground truth wherever HR >= 40 bpm.

Two honest caveats travel with every number in this report:

Because the reference is smoothed, some of the residual disagreement is method difference rather than error — particularly during fast HR transitions, where a smoothed reference lags a true change in either direction.

3.2 The three corpora

corpusrecordingsscored windowsrestactivePPG dutycharacter
daily/1250,61823,27427,344100 %unselected whole days
sleep/87,7717,67010115–34 %overnight + naps
step/625513432088–23 %labelled walks, commutes, confounders
total8258,94031,28727,653~82 h referenced

They are complementary, and each answers something the others cannot:

3.3 Scoring method

12 s windows at a 5 s hop (1 s in production; 5 s bounds study runtime). Windows are scored only where the chip reference is valid. Reported metrics:


4. Results

4.1 Headline, by corpus

corpuscohortndirect MAEwithin ±5biascov directcov filledfilled MAE
dailyrest23,2742.7089.2 %−0.7077.5 %96.4 %3.27
dailyactive27,3448.3362.9 %−4.9539.3 %76.7 %8.84
dailyboth50,6184.8179.4 %−2.2956.9 %85.7 %5.96
sleeprest7,6702.3193.1 %+1.5956.4 %76.0 %4.06
sleepboth7,7712.3692.7 %+1.6356.4 %76.3 %4.12
steprest3434.4675.4 %+2.2481.9 %97.7 %4.75
stepactive20811.1463.9 %−0.1857.2 %93.3 %12.82
stepboth5516.4572.0 %+1.5272.6 %96.0 %7.71
allrest31,2872.6589.8 %−0.2272.4 %91.4 %3.45
allactive27,6538.3562.9 %−4.8539.5 %76.9 %8.87
allboth58,9404.5181.0 %−1.7356.9 %84.6 %5.76

At rest the algorithm is good and this is not an artifact of scene selection. MAE 2.65 bpm and ~90 % within ±5 across 31k unselected resting windows, and the sleep corpus independently reproduces it (2.31 / 93.1 %). An earlier headline from the curated scene sessions (2.33 bpm) holds up on data nobody picked.

Under motion it degrades sharply and abstains heavily — MAE 8.35 bpm at 39.5 % direct coverage. Since the active cohort is ~47 % of real-world windows, the whole-day figure (4.51) is much closer to lived experience than the rest figure is.

Sleep is the best corpus and daily the hardest, which is the expected ordering: sleep is quiet and steady, daily contains everything.

4.2 Error by heart-rate band, and by motion

Distributing total absolute error across bands on the daily corpus:

chip-HR bandshare of windowsMAEshare of total error mass
90–12029.5 %9.6442.1 %
75–9048.9 %3.9438.9 %
60–7512.8 %2.448.8 %
48–608.0 %1.684.7 %
<480.42 %38.444.3 %
≥1200.37 %27.201.2 %

81 % of all error sits in the ordinary 75–120 bpm daytime range, and 64.7 % of it in the active cohort. Any work that does not touch elevated-HR-under-motion is working on the margins.

Motion is the cleanest single predictor of error, and it degrades accuracy and coverage together — the estimator gets worse and abstains more, so what it emits under motion is a shrinking, harder set:

motion octilemean ACC-std (g)MAEwithin ±5coverage
10.00251.6595.7 %71.0 %
20.00301.6895.5 %79.8 %
30.00573.0087.9 %79.2 %
40.01584.0981.0 %62.8 %
50.03695.0475.7 %54.2 %
60.07686.7667.5 %41.9 %
70.14798.4858.9 %34.5 %
80.276814.2746.3 %32.2 %

4.3 Estimator comparison

estimatornMAEbiaswithin ±5within ±10
FFT33,5644.70−1.7879.8 %88.3 %
peak-interval33,5644.60−1.6880.9 %88.7 %
fused33,5644.51−1.7381.0 %88.6 %

Fusion beats both inputs, which is the design working. The margin is small on admitted windows because the agreement gate has already removed most cases where they differ — the value of the second estimator shows up as abstention quality, not as a lower MAE.

An oracle that picked the better of the two per window would reach MAE 3.97 (vs 4.51), so perfect arbitration is worth ~0.54 bpm. That bounds how much better fusion logic alone can make things — real, but an order of magnitude less than what motion cancellation is worth (§6).

4.4 Accuracy and coverage by corpus


5. Bradycardia and the band floor — a corrected finding

The pulse band starts at 0.8 Hz = 48 bpm, so on paper a 40–47 bpm fundamental is filtered out before the FFT sees it. An earlier evaluation measured that on the daily corpus and concluded it was “the most serious defect”: a silent, systematic 2× error “in exactly the regime overnight analysis depends on”.

Adding the sleep corpus — the only data that actually contains overnight bradycardia — shows that conclusion is wrong.

corpussub-48 windowsdirect coverageMAEwithin ±5median PPG/chip2× locksmedian motion
daily21372.3 %38.440.0 %1.9572.1 %0.0234 g
sleep61618.7 %9.3671.3 %1.044.3 %0.0026 g

On genuine, quiet overnight bradycardia the algorithm behaves well: median PPG/chip ratio 1.04, 71 % within ±5, only 4.3 % harmonic locks. What it does instead is abstain — direct coverage falls to 18.7 % against 70–85 % in the 52–60 bpm buckets:

chip-HR bucketndirect coverageMAEwithin ±5
40–454015.0 %19.9033.3 %
45–4857618.9 %8.7873.4 %
48–522,67231.0 %3.1188.5 %
52–562,26670.8 %1.7496.3 %
56–601,55082.8 %1.7295.6 %

Three conclusions follow.

The floor is soft, not a cliff. A 3rd-order Butterworth is not a brick wall; enough of a 45–47 bpm fundamental survives it that admitted windows there are mostly correct. Coverage degrades smoothly as HR approaches and crosses 48 bpm rather than accuracy collapsing — which is the right failure direction.

The daily sub-48 population is deeper bradycardia, not daytime artifact. (Corrected — an earlier version of this section attributed it to daytime motion. Recovering the local clock time of those windows disproves that.) 78.9 % of the 213 daily sub-48 windows occur at night — 99 between 00:00–04:00 and 69 between 04:00–08:00, against only 45 across the entire 08:00–22:00 day. Their chip median is 44.0 bpm and our median is 88.1, a ratio of 1.98.

So the discriminator between the corpora is neither day-versus-night nor motion: it is how far below 48 bpm the true rate is. The sleep corpus’s sub-48 windows cluster at 45–48 (median 47), right at the filter edge where enough fundamental survives — ratio 1.04. The daily ones sit at 40–45 (median 44), genuinely below the floor — ratio 1.98. These are the same phenomenon sampled at different depths, and in both cases the chip is the trustworthy one.

The genuinely broken region is below ~45 bpm. In the sleep corpus’s 40–45 bucket MAE is 19.9 with 33 % within ±5, and the daily night population shows the same 2× behaviour. We work down to ~45 and break below it. The evidence is still thin — 40 sleep windows in 40–45, 7 below 42 — but it is consistent across two corpora.

Separately, HPS (harmonic-product-spectrum) fundamental selection — deferred to in the design notes as “the correct fix” — was prototyped and does not work: it moves sub-48 within-±5 from 0 % to 4.8 %. Lowering the band floor to 0.6 Hz regresses the common case, confirming the existing note. Neither is worth pursuing on current evidence.

One further correction: the earlier evaluation states that the sleep module inherits this defect. It does not. The sleep module has an independent HR path with its own band (0.7–3.5 Hz, a 42 bpm floor) and imports nothing from the cardio module.


6. Where the remaining headroom is

Motion artifact cancellation is the only large prize. The active cohort carries 64.7 % of total error mass at MAE 8.35 and 39.5 % coverage, and the accelerometer — already loaded, parsed and time-aligned — never enters the estimate. Adaptive filtering or ACC spectral subtraction ahead of the FFT is standard for wrist/ring PPG and is the one change that would move the user-visible number materially. It is weeks of work, not hours.

This is our gap to close, not an artifact of the reference. The chip vendor’s own SDK documentation confirms that its HR estimate already accounts for motion, and the data agrees: the chip emits a valid HR on 97–99 % of PPG-on rows including the highest-motion octiles, where our estimator abstains on ~68 % of windows. The asymmetry in the motion table above is therefore ours, and it is roughly the whole story of the active cohort.

The two estimators stop being independent above ~105 bpm. Bias by band and motion:

bandoverall biasstill (<0.01 g)moving (≥0.05 g)
60–75+0.75+0.49+2.11
75–90−2.04−0.89−3.64
90–105−6.31−2.89−9.14
105–120−16.07−17.15−15.50
120–200−25.85−28.80 (n=2)−23.42

Below ~105 bpm the bias scales with motion, as artifact should. Above it the bias is large and does not depend on motion at all — structural, not artifact. Looking at the estimators separately at 105–120 bpm: across all windows FFT bias is −26.0 and peak −16.0, but across admitted windows they are −16.00 and −16.13 — converged on nearly the same wrong answer, with both reading >10 bpm low in 41 % of admitted windows. The gate still discriminates (admitted MAE 17.9 vs rejected 33.7), but among what it admits, agreement has stopped being evidence of correctness, because both estimators share one band-pass and lock onto the same low-frequency feature. Independence is the assumption the whole fusion design rests on, and it fails exactly where the error is worst.

Not worth prioritising: sub-48 bradycardia (§5) — the algorithm abstains correctly, the broken sub-42 region has 7 windows of evidence, and the proposed HPS fix is measured not to work. Revisit only if overnight resting-HR becomes a product commitment, and collect bradycardia data first.


7. Limitations