Files
ruvnet--RuView/docs/adr/ADR-275-rf-aware-gaussian-spatial-memory.md
T
rUv 2e018f4f19 feat(ruview-unified): Unified RF spatial world model — ADR-273..282 (#1437)
Native frame contract, universal RF encoder, RF-aware Gaussian spatial memory, physics-guided synthetic RF worlds, edge sensing control plane, BLE-CS + factorized pose. All 10 ADRs (273-282) fully implemented and tested (99 tests); ADR-278 (radar inverse rendering) honestly gated with zero code as a future research program.

Deep-reviewed and hardware-tested against a live ESP32-C6 CSI node before merge: fixed a reachable panic, a silent NaN-corruption path, a cross-entity Gaussian conflation bug, and a wrong-center-frequency bug in the WiFi adapter (confirmed live: was misreporting channel 4 as 2437 MHz, now correctly reports 2427 MHz matching the hardware parser exactly). Added a standing hardware-in-the-loop test (examples/esp32_live_hardware_test.rs). Also fixed unrelated pre-existing issues surfaced during validation (wifi-densepose-core clippy warnings, a ruview-auth Windows build break, a sensing-server test flake).

Full review: https://gist.github.com/ruvnet/89795f3c4b8ea166cff5ac35ae4c7651
2026-07-26 14:37:56 -07:00

80 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ADR-275: RF-aware Gaussian spatial memory — the persistent scene representation
| Field | Value |
|-------|-------|
| **Status** | Accepted — **P1 implemented** (`ruview-unified/src/gaussian/`: `primitive.rs`, `map.rs`, `gain.rs`, `graph.rs`; 16 unit tests, criterion benches) |
| **Date** | 2026-07-26 |
| **Parent** | ADR-273 |
| **Relates to** | ADR-030 (persistent field model — superseded in direction by this), ADR-134 (CIR/ISTA), ADR-147 (OccWorld priors), ADR-261 (RuVector graph-ANN — the retrieval layer this memory will index into) |
## 0. PROOF discipline
Grades per ADR-273 §0. The July 2026 external motivators (EmbodiedSplat ~5 fps online semantic Gaussian mapping, ~67× memory efficiency; TGSFormer bounded temporal Gaussian memory; physics-informed channel-gain mapping with incremental Gaussian insertion; JITOMA task-gated activation) are EXTERNAL-UNVERIFIED throughout.
## 1. Context
RuView's spatial state is currently scattered (pose tracker state, field-model eigenstructure, worldgraph tracks). Vision-side SOTA converged on Gaussian fields as the common continuous scene memory, and — the July signal that matters here — the representation crossed into RF: propagation geometry, opacity, attenuation, and scattering as Gaussian primitives, updated *incrementally* when the environment changes. That is exactly the bridge from RuView sensing to a queryable digital twin: one store that answers both geometric questions ("what is near the sofa") and RF questions ("which object caused the channel anomaly", "where did multipath change").
## 2. Decision — the primitive
`RfGaussian` (`primitive.rs`) carries all six ADR-273 attribute groups:
1. **Geometry**: position, per-axis scale (σ), unit-quaternion orientation → anisotropic metric `Σ⁻¹ = R·diag(1/σ²)·Rᵀ`.
2. **Semantics**: 16-d embedding (RuVector-alignable).
3. **RF response**: reflectivity `[4 bands × 4 incident-angle bins]` (2.4/5/6/60 GHz), plus `occupancy` = peak extinction coefficient (nepers/m) used by the gain model.
4. **Motion**: signed Doppler m/s + `{Static, Slow, Fast}` class.
5. **Trust/lifecycle**: confidence ∈ [0,1], timestamp, decay τ, `Provenance {device, model_version, synthetic}`.
6. **Links**: typed references into the scene graph / RuVector entities.
Validated constructor (quaternion normalized, ranges checked); anisotropy and rotation are proven behaviorally (thin axis decays ≥ 80× faster at 0.3 m — the analytic ratio is 86; a 90° quaternion rotates the metric with it).
## 3. Decision — the map
`GaussianMap` (`map.rs`): spatial-hash grid (1 m default pitch) over a flat store.
- **Fusion, not accumulation**: an insert within Mahalanobis² 9 of a same-entity-kind Gaussian merges — confidence-weighted position/scale/occupancy/semantics/reflectivity/Doppler, noisy-OR confidence (`c₁+c₂−c₁c₂`), newest provenance wins, links union. Test: two 0.5-confidence observations 0.1 m apart fuse to one Gaussian at the weighted midpoint with confidence 0.75.
- **Decay + static persistence** (update-loop step 7): exponential confidence decay per Gaussian τ, **stretched by observed lifetime**`τ_eff = τ·(1 + ln(1 + lifetime/τ))` with `lifetime = last_seen first_seen` — so a wall confirmed over 30 min outlives a once-seen transient at equal nominal τ (test `long_lived_structure_outlives_transients_at_equal_tau`); prune below 0.02; deterministic (replay test).
- **Merge pass** (update-loop step 5): `merge_overlapping` collapses pairs that are *mutually* inside each other's Mahalanobis gate **and** semantically compatible (cosine ≥ 0.7, or both unlabeled) — orthogonal-semantic overlaps stay separate (test `merge_pass_collapses_mutual_overlaps_but_respects_semantics`). This catches drift the insert-time gate (±1 cell neighborhood only) misses.
- **Queries**: radius (hash + linear reference impl, equivalence-tested on 100-Gaussian grids), kNN (expanding ring), semantic cosine top-k, and the segment-corridor query below.
## 4. Decision — channel gain as a first-class query + inverse update
`gain.rs` implements the RF query surface:
```text
H(tx,rx,f) = (λ/4πd)·e^{-j2πd/λ} · exp(−Σ_g occ_g·I_g)
```
with `I_g` the **closed-form** line integral of each Gaussian's density along the TX→RX segment (1-D Gaussian integral via erf; derivation in the module doc).
**Exactness anchors (MEASURED-CODE):**
- Empty map ⇒ **exact Friis** amplitude (< 1e-15) and propagation phase (`empty_map_returns_exact_friis`).
- Closed-form line integral matches 1 mm trapezoid quadrature through a rotated anisotropic Gaussian to < 1e-6 (`line_integral_matches_numeric_quadrature`).
- On-path absorber attenuates strictly monotonically in occupancy; a 10σ off-path absorber changes LoS gain < 1e-6 dB.
**Inverse update** (`observe_link`) — the incremental-mapping move: measured link amplitude → target optical depth `τ* = ln(friis/measured)`; a projected-gradient step distributes the residual over intersected Gaussians proportional to their path integrals (exact Newton along the link at lr = 1), clamped at occupancy ≥ 0; if nothing intersects and attenuation is demanded, a compact absorber is spawned at the midpoint sized to close the residual. **Measured**: from an empty map, 20 observations of a link with an unseen 0.7-neper (≈6.1 dB) obstruction converge to < 0.06 neper residual and < 0.5 dB prediction error (`inverse_update_learns_a_wall_from_link_residuals`).
## 5. Decision — task-gated scene graph
`graph.rs`: sparse typed nodes (`Object/Room/PersonClass/Device/Event` — person *classes* only; identity lives behind ADR-277's double gate) and relations (`Contains/Near/CausedBy/ObservedBy`). The only sanctioned read is `activate(relevant_kinds, seeds, max_nodes)` — bounded BFS that reports truncation instead of silently scanning (the JITOMA lesson). Tests: an "which object caused the anomaly" activation pulls exactly {event, object, room} and gates out devices/person-classes; the node budget is enforced and truncation is flagged.
## 6. Performance (criterion, release, this machine)
| Benchmark | Result | Note |
|---|---|---|
| `channel_gain`, 1 k Gaussians | **26.9 µs** | was 139 µs with the midpoint-ball candidate query |
| `channel_gain`, 16 k Gaussians | **27.7 µs** | ~O(1) in map size after the corridor rewrite |
| segment corridor query, hash vs linear | 24 µs vs 6 µs (1 k) / 24 µs vs **163 µs** (16 k) | crossover ≈ 4 k Gaussians — reported honestly; both paths kept + equivalence-tested |
| radius query, hash vs linear | 4.3 µs vs 101 µs @ 16 k (23×) | hash loses at 1 k (4.0 vs 1.9 µs) — small maps are brute-force territory |
| `observe_link` inverse update | **74 µs** | was 305 µs pre-optimization |
| map insert+fuse (64 Gaussians, in observe bench setup) | included above | |
The optimization pass replaced a midpoint-ball candidate search (`(2·(L/2+3)+1)³ ≈ 9,300` cell lookups on a 14 m link) with an AABB sweep prefiltered by cell-centre-to-segment distance (bound `margin + √3/2·cell`), after a first corridor attempt (per-sample cube inserts into a BTreeSet) measured *worse* (1.2 ms) and was discarded — kept in this record as the honest negative result.
## 7. Consequences
- The map answers "where is a person likely", "where did multipath change", and "which object caused a channel anomaly" (gain residual → `CausedBy` edge) from one store.
- RuVector integration (ADR-261) becomes: vector search retrieves candidate Gaussians/nodes → graph traversal enforces relations → the gain model *verifies* answers against geometry. The LLM plans the query; it never invents the spatial answer.
- Not yet done (P3): live wiring into `wifi-densepose-sensing-server`, visual/depth Gaussian ingestion, and RuVector index sync.