Quantitative Cramer-Rao Lower Bound analysis for WiFi ranging via both
Time-of-Arrival and phase-based methods, with multistatic 4-anchor
position-error budget.
Headline (20 MHz HT20, 20 dB SNR, 100 averaged frames):
- ToA range CRLB: 4.1 cm
- Phase (5 deg noise): 0.17 mm
- Phase advantage: 240x (after ambiguity resolution)
4-anchor convex-hull room (GDOP 1.5):
- ToA position precision: 25 cm (room-pose-quality floor)
- Phase position precision: 1 mm (RTK-quality, ambiguity-resolved)
This is the strongest architectural lever this loop has surfaced for
ADR-029 (multistatic sensing). The current learning-based attention
approach has no provable precision floor; an explicit ToA-then-phase
pipeline sits within 2x of CRLB by Kay's theory.
Composes cleanly with R6:
- R6 gives the spatial sensitivity envelope (40 cm Fresnel at 2.4 GHz)
- R1 gives the ranging precision within it (1 mm phase, 4 cm ToA averaged)
- Independent, additive, together bound full multistatic geometry budget
Closes a gap R10 created: foliage drops SNR, which directly worsens
ToA CRLB. A 50 m foliage link at 5 dB SNR drops to ~1 m ToA precision.
R10's 100 m sparse-foliage range is *detectable* not *localisable*.
Honest scope:
- CRLB is a lower bound; real estimators sit 1-2x above it
- 5 deg phase noise assumes phase_align.rs is applied
- Multipath degrades CRLB by 2-5x even with MUSIC super-resolution
- Integer-ambiguity (cycle-slip) is unsolved per-subcarrier; needs
multi-subcarrier wide-lane unwrap
Coordination: ticks/tick-9.md, no PROGRESS.md edit.
The workspace DSP (vital_signs, multistatic, pose_tracker, tomography)
implicitly assumes a forward model that maps scatterer geometry to
per-subcarrier phase shifts. Nobody had written it down. This tick
makes it explicit.
Closed-form first-Fresnel-zone radius + point-scatterer path-delta +
per-subcarrier phase prediction over 802.11n/ac 20 MHz channels (52
subcarriers, 312.5 kHz spacing). Pure NumPy demo + JSON output for
downstream consumers.
Headline numbers:
- 5 m link first-Fresnel radius @ midpoint: 40 cm (2.4 GHz), 27 cm (5 GHz)
- Inside zone-1: phase spread <0.5 deg across 52 subcarriers (band-flat)
- Outside zone-1: phase spread up to 16 deg (band-dispersed)
This unifies R5 + R6: R5's experimentally measured band-spread top
subcarriers is exactly what the Fresnel forward model predicts for
zone-1 occupancy.
Closes the loop on three earlier threads:
- R7 (mincut adversarial) gets a precise definition of 'physically
inconsistent' instead of a learned classifier
- R10 (foliage range) needs to retract 100 m sparse estimate to ~70 m
to account for Fresnel-zone obstruction
- R12 (eigenshift negative result) gets its revision basis: PABS over
Fresnel-grounded forward operator
Honest scope: point-scatterer only, first Fresnel only, frequency-flat
reflectivity, LOS-only (no multipath). The scalar version is the right
first-order approximation; volume-integral / multi-zone / multipath
extensions catalogued as R6.1+R6.2 follow-ups.
Coordination: ticks/tick-8.md, no PROGRESS.md edit.
ITU-R P.833-9 vegetation-attenuation model + ESP32-S3 link-budget
solver produce bounded sensing range estimates per frequency and
foliage density. Plus a biomechanics-grounded gait-frequency taxonomy
spanning bears (0.5 Hz) to mice (15 Hz).
Headline ranges (121 dB link budget, 10 dB SNR margin):
freq sparse moderate dense
2.4 GHz 99.6 m 12.0 m 4.1 m
5 GHz 19.9 m 5.2 m 2.1 m
The 2.4 GHz / sparse cell (~100 m) is the practical sweet spot —
10x camera-trap coverage, always-on rather than PIR-triggered.
Honest scope called out explicitly: this is feasibility math, not
field measurements. Animal cooperation, foliage flutter, regulatory
limits, and BSSID-fingerprint degradation in remote forest are all
real follow-up problems.
Vertical applications (10-20 year horizon) catalogued:
- Endangered-species population census
- Wildlife corridor verification
- Invasive-species early warning
- Anti-poaching (human gait well-separated from wildlife)
- Livestock-on-rangeland tracking
- Agricultural pest control
Cross-connects to:
- R5 (saliency is task-specific — per-species classifier needs own
saliency map, same lesson as R12)
- R8 (wildlife sensing wants CSI not RSSI for per-subcarrier shape)
- R9 (fingerprint K-NN primitive transfers to per-individual ID)
- R7 (multi-link consistency for corridor coverage)
Pure-NumPy, no framework deps. ITU model + binary search solver.
Coordination: tick avoided PROGRESS.md to prevent races (horizon-
tracker M3+ track concurrent at the time).
Files:
* examples/research-sota/r10_foliage_attenuation.py
* examples/research-sota/r10_foliage_results.json
* docs/research/sota-2026-05-22/R10-through-foliage-wildlife.md
* docs/research/sota-2026-05-22/ticks/tick-6.md
Tests the simplest possible algorithm for RF-weather change detection:
SVD on per-frame CSI matrix, top-10 singular values, cosine distance
between spectra over time. Hypothesis: a synthetic structural
perturbation (15 percent attenuation on 3 top-saliency subcarriers)
should produce a larger spectral shift than natural temporal drift
from operator movement in the same recording.
Result honestly: it does not. The perturbation distance (0.00024) is
*smaller* than the control distance (0.00035) — signal/drift ratio
0.69x. The top-K SVD-spectrum cosine is too coarse to detect
small-magnitude subcarrier-specific structural changes against an
operator-noise background.
Three concrete fixes identified for follow-up ticks:
1. Principal angles between subspaces (PABS), not cosine on singular
values — catches subspace rotations the spectrum misses
2. Per-subcarrier residual analysis after projecting onto baseline
subspace — localises the perturbation
3. Multi-day baseline — knocks down operator-noise floor by 50-100x
Useful cross-validations the negative result produces:
* R5 task-specific saliency (count-task) does not generalise to
structure-detection saliency. Same data, different relevant
features. Publishable distinction.
* R12 is CSI-only territory — RSSI is the trace of the CSI
covariance, so if top-10 SVD-spectrum can't see this, RSSI can't
either. Bounds R8 commercial-enablement story to counting only.
* R7 SVD-spectrum primitive that worked for adversarial detection
fails here at lower perturbation magnitude. Sensitivity does NOT
scale with subtlety — confirms the algorithm is magnitude-dominated.
Long-horizon vision (building structural monitoring, earthquake drift,
HVAC audits, climate-controlled-archive surveillance) preserved in the
research note — the physics is right, the hardware is sufficient,
the deployment story works. Just need PABS + multi-day data.
Coordination note: this tick avoided PROGRESS.md edits entirely
because horizon-tracker is concurrently editing it. Tick-5 summary
written to ticks/tick-5.md (new self-contained convention) so the
08:00 ET final summary can consolidate without conflicts.
Files:
* examples/research-sota/r12_rf_weather_eigenshift.py
* examples/research-sota/r12_rf_weather_results.json
* docs/research/sota-2026-05-22/R12-rf-weather-mapping.md
* docs/research/sota-2026-05-22/ticks/tick-5.md
* research(R9): RSSI fingerprint K-NN — 2.18x lift (MODERATE); surfaces counting-vs-localization asymmetry
Hypothesis: if temporal proximity correlates with RSSI-feature
proximity in the existing single-session data, RSSI fingerprinting is
viable. If K-NN of each query is random in time, RSSI sequences are
too noisy for fingerprint localization.
Test: 1077 samples, 20-dim RSSI proxy (band-mean across 56
subcarriers), cosine-NN with K=5, measure fraction of K-NN within
plus/minus 60s of each query timestamp. Compare to random baseline.
Result (honest):
5-NN within +/-60s 0.169
Random baseline 0.077
Lift over random 2.18x (verdict: MODERATE)
Per-query stdev 0.183
Below the >=3x STRONG-fingerprint threshold but well above 1x random.
Real signal, but weaker than R8 counting result on the same data.
Important asymmetry surfaced (publishable distinction):
Task RSSI vs CSI retention Verdict
------- ----- -----
Counting 94.82% (R8) RSSI works well
Localization ~2x random (R9) RSSI struggles in this regime
This is consistent with R5's band-spread observation: the count signal
integrates across the band, but localization may require per-subcarrier
shape that the band-mean discards.
Three actionable explanations for the MODERATE result:
1. 20-frame windows (~2s) too short for stable fingerprint while operator
moves — longer windows might lift to 3-4x.
2. Within-room fingerprint space too narrow — multi-room data would
show categorical lift jump (5-10x).
3. Band-mean discards the per-subcarrier shape needed for localization.
Once multi-room data lands (#645), this test should be re-run; if
hypothesis (2) is right, the lift will jump categorically.
Files:
* examples/research-sota/r9_rssi_fingerprint_knn.py
* examples/research-sota/r9_rssi_fingerprint_results.json
* docs/research/sota-2026-05-22/R9-rssi-fingerprint-knn.md
* docs/research/sota-2026-05-22/PROGRESS.md updated
* feat(tools/ruview-mcp): M2 — wire real inference via cog health subcommand
ruview_pose_infer and ruview_count_infer now run the cog binary's `health`
subcommand (ADR-100 contract) which performs real Candle forward-pass
inference on a synthetic CSI window and emits a structured health.ok JSON
event containing backend, confidence (pose) or count/confidence/p95_range
(count). The MCP tools parse this event and return typed inference results.
This satisfies the ADR-104 acceptance gate: "ruview_pose_infer returns a
finite output for a synthetic CSI window" when the cog binary is installed.
On machines without the binary, both tools still fail-open with {ok:false,
warn:true} and actionable install hints.
Also updates PROGRESS.md with cross-links: R7 (Stoer-Wagner) and R8
(RSSI-only 94.82% retained) marked done with cron-originated findings
distilled into the research vectors section.
Co-Authored-By: claude-flow <ruv@ruv.net>
Premise: in a multi-node CSI mesh, all nodes see the same physical
scene through slightly different multipath. Their per-window CSI
vectors cluster tightly under cosine similarity. An adversarial node
(replay / shift / noise injection) sits *outside* that cluster. The
Stoer-Wagner minimum cut on the inter-node similarity graph isolates
it cleanly when the cut is sharp.
Demo synthesises 4 honest nodes (one real CSI window from the paired
data + per-node Gaussian noise 6 dB below signal) and 1 adversarial
node under three attack modes. Cosine-similarity matrix, then
Stoer-Wagner mincut, then check whether partition_B is the singleton
{4} — the adversarial node.
Attack Mincut value Partition_B Isolated?
------- ------------ ----------- ---------
replay 3.4513 {4} YES
shift 3.5724 {4} YES
noise 2.5586 {4} YES
Detection rate: 3/3 = 100%.
Architectural payoff: this is the primitive that fills the stub at
. ADR-103 v0.2.0
can wire it in directly. The mincut value also becomes a continuous
'mesh trustworthiness' metric for the cog-gateway dashboard.
Honest scope: the demo uses sloppy attackers. Adaptive attackers who
have read this note can almost certainly evade by adding calibrated
noise that keeps cosine similarity above the cluster floor. The next
research step is the Stackelberg-game extension. See the
'Honest scope of this result' section in the research note.
Connections:
* R5 — top-8 saliency subcarriers are the priority list for a
more-targeted per-subcarrier consistency check.
* R8 — same primitive likely works at lower SNR with RSSI-only
metrics; cluster structure is preserved by the band integral.
Files:
* examples/research-sota/r7_multilink_consistency.py — pure-NumPy
Stoer-Wagner mincut + synthetic-adversary harness.
* examples/research-sota/r7_multilink_consistency_results.json —
full result JSON for cross-tick reproducibility.
* docs/research/sota-2026-05-22/R7-multilink-consistency.md — note.
* docs/research/sota-2026-05-22/PROGRESS.md — updated index + Done.
Builds directly on R5's band-spread observation. If the count-task
signal is spread across the WiFi band (R5: max/mean ratio 2.85× across
56 subcarriers), then RSSI — which is the integral of |H_k|^2 across
the band — keeps most of the information. The naive prior (RSSI throws
away 98% of CSI bytes) is misleading; the relevant metric is how much
of the *signal* is in the integral, not how many bytes are in the
representation.
Tested by aggregating each existing [56 × 20] CSI window down to a
[20]-vector RSSI proxy (mean across subcarriers per frame), training a
tiny MLP (Linear 20→32→8, 656 params, 5 KB) with vanilla NumPy SGD for
200 epochs on the same random 80/20 split as cog-person-count v0.0.2.
Result:
Full CSI v0.0.2 62.3% accuracy
RSSI-only (this) 59.1% accuracy = 94.82% retained
Per-class is also markedly more *balanced* (RSSI: 59.5 / 58.6 ; full
CSI: 86.2 / 34.3) — the tiny model on a low-dim input can't cheat by
leaning on class 0 the way v0.0.2's larger model does at inference.
What this enables on a 10-year horizon: phones, laptops, smart
speakers, smart TVs, smart lights — anything with WiFi reports RSSI
and anything with a CPU can run a 656-param MLP. Person counting
becomes a federated property of any room with WiFi, not a property of
the ESP32-S3 fleet.
What this doesn't prove (called out explicitly in the research note):
- Single room, single operator, single 30-min recording
- 2-class problem (label distribution is {0, 1})
- Single random draw — needs K-fold + multi-room replication
Three follow-up experiments queued in R8-rssi-only-count.md §'What's
next on this thread':
- Multi-room replication once #645 lands
- 3-class extension (0 / 1 / 2+) — measure the info-rate cliff
- Run on a non-ESP32 RSSI source (e.g. iw event on Linux laptop)
Files:
* examples/research-sota/r8_rssi_only_count.py — pure-NumPy, no
framework deps. Trains + evals in 0.72 s on CPU.
* examples/research-sota/r8_rssi_only_results.json — full JSON dump
for cross-tick reproducibility.
* docs/research/sota-2026-05-22/R8-rssi-only-count.md — method,
measured numbers, interpretation, what doesn't work yet.
* docs/research/sota-2026-05-22/PROGRESS.md — updated index + Done
log.
Coordination note: horizon-tracker is working on tools/ruview-mcp/
+ tools/ruview-cli/ + ADR-104 — this commit deliberately stays out
of those paths.
Sets up docs/research/sota-2026-05-22/ as the autonomous-research
output dir, with PROGRESS.md as the canonical 15-vector research
agenda spanning spatial intelligence, RF features, RSSI-only, and
exotic/long-horizon verticals. Cron d6e5c473 (*/10 * * * *) picks
threads from this file and self-terminates at 2026-05-22 08:00 ET.
First concrete contribution this tick — R5 subcarrier saliency:
* examples/research-sota/r5_subcarrier_saliency.py: pure-numpy port
of the count cog's Conv1d encoder + count head, computes per-
subcarrier input×gradient saliency via central-difference. 128
samples × 56 subcarriers × 2 forward passes/subcarrier ≈ ~3 s on
CPU, no GPU or framework dependency.
* docs/research/sota-2026-05-22/R5-subcarrier-saliency.md: research
note with motivation, method, novelty argument, and the first
measured ranking. Top-8 subcarriers for cog-person-count v0.0.2:
[41, 52, 30, 31, 10, 35, 2, 38]. Max/mean ratio 2.85x.
* v2/crates/cog-person-count/cog/artifacts/saliency.json: machine-
readable per-subcarrier saliency + top-K lists, so future-tick
experiments (retrain at K=8/16/32) consume it without re-running.
Key insight from the first measurement: top-8 saliency is *band-
spread* (indices span 2-52), not concentrated. This directly raises
R8's (RSSI-only) feasibility ceiling, because RSSI is a band-
aggregate — it retains the integral of a band-spread signal. First-
order estimate: RSSI-only should hit ~60% of full-CSI accuracy for
the count task. R7 (adversarial defence) inherits a concrete defender-
priority list: corroborate these 8 subcarriers across nodes.
This commit is the first of many short, focused contributions over
the next ~12 hours. PROGRESS.md is the canonical pointer for the
next tick to pick up the next thread.
Demos 04 and 05 work fine locally — operator has assets/X Bot.fbx
present. On the gh-pages deploy the FBX is intentionally absent
(Mixamo license boundary, .gitignored) and the previous onError
handler just logged 'FBX load failed' to the console and left a
stuck '⚠ Load failed — see console' message in the overlay.
Replaces both onError handlers with an in-page card that:
- Explains why the asset is missing (license boundary, not a bug)
- Tells you exactly how to run it locally (Mixamo download path,
where to drop the file, the serve-demo.py command)
- Links to Mixamo + the repo source + back to the gallery
- Lets the ADR-097 helpers scene keep rendering behind it
- Logs at warn (not error) — no more uncaught console.error noise
The success branch is untouched, so local development is identical
to before.
Adds a new GitHub Pages workflow that publishes the ADR-097 three.js
demo gallery alongside the existing observatory/, pose-fusion/,
pointcloud/, and nvsim/ deployments. Uses keep_files: true so the
other deployments are preserved.
What ships:
* `examples/three.js/index.html` — new landing page that lists all 5
demos with screenshots, "standalone" vs "needs FBX" badges, and an
honest note explaining the Mixamo X Bot.fbx license boundary
(demos 04 and 05 need a local download from mixamo.com; demos
01-03 run standalone in any modern browser).
* `.github/workflows/threejs-pages.yml` — staged copy of demos/,
screenshots/, README.md, and the new index.html into
`_site/three.js/`. Drops an `assets/README.txt` placeholder
explaining the FBX-not-shipped policy. Triggered on changes to
examples/three.js/** or the workflow itself.
* README.md — adds the live link to the existing demo row
(`▶ three.js Demos (5)`) plus a one-line callout describing the
gallery and the FBX caveat.
After this PR merges, the workflow runs and publishes:
https://ruvnet.github.io/RuView/three.js/
* feat(examples/three.js): cinematic skinned realtime pose demo + ESP32 CSI bridge
Five-stage example progression exploring three.js helpers (ADR-097 surface) as
a viewer for live RuView sensor data:
1. helpers-demo.html — clean ADR-097 helper reference (GridHelper,
PolarGridHelper, BoxHelper, AxesHelper),
file://-safe, no backend
2. helpers-cinematic.html — same scene + UnrealBloomPass + pseudo-CSI
sonar pings + tomography sweep + procedural
cyber floor + ambient drift particles
3. helpers-skinned.html — replaces sphere skeleton with Mixamo X Bot
via GLTFLoader from threejs.org CDN, plays
bundled animations with additive blending
4. helpers-skinned-fbx.html — same but loads a local Mixamo FBX (needs
serve-demo.py — file:// can't fetch local
siblings). Drop X Bot.fbx alongside.
5. helpers-skinned-realtime.html — webcam → MediaPipe Pose Heavy →
poseWorldLandmarks → direct quaternion
retargeting onto the Mixamo skeleton.
Real ESP32-S3 CSI streamed over WebSocket
from ruvultra (Tailscale, port 8766).
Supporting:
- serve-demo.py threaded HTTP server with no-cache headers
(fixes net::ERR_EMPTY_RESPONSE on the FBX path)
- ruvultra-csi-bridge.py ESP32 RuView firmware tick → WebSocket bridge,
runs as systemd-run unit on ruvultra
Bugs found + fixed along the way (all documented in code comments):
- FBX exports yield TWO parallel Bone trees with identical names; only the
SkinnedMesh.skeleton.bones one drives visible deformation. model.traverse
finds orphans.
- Mixamo FBX nests a zero-length wrapper bone above the real bone, same name.
bone.children[0].getWorldPosition == bone.getWorldPosition → restDir is
(0,0,0) → setFromUnitVectors collapses to identity. Walk past same-named
same-position wrappers when computing tail.
- AnimationMixer.update() with a "stopped" action still mutates bones unless
enabled=false is set.
Retargeting layer in helpers-skinned-realtime.html:
- 12 bones direct quaternion retarget (arms × 2, legs × 2, spine × 3, neck)
- Hips root rotation from shoulder/hip line basis (torso twist + lean)
- Neck aims at ear-midpoint (kp 7+8), not nose (kp 0), to remove the
forward bias of the protruding-nose anchor
- One Euro Filter per landmark per axis (Casiez 2012) — adaptive low-pass
- Visibility-weighted per-bone slerp gain — occluded limbs relax to rest
- URL toggles: ?mirror= ?yflip= ?zflip= ?cnn=0/1/2 ?csi=ws://...
Live CSI integration:
- Bridge parses adaptive_ctrl tick lines (motion/presence/rssi/yield)
- Browser fans single ESP32 reading across 4 UI nodes with phase-shifted
wobble (0.88–1.00 × sin(t·0.55 + offsetᵢ))
- EMA α=0.06 (~3 sec time constant), HUD update throttled 3 Hz
Co-Authored-By: claude-flow <ruv@ruv.net>
* refactor(examples/three.js): organize into demos/screenshots/server/assets + add README
Flatten the 13-file flat layout into purposeful subfolders so the demo
collection has a clean top-level entry point (README.md) and the file roles
are obvious from a directory listing.
Layout:
demos/ 01..05 — numbered for the progression (helpers → cinematic →
skinned → skinned-fbx → skinned-realtime)
screenshots/ one PNG per demo, matching the demo's filename prefix
server/ serve-demo.py + ruvultra-csi-bridge.py
assets/ X Bot.fbx (gitignored, used by demos 04 and 05)
Touched files (beyond the renames):
- 04-skinned-fbx.html, 05-skinned-realtime.html: MODEL_URL now resolves
'../assets/X%20Bot.fbx' instead of './X%20Bot.fbx'
- server/serve-demo.py: chdir() walks 3 levels up to repo root (was 2), and
the URL banner now lists all 5 demos
- .gitignore: comment refresh — points at assets/ and screenshots/
- 05-skinned-realtime.html also picks up in-flight fps-tune work from this
branch (Holistic script, SMOOTH_K URL param, slerp gain scaling) since
those edits and the rename hit the same file
Verified end-to-end:
- python examples/three.js/server/serve-demo.py
- all 5 demos return 200, X Bot.fbx returns 200 from new asset/ path
- demos 04 + 05 render the X Bot mesh; 0 JS errors via browser eval
- screenshots reproduced match the originals
Co-Authored-By: claude-flow <ruv@ruv.net>
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/)
without any sibling under rust-port/ that warranted the extra level.
Move the whole workspace up to v2/ to match v1/ (Python) at the same
depth and shorten every cd / build command across the repo.
git mv preserves history for all tracked files. 60 files updated for
path references (CI workflows, ADRs, docs, scripts, READMEs, internal
.claude-flow state). Two manual fixes for relative-cd paths in
CLAUDE.md and ADR-043 that became wrong after the depth change
(cd ../.. → cd ..).
Validated:
- cargo check --workspace --no-default-features → clean (after target/
nuke; the gitignored target/ was carried by the OS rename and had
hard-coded old paths in build scripts)
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed,
8 ignored (same totals as pre-rename)
- ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm)
After-merge follow-up: contributors should `rm -rf v2/target` once and
let cargo regenerate from the new path.
- examples/medical/README.md: full guide for BP estimator,
hardware requirements, sample output, accuracy table, AHA
categories, disclaimer, RuView integration explanation
- README.md: added Medical Examples to documentation table
Co-Authored-By: claude-flow <ruv@ruv.net>
Reads real-time heart rate from MR60BHA2 60 GHz mmWave sensor and
estimates BP trends using HR/HRV correlation model:
- Mean HR → baseline SBP/DBP
- SDNN (HRV) → sympathetic/parasympathetic adjustment
- LF/HF spectral ratio → fine adjustment (with numpy)
- Optional calibration with a real BP reading
Verified on real hardware: 125/83 mmHg estimate from 35 HR samples
over 60 seconds at 84 bpm mean HR with 91ms SDNN.
NOT A MEDICAL DEVICE — research/wellness tracking only.
Co-Authored-By: claude-flow <ruv@ruv.net>