process_frame in vital_signs.rs uses arithmetic mean + arithmetic variance on phase values, but the phases come from q_val.atan2(i_val) (csi.rs:90, main.rs:855) so they're wrapped to (-pi, pi]. When two subcarriers happen to land on opposite sides of the wrap (e.g. 3.14 and -3.14, physically ~0.003 rad apart), the linear formula treats them as ~2*pi apart and you get a variance of ~pi^2 instead of ~1e-6. That value goes straight into the heart-rate FFT buffer.
Repro on a fresh clone:
git clone --depth 1 https://github.com/ruvnet/RuView.git && cd RuView
sed -n '142,148p' v2/crates/wifi-densepose-sensing-server/src/vital_signs.rs
python3 -c "
import math
phases = [math.pi - 0.001, -math.pi + 0.001]
m = sum(phases) / 2
print('linear: ', sum((p - m) ** 2 for p in phases) / 2)
s = sum(math.sin(p) for p in phases); c = sum(math.cos(p) for p in phases)
print('circular:', 1 - (s * s + c * c) ** 0.5 / 2)
"
You'll see linear: 9.86... vs circular: 5e-07 on the same input.
Probably the cause of the very jumpy vitals reported in #519, and the +/-15 bpm jitter mentioned in #485's description. #485 routes around the buggy detector with an external pipeline but keeps VitalSignDetector as the low-confidence fallback, so the math is still live in that path.
Fix is the standard circular variance (1 - R, mean resultant length). PR coming.
process_frame in vital_signs.rs uses arithmetic mean + arithmetic variance on phase values, but the phases come from
q_val.atan2(i_val)(csi.rs:90, main.rs:855) so they're wrapped to (-pi, pi]. When two subcarriers happen to land on opposite sides of the wrap (e.g. 3.14 and -3.14, physically ~0.003 rad apart), the linear formula treats them as ~2*pi apart and you get a variance of ~pi^2 instead of ~1e-6. That value goes straight into the heart-rate FFT buffer.Repro on a fresh clone:
You'll see
linear: 9.86...vscircular: 5e-07on the same input.Probably the cause of the very jumpy vitals reported in #519, and the +/-15 bpm jitter mentioned in #485's description. #485 routes around the buggy detector with an external pipeline but keeps VitalSignDetector as the low-confidence fallback, so the math is still live in that path.
Fix is the standard circular variance (1 - R, mean resultant length). PR coming.