docs/adr/ADR-275-rf-aware-gaussian-spatial-memory.md
| 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) |
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.
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").
RfGaussian (primitive.rs) carries all six ADR-273 attribute groups:
Σ⁻¹ = R·diag(1/σ²)·Rᵀ.[4 bands × 4 incident-angle bins] (2.4/5/6/60 GHz), plus occupancy = peak extinction coefficient (nepers/m) used by the gain model.{Static, Slow, Fast} class.Provenance {device, model_version, synthetic}.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).
GaussianMap (map.rs): spatial-hash grid (1 m default pitch) over a flat store.
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.τ_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_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.gain.rs implements the RF query surface:
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_returns_exact_friis).line_integral_matches_numeric_quadrature).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).
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.
| 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.
CausedBy edge) from one store.wifi-densepose-sensing-server, visual/depth Gaussian ingestion, and RuVector index sync.