Motion Artifact Cancellation Experiment

2026/07/22

A ring’s PPG heart-rate estimate falls apart under motion. This is a review of the experiment that tried to fix it: what motion actually does to the signal, which mechanisms considered, what shipped, what it bought, and where the remaining error lives. It is a companion to Heart Rate Estimation from Raw PPG, which covers the estimator as a whole and which this assumes.


1. The problem

The ring streams two PPG channels and a 3-axis accelerometer at 25 Hz. The previous release reduced the accelerometer to a single per-second magnitude standard deviation and used it only to label windows rest-versus-active. It never entered the HR estimate. That is the gap this work closes, and the motivating asymmetry is stark: the Goodix chip’s own HR is demonstrably motion-compensated — flat at 86–103 bpm across cadences spanning 73–216 bpm-equivalent, correlation with cadence −0.32 — while ours was not.

The cost showed up as a clean monotonic degradation across motion octiles. In the baseline, MAE runs 1.65 bpm in the stillest octile to 14.08 bpm in the highest, while direct coverage falls 71.0 % → 32.1 %. Accuracy and availability degrade together: the estimator gets worse and abstains more, so what it emits under motion is a shrinking, harder set. Under motion nearly one usable window in five was already confidently wrong (2,016 of 10,834 active usable windows, 18.6 %, against a 3.1 % resting rate).


2. Pipeline

per 12 s window:

  ACC x/y/z @25Hz ──┬─ magnitude → band-pass 0.5–4.0 Hz → dominant cadence
                    │                                     + prominence
                    │
                    └─ per-axis band-pass ────────────┐  [only above 0.20 g]
                                                      │
  PPG1, PPG2 (raw) ───────────────────────────────────┤
                                                      ▼
                              ┌───────────────────────────────────────┐
                              │ adaptive cancellation                 │
                              │  gate: motion ≥ 0.20 g                │
                              │  ridge block least-squares of PPG on  │
                              │  a lagged ACC basis (3 axes × 8 taps) │
                              │  subtract the fitted part             │
                              └───────────────────────────────────────┘
                                                      │
                             adopted only if energy was removed ───────┤
                                                      ▼
   channel select → band-pass → ┬─ beat counting     [TIME DOMAIN]
                                └─ spectral peak     [FREQUENCY DOMAIN]
                                                      │
                        fuse   agree ≤ 8 bpm → mean; else the spectral estimate
                                                      │
                        gate   usable ⟺ agree ≤ tolerance
                               tolerance = 6.0 if the window was cleaned, else 5.0

Cancellation runs before channel selection, deliberately. Which channel is cleaner is a question about the pulse; under motion, ranking the raw channels answers it about the artifact instead.


3. Mechanisms considered

Three were proposed. Two are not in the shipped path, and both exclusions are measured rather than assumed.

3.1 Cadence-informed spectral de-weighting — implemented, measured, rejected

Attenuate the ACC cadence and its harmonics in the PPG spectrum before the estimator picks a peak. Shipped disabled rather than deleted, so the negative result stays reproducible.

It fails for reach, not for correctness. On the 3.28 % of active windows it targets it genuinely helps — MAE 33.77 → 26.78, improving 68 % of them. But of the windows the spectrum already had right, de-weighting moved 36.1 % by more than 5 bpm and made 36.6 % of those wrong:

policyactive coverageMAEwithin ±5confidently wrong
baseline (no cancellation)35.6 %14.3949.8 %92
de-weight always27.4 %14.6746.2 %67
de-weight, gated on evidence of a lock37.7 %14.7247.4 %98

Gating on positive evidence of a lock removes the collateral damage but exposes a subtler defect: it manufactures agreement. De-weighting pushes the spectral estimate toward the beat-counter estimate, which inflates the very statistic that decides usability — so coverage rises and confidently-wrong rises with it (92 → 98). The mechanism corrupts its own quality gate. That is an argument against the mechanism, not against the gate.

3.2 ACC spectral subtraction — not implemented

Skipped once the broadband finding landed. Spectral subtraction addresses the same narrow failure de-weighting does — a competing peak — so it would have failed for the same reason. Recorded as a deliberate omission rather than an oversight.

3.3 Adaptive cancellation — shipped

Regress each PPG window on a lagged basis of the ACC axes and subtract the fitted part, so what reaches both estimators is already motion-cleaned.

This succeeds where de-weighting cannot because it uses the accelerometer’s phase and waveform, not merely the frequency of its peak. It removes what actually correlates with measured motion, at whatever frequency that happens to be — which is what lets it address broadband corruption. It also cleans the time domain, so the beat counter is no longer captured by the same artifact as the spectrum.

Implementation. Ridge-regularised block least-squares — the Wiener solution an adaptive filter converges to, solved directly. Exact, no step-size or convergence tuning, fully vectorised; the sample-by-sample alternative would have been ~19 M Python iterations across the corpus. A streaming port swaps in the recursive update that converges to the same filter; the model is identical, only the solver differs. It is causal-capable: only lags 0…7 are used, nothing outside the window, verified by a test asserting that a window estimated in isolation equals the same window estimated inside a longer segment.


4. Parameters, and the evidence behind them

Every value below was set by measurement. The two that matter most are the gate and the ridge.

parametervaluebasis
filter taps8 (320 ms)Long enough to cover foot-strike → finger pressure wave; short enough to keep 3 × 8 = 24 parameters against a 300-sample window (~12 samples/parameter). At 13 taps the ratio falls to ~7.7 and the fit starts explaining the pulse.
ridge0.05Fraction of the design’s mean eigenvalue, so one constant works across very different motion amplitudes. This is the knob that keeps a 24-parameter fit from removing real pulse energy.
cancellation gate0.20 gMeasured crossover — not the 0.03 g rest cutoff. See below.
cadence prominence floor15.0Peak-to-median in-band prominence. At the breathing path’s 4.0 the test passes 90 % of active and 84 % of resting windows — it barely discriminates. 15 gives 26.3 % / 6.5 %, ~4× discrimination.
agreement tolerance, cleaned6.0 bpmLargest value the correctness budget affords.

The 0.20 g gate is the single most important parameter, because cancellation is not free. Cleaning changes the spectral estimate, and some cleaned windows then legitimately disagree with the beat counter and are correctly dropped. Below ~0.2 g the artifact removed is worth less than the signal it costs:

motion bandMAE before → aftercoverage before → after
0.03–0.08 g5.05 → 5.8751.4 % → 40.3 % (worse)
0.08–0.20 g7.24 → 7.9335.0 % → 28.1 % (worse)
0.20 g +14.56 → 10.8132.1 % → 28.7 % (much better)

Below ~0.2 g, cancelling costs both accuracy and coverage; above it, accuracy improves sharply for a small coverage cost. The gate sits at the crossover. These are the recorded threshold-study values — the below-gate arms require running the canceller with a lowered gate, which the shipped code never does.

A threshold sweep over the whole active cohort confirms the optimum (MAE / confidently wrong): 0.10 → 7.09/153, 0.20 → 6.90/155, 0.15 → 7.04/156, 0.30 → 7.17/165, against a baseline of 7.81/186.

A consequence worth stating plainly: because the gate sits well above the rest cutoff, a resting window never enters the cancellation branch at all. The old path is not reproduced for rest — it is the path that runs.


5. Results

Measured by running the harness twice on identical windows, once with the motion reference and once without. The window grid, chip reference, motion and cohort labels are verified identical across the two arms, so this is a paired comparison rather than a cross-version one.

5.1 By corpus

corpusndirect MAEwithin ±5filled MAE
daily50,4794.71 → 4.38 (−0.33)79.6 → 80.3 %5.87 → 5.48
sleep7,7602.35 → 2.34 (−0.01)92.7 → 92.8 %4.10 → 4.10
step5386.13 → 6.62 (+0.49)72.7 → 71.8 %7.27 → 8.06
all58,7774.42 → 4.14 (−0.28)81.2 → 81.8 %5.67 → 5.34

5.2 By cohort

cohortnMAEwithin ±5coverageconfidently wrong
rest31,2872.65 → 2.6589.8 → 89.8 %72.37 → 72.37 %705 → 705
active27,4908.12 → 7.2963.4 → 65.1 %39.41 → 39.12 %2,016 → 1,750

5.3 Where the effect actually lands

The effect is surgically confined to what it was aimed at:

motion octilemean motionMAE before → aftercoverageconfidently wrong
1–70.0025–0.148 gidentical, +0.00 bpm eachunchangedunchanged
80.277 g14.08 → 10.33 (−26.6 %)32.1 → 31.0 %861 → 595 (−30.9 %)

Seven of eight octiles are untouched because they sit below the 0.20 g gate and run the old code path. All of the release’s effect — in both error and confident-error count — lands in octile 8.

Cancellation touched 6,853 windows (11.7 % of the corpus). On those windows alone, MAE moves 14.45 → 10.42. The other 51,924 windows are bit-identical between the two arms — the strongest available form of the no-regression claim, since it is exact equality on real data rather than a tolerance or a comparison of summary statistics.

5.4 What cancellation does to the windows it touches

The aggregate improvement conceals what the mechanism is actually doing, and the detail matters more than the headline.

Scored on the 990 cleaned windows both arms emitted — the like-for-like set, since a window present in only one arm has no error to compare — MAE moves 10.01 → 7.98. But that net is the residual of two much larger opposing quantities:

windowsabsolute error changedmean per window
improved5253,222 bpm removed6.14 bpm
worsened4651,220 bpm added2.62 bpm
net9902,002 bpm removed

Nearly half the windows the canceller touches get worse, and only 62 % of the gross gain survives. The mechanism is not a uniform small correction; it is a high-variance trade that comes out ahead. The gain is also concentrated — the top 10 % of improved windows carry 69 % of the gross gain, the top 25 % carry 91 % — which is how a median energy removal of just 0.20 dB (p90 0.67 dB) produces a 26.6 % octile improvement.

Two real windows make the trade concrete — the largest gain and the largest loss in one recording, both above the gate, both emitted by both arms. First the good case, where cancellation strips the gait energy that had the old path locked at half rate:

And the bad one, which is not a curiosity but the coincidence failure of §7 caught in the act. Its cadence (~61 steps/min) sits about an eighth of an FFT bin from its heart rate (~62 bpm) — close enough that the two cannot be told apart in the spectrum at all — so the lagged accelerometer basis spans the pulse as well as the artifact, the regression takes both, and the estimator locks onto a harmonic at ~117 bpm:

5.5 Release gate

Targets set before the work began, scored honestly against it:

gatebeforeaftertargetstatus
active MAE / within ±58.12 / 63.4 %7.29 / 65.07 %≤ 6.0 / ≥ 72 %improved but short
active direct coverage39.41 %39.12 %≥ 55 %didn not improve
pooled filled MAE5.675.34≤ 4.60improved but short
rest no-regression2.65 / 89.79 %2.65 / 89.79 %≤ 2.70 / ≥ 89.5 %
confidently wrong2,7212,455no increase
reference accountingpresentauditable
core purityno new dependencies