Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix(calibration): ADR-151 presence flatline — per-frame p90|z| replaces floored median|z|; gates re-derived from bench; unsanitised phase excluded from motion#1015

Open
stuinfla wants to merge 1 commit into
ruvnet:main from
stuinfla:fix/adr-151-presence-p90

Conversation

@stuinfla

@stuinfla stuinfla commented Jun 11, 2026

Copy link
Copy Markdown

Summary

ADR-151 enrollment can accept an occupied room as the empty anchor — bench-reproduced on real ESP32-C6 HE20 captures and fixed here by replacing the per-frame presence statistic (median of |z| over subcarriers) with the per-frame p90 of |z|, re-deriving the gates from measured data, and excluding the unsanitised phase channel from motion.

The defect (measured, not theorized)

AnchorRecorder computes presence_z as the mean over frames of CalibrationDeviationScore::amplitude_z_median — the per-frame median of |z| across all active subcarriers (enrollment.rs, scored in wifi-densepose-signal ruvsense/calibration.rs::deviation).

The median-floor math: a person perturbs a minority of subcarriers strongly. For the majority of bins that stay statistically like the baseline, |z| ~ |N(0,1)|, whose median is 0.674. The median over all bins therefore floors near 0.674 and barely moves when someone enters the room. The default gates (empty_max_z = 1.0, min_presence_z = 1.5) sit in a band the statistic can rarely reach in either direction.

Measured on real captures (ESP32-C6 XIAO, HE20 2.4 GHz, 256-bin frames, node 8, replayed over UDP into the unmodified d0e27e65 CLI):

segment per-frame median |z| (old statistic) per-frame p90 |z| (this PR)
empty room (3 segments, same epoch) 0.40–0.42 1.27–1.33
person moving 0.74–0.75 2.26–2.29

Both old values are below every gate — so on the clean upstream binary, enroll fed 100 % person-moving frames ACCEPTED the empty anchor (presence_z = 0.93). The p90 separates cleanly because the upper tail is exactly where the body's multipath perturbation lives.

Second finding — phase channel is noise on the CLI path: CalibrationRecorder's documented precondition is PhaseSanitizer + phase_align.rs, but the CLI UDP path feeds raw phase. The resulting baseline phase_dispersion ≈ 0.965 (uniformly random), so phase_drift_median ≈ π/2 on every frame and the |Δφ| motion term inflated still-room motion rates to ~60–70 % pure noise.

The fix

  • CalibrationDeviationScore gains amplitude_z_p90 (numpy-convention linear interpolation; the median stays as a secondary/diagnostic field) and phase_usable (baseline median Von Mises dispersion ≤ PHASE_DISPERSION_USABLE_MAX = 0.5; the bench's unsanitised baseline measures 0.965, sanitized pipelines land ≤ 0.3 per CalibrationConfig::max_phase_variance).
  • motion_flagged and the recorder's |Δφ| motion term only use phase when phase_usable.
  • AnchorRecorder gates presence on the p90; gates re-derived from the bench bands with margin: EMPTY_MAX_Z = 1.6, MIN_PRESENCE_Z = 1.9 (constants, with the bench numbers cited in the doc comments).
  • calibrate banner now prints z_p90 alongside z_med.

Bench proof (replay harness, same-epoch baseline)

Harness: parse the Pi's raw csi_raw bins, replay node-8 256-bin frames over UDP at 50 Hz into the rebuilt CLI; calibrate a 600-frame baseline from an empty segment, then run enroll (first anchor = empty) against held-out segments from the same capture window (same-epoch requirement — cross-epoch baselines drift; mean-shape correlation across epochs measured only ~0.31).

test input verdict
(a) calibrate empty segment (10:30) baseline 600 frames / 242 subcarriers, banner z_med ≈ 0.3–0.4, z_p90 ≈ 0.7–1.4
(b) empty anchor fed person-moving frames occupied bin REJECTEDroom not empty (presence_z 2.54 > 1.60) (upstream binary: ACCEPTED at 0.93)
(c) empty anchor fed held-out empty segment (10:31) empty bin ACCEPTEDpresence_z=1.21

Separation 1.21 vs 2.54 against a gate of 1.6, consistent with the offline study bands.

Tests

  • wifi-densepose-calibration: 43 unit tests green (suite updated for the p90 statistic; new regression tests: presence_uses_p90_not_median_floor, unusable_phase_does_not_create_motion) + full_loop integration green (sim now models the measured empty z-scale: real baselines integrate drift into the Welford variance, so runtime z ~ N(0, 0.82) — matching the measured empty p90 of 1.27–1.33 vs 1.645 for ideal i.i.d. noise).
  • wifi-densepose-cli: 19 tests green. wifi-densepose-signal: all suites green.
  • No new clippy warnings (per-file warning counts identical to d0e27e65).

🤖 Generated with claude-flow

...ces floored median|z|
The enrollment presence gate computed presence_z as the mean over frames of
the per-frame MEDIAN of |z| across all subcarriers. A person perturbs only a
minority of subcarriers, so that median floors at median(|N(0,1)|) ≈ 0.674
and the gates (empty_max_z=1.0, min_presence_z=1.5) were unreachable: on
real ESP32-C6 HE20 captures an occupied room measured 0.74–0.75 — the
'enroll accepts an occupied room as empty' flatline.
- CalibrationDeviationScore gains amplitude_z_p90 (the upper tail is where
 the body's multipath perturbation lives); the median stays as a secondary
 diagnostic. Bench separation on real node-8 captures vs a same-epoch
 baseline: empty 1.27–1.33 vs person-moving 2.26–2.29.
- AnchorRecorder gates presence on p90; gates re-derived from the bench:
 EMPTY_MAX_Z = 1.6, MIN_PRESENCE_Z = 1.9.
- Unsanitised phase excluded: scores carry phase_usable (baseline median
 Von Mises dispersion ≤ 0.5); when false the phase channel is dropped from
 motion_flagged and from the recorder's |Δφ| motion term (the CLI UDP path
 skips PhaseSanitizer; its baseline dispersion ≈ 0.965 made phase drift
 ≈ π/2 noise that inflated motion rates to ~60–70 % on a still room).
- Tests: enrollment suite updated + p90/median-floor and phase-noise
 regression tests; full_loop sim models the measured empty z-scale (0.8).
Co-Authored-By: claude-flow <ruv@ruv.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /