mirror of
https://github.com/ruvnet/RuView
synced 2026-07-19 16:53:18 +00:00
fix: rename HeartRateExtractor.extract() weights param to phases
This commit is contained in:
@@ -57,12 +57,12 @@ def test_heart_rate_extract_per_frame_cost(benchmark) -> None:
|
||||
hr = HeartRateExtractor.esp32_default()
|
||||
rng = Random(43)
|
||||
for i in range(1500):
|
||||
residuals, weights = _synth_frame(56, 100.0, i / 100.0, 1.2, rng)
|
||||
hr.extract(residuals=residuals, weights=weights)
|
||||
residuals, phases = _synth_frame(56, 100.0, i / 100.0, 1.2, rng)
|
||||
hr.extract(residuals=residuals, phases=phases)
|
||||
|
||||
def _one_frame():
|
||||
residuals, weights = _synth_frame(56, 100.0, 16.0, 1.2, rng)
|
||||
return hr.extract(residuals=residuals, weights=weights)
|
||||
residuals, phases = _synth_frame(56, 100.0, 16.0, 1.2, rng)
|
||||
return hr.extract(residuals=residuals, phases=phases)
|
||||
|
||||
benchmark(_one_frame)
|
||||
|
||||
|
||||
@@ -242,7 +242,22 @@ impl PyBreathingExtractor {
|
||||
// ─── HeartRateExtractor ──────────────────────────────────────────────
|
||||
|
||||
/// Extracts heart rate (40–120 BPM) from per-subcarrier amplitude
|
||||
/// residuals via 0.8–2.0 Hz bandpass + autocorrelation peak detection.
|
||||
/// residuals and per-subcarrier unwrapped phases (radians) via
|
||||
/// 0.8–2.0 Hz bandpass + autocorrelation peak detection.
|
||||
///
|
||||
/// Python:
|
||||
/// ```python
|
||||
/// from wifi_densepose import HeartRateExtractor
|
||||
///
|
||||
/// hr = HeartRateExtractor.esp32_default() # 56 subcarriers, 100 Hz, 15s window
|
||||
///
|
||||
/// # Feed residuals and matching unwrapped phases from your preprocessor.
|
||||
/// # Unlike BreathingExtractor weights, phases=[] is invalid for heart-rate
|
||||
/// # extraction because the Rust core requires phase data for each subcarrier.
|
||||
/// est = hr.extract(residuals=[0.01, -0.02, …], phases=[0.0, 0.01, …])
|
||||
/// if est is not None:
|
||||
/// print(est.value_bpm, est.confidence)
|
||||
/// ```
|
||||
#[pyclass(name = "HeartRateExtractor")]
|
||||
pub struct PyHeartRateExtractor {
|
||||
inner: HeartRateExtractor,
|
||||
@@ -265,10 +280,17 @@ impl PyHeartRateExtractor {
|
||||
Self { inner: HeartRateExtractor::esp32_default() }
|
||||
}
|
||||
|
||||
/// Extract heart rate from per-subcarrier residuals. GIL released
|
||||
/// during DSP.
|
||||
fn extract(&mut self, py: Python<'_>, residuals: Vec<f64>, weights: Vec<f64>) -> Option<PyVitalEstimate> {
|
||||
let est = py.allow_threads(|| self.inner.extract(&residuals, &weights));
|
||||
/// Extract heart rate from per-subcarrier residuals and matching
|
||||
/// per-subcarrier unwrapped phases (radians). Empty phases are invalid
|
||||
/// and return `None` because the Rust extractor requires phase data.
|
||||
/// GIL released during DSP.
|
||||
fn extract(
|
||||
&mut self,
|
||||
py: Python<'_>,
|
||||
residuals: Vec<f64>,
|
||||
phases: Vec<f64>,
|
||||
) -> Option<PyVitalEstimate> {
|
||||
let est = py.allow_threads(|| self.inner.extract(&residuals, &phases));
|
||||
est.map(PyVitalEstimate::from_rust)
|
||||
}
|
||||
|
||||
|
||||
@@ -185,10 +185,44 @@ def test_heart_rate_explicit_ctor() -> None:
|
||||
|
||||
def test_heart_rate_extract_returns_none_with_too_few_samples() -> None:
|
||||
hr = HeartRateExtractor.esp32_default()
|
||||
out = hr.extract(residuals=[0.0] * 56, weights=[])
|
||||
out = hr.extract(residuals=[0.0] * 56, phases=[0.0] * 56)
|
||||
assert out is None
|
||||
|
||||
|
||||
def test_heart_rate_extract_rejects_old_weights_keyword() -> None:
|
||||
hr = HeartRateExtractor.esp32_default()
|
||||
with pytest.raises(TypeError):
|
||||
hr.extract(residuals=[0.0] * 56, weights=[0.0] * 56)
|
||||
|
||||
|
||||
def test_heart_rate_extract_with_synthetic_signal_and_phases() -> None:
|
||||
"""Drive the extractor with a synthetic 1.2 Hz sine (72 BPM) plus
|
||||
same-length phase data. This proves the binding feeds Rust's required
|
||||
`phases` slice instead of an empty vector that would keep returning None."""
|
||||
hr = HeartRateExtractor.esp32_default()
|
||||
sample_rate = 100.0
|
||||
target_freq = 1.2 # 72 BPM
|
||||
n_samples = int(60 * sample_rate)
|
||||
phases = [i * 0.01 for i in range(56)]
|
||||
|
||||
produced_estimate = False
|
||||
rng = Random(43)
|
||||
for i in range(n_samples):
|
||||
t = i / sample_rate
|
||||
base = math.sin(2.0 * math.pi * target_freq * t)
|
||||
residuals = [base + rng.gauss(0.0, 0.01) for _ in range(56)]
|
||||
est = hr.extract(residuals=residuals, phases=phases)
|
||||
if est is not None:
|
||||
produced_estimate = True
|
||||
assert math.isfinite(est.value_bpm)
|
||||
assert 0.0 <= est.confidence <= 1.0
|
||||
break
|
||||
|
||||
assert produced_estimate, (
|
||||
"HeartRateExtractor never produced an estimate after 60s of synthetic data"
|
||||
)
|
||||
|
||||
|
||||
# ─── Build feature flag ──────────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user