mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-22 22:52:34 -05:00
fix(vitals): reject respiratory peaks and cap filtered confidence
This commit is contained in:
1 parent
e1fbbede61
commit
1a1339c39b
5 files changed
+254
-52
No files matched your search
Generated
+6
-6
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user