fix(vitals): reject respiratory peaks and cap filtered confidence

This commit is contained in:
ruv committed 2026-09-30 09:46:01 -04:00
1 parent e1fbbede61
commit 1a1339c39b
5 files changed
+254 -52

No files matched your search

+6 -6
View File
@@ -3416,7 +3416,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-core"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"blake3",
"chrono",
@@ -3429,7 +3429,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-mat"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"anyhow",
"async-trait",
@@ -3469,7 +3469,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-ruvector"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"ruvector-attention",
"ruvector-attn-mincut",
@@ -3483,7 +3483,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-signal"
version = "0.3.5"
version = "0.3.6"
dependencies = [
"chrono",
"midstreamer-attractor",
@@ -3507,7 +3507,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-train"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"anyhow",
"chrono",
@@ -3539,7 +3539,7 @@ dependencies = [
[[package]]
name = "wifi-densepose-vitals"
version = "0.3.1"
version = "0.3.2"
dependencies = [
"serde",
"tracing",
+16 -2
View File
@@ -81,8 +81,22 @@ for residuals, weights in your_csi_source: # one frame at a time
print(f"{est.value_bpm:.1f} BPM (confidence={est.confidence:.2f})")
```
Heart rate is the same shape — `HeartRateExtractor.esp32_default()` with a
0.8–2.0 Hz band-pass and a 15-second window.
For heart rate, use `HeartRateExtractor.esp32_default()` with a 0.8–2.0 Hz
bandpass and a 15-second window. Its second argument is `phases`, not the
breathing extractor's `weights`:
```python
hr = HeartRateExtractor.esp32_default()
est = hr.extract(residuals=residuals, phases=phases)
# If phase measurements are unavailable, phases=[] uses equal weighting.
```
The current binding rejects the old `weights` keyword with `TypeError`.
Heart-rate estimation requires a local autocorrelation peak; a declining
breathing-only correlation is not treated as a pulse at the band edge.
Additional filtering reduces respiratory leakage, while confidence remains
capped by evidence from the original filter stage. Synthetic regression tests
cover these cases; they do not establish accuracy on real CSI or medical use.
### Subscribe to a live sensing-server
+30
View File
@@ -19,6 +19,7 @@ from __future__ import annotations
import math
from random import Random
import numpy as np
import pytest
import wifi_densepose
@@ -255,6 +256,35 @@ def test_heart_rate_extract_with_empty_phases_produces_estimates() -> None:
)
@pytest.mark.parametrize("heart_amplitude", [0.0, 0.01])
def test_issue_2057_breathing_leakage(heart_amplitude: float) -> None:
"""Replay the reported synthetic input through the installed Rust wheel."""
hr = HeartRateExtractor(n_subcarriers=56, sample_rate=100.0, window_secs=15.0)
rng = np.random.default_rng(0)
last = None
for i in range(6000):
t = i / 100.0
base = 0.1 * math.sin(2.0 * math.pi * 0.25 * t)
base += heart_amplitude * math.sin(2.0 * math.pi * 1.2 * t)
residuals = (base + 0.01 * rng.standard_normal(56)).tolist()
last = hr.extract(residuals=residuals, phases=[])
if heart_amplitude == 0.0:
assert last is None or last.confidence < 0.3
else:
assert last is not None
assert abs(last.value_bpm - 72.0) < 3.0
def test_issue_2057_filtering_does_not_promote_noise() -> None:
hr = HeartRateExtractor(n_subcarriers=56, sample_rate=50.0, window_secs=15.0)
rng = np.random.default_rng(79)
last = None
for frame in 0.01 * rng.standard_normal((250, 56)):
last = hr.extract(residuals=frame.tolist(), phases=[])
assert last is None or last.confidence < 0.6
assert last is None or last.status != VitalStatus.Valid
# ─── Build feature flag ──────────────────────────────────────────────