* Add temporal graph evolution & RuVector integration research GOAP Agent 8 output: 1,528-line SOTA research document covering temporal graph models (TGN, JODIE, DyRep), RuVector graph memory design, mincut trajectory tracking with Kalman filtering, event detection pipelines, compressed temporal storage, cross-room transition graphs, and a 5-phase integration roadmap. Part of RF Topological Sensing research swarm (10 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add transformer architectures for graph sensing research GOAP Agent 4 output: 896-line SOTA document covering Graph Transformers (Graphormer, SAN, GPS, TokenGT), Temporal Graph Transformers (TGN, TGAT, DyRep), ViT for RF spectrograms, transformer-based mincut prediction, positional encoding for RF graphs, foundation models for RF sensing, and efficient edge deployment with INT8 quantization. Part of RF Topological Sensing research swarm (10 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add attention mechanisms for RF sensing research GOAP Agent 3 output: 1,110-line document covering GAT for RF graphs, self-attention for CSI sequences, cross-attention multi-link fusion, attention-weighted differentiable mincut, spatial node attention, antenna-level subcarrier attention, and efficient attention variants (linear, sparse, LSH, S4/Mamba). 8 ASCII architecture diagrams. Part of RF Topological Sensing research swarm (10 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add sublinear mincut algorithms research GOAP Agent 5 output: 698-line document covering classical mincut complexity, sublinear approximation (sampling, sparsifiers), dynamic mincut with lazy recomputation hybrid, streaming sketch algorithms, Benczur-Karger sparsification, local partitioning (PageRank-guided cuts), randomized methods reliability analysis, and Rust implementation with const-generic RfGraph, zero-alloc Stoer-Wagner, SIMD batch updates. Part of RF Topological Sensing research swarm (10 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add CSI edge weight computation research GOAP Agent 2 output: ~700-line document covering CSI feature extraction, coherence metrics (cross-correlation, mutual information, phasor coherence), multipath stability scoring (MUSIC, ESPRIT, ISTA), temporal windowing (EMA, Welford, Kalman), noise robustness (phase noise, AGC, clock drift), edge weight normalization, and implementation architecture showing 32KB memory for 120 edges within ESP32-S3 capability. Part of RF Topological Sensing research swarm (10 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add contrastive learning for RF coherence research GOAP Agent 7 output: 1,226-line document covering SimCLR/MoCo/BYOL for CSI, AETHER-Topo dual-head extension, coherence boundary detection with multi-scale analysis, delta-driven updates (2-12x efficiency), self-supervised pre-training protocol, triplet networks for 5-state edge classification, and MERIDIAN cross-environment transfer with EWC continual learning. Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add resolution and spatial granularity analysis research GOAP Agent 9 output: 1,383-line document covering Fresnel zone analysis, node density vs resolution (16-node/5m room → 30-60cm), Cramer-Rao lower bounds with Fisher Information Matrix, graph cut resolution theory, multi-frequency enhancement (6cm coherent dual-band limit), RF tomography comparison, experimental validation protocols, and resolution scaling laws (8.8cm theoretical limit). Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add RF graph theory and minimum cut foundations research GOAP Agent 1 output: Graph-theoretic foundations covering max-flow/min-cut for RF (Ford-Fulkerson, Stoer-Wagner, Karger), RF as dynamic graph with CSI coherence weights, topological change detection via Fiedler vector and Cheeger inequality, dynamic graph algorithms, comparison to classical RF sensing, formal mathematical framework, and 9 open research questions. Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add ESP32 mesh hardware constraints research GOAP Agent 6 output: ESP32 CSI capabilities (52/114 subcarriers), 16-node mesh topology with 120 edges, TDM synchronized sensing (3ms slots), computational budget (Stoer-Wagner uses 0.07% of one core), channel hopping, power analysis (0.44W/node), dual-core firmware architecture, and edge vs server computing with 100x data reduction on-device. Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add system architecture and prototype design research GOAP Agent 10 output: End-to-end architecture with pipeline diagrams, existing crate integration mapping, new rf_topology module design (DDD aggregate roots), 100ms latency budget breakdown, 3-phase prototype plan (4-node POC → 16-node room → 72-node multi-room), benchmark design with 8 metrics, ADR-044 draft, and Rust trait definitions (EdgeWeightComputer, TopologyGraph, MinCutSolver, BoundaryInterpolator). Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add quantum sensing and quantum biomedical research documents Agent 11: Quantum-level sensors (729 lines) — NV centers, SQUIDs, Rydberg atoms, quantum illumination, quantum graph theory (walks, spectral, QAOA), hybrid classical-quantum architecture, quantum ML (VQC, kernels, reservoir computing), NISQ applications (D-Wave, VQE), hardware roadmap. Agent 12: Quantum biomedical sensing (827 lines) — whole body biomagnetic mapping, neural field imaging without electrodes, circulation sensing, cellular EM signaling, non-contact diagnostics, coherence-based diagnostics (disease as coherence breakdown), neural interfaces, multimodal observatory, room-scale ambient health monitoring, graph-based biomedical analysis. Part of RF Topological Sensing research swarm (12 agents). https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add research index synthesizing all 12 documents (14,322 lines) Master index for RF Topological Sensing research compendium covering: graph theory foundations, CSI edge weights, attention mechanisms, transformers, sublinear algorithms, ESP32 hardware, contrastive learning, temporal graphs, resolution analysis, system architecture, quantum sensors, and quantum biomedical sensing. Includes key findings, proposed ADRs (044, 045), and 5-phase implementation roadmap. https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add SOTA neural decoding landscape and 10 application domains research - Doc 21: Comprehensive SOTA map (2023-2026) of brain sensors, decoders, and visualization systems with RuVector/mincut positioning analysis - Doc 22: Ten application domains for brain state observatory including disease detection, BCI, cognitive monitoring, mental health diagnostics, neurofeedback, dream reconstruction, cognitive research, HCI, wearables, and brain network digital twins with strategic roadmap https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add NV diamond neural magnetometry research document (13/22) Comprehensive 600+ line document covering NV center physics, neural magnetic field sources, sensor architecture, SQUID comparison, signal processing pipeline, RuVector integration, and development roadmap. https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add ruv-neural workspace Cargo.toml with 12 crate definitions Workspace structure for the rUv Neural brain topology analysis system. 12 mix-and-match crates with shared dependencies including RuVector integration, petgraph, rustfft, and WASM/ESP32 support. https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add ruv-neural crate ecosystem — 12 mix-and-match crates (WIP) Initial implementation of the rUv Neural brain topology analysis system: - ruv-neural-core: Core types, traits, errors, RVF format (compiles) - ruv-neural-sensor: NV diamond, OPM, EEG sensor interfaces (in progress) - ruv-neural-signal: DSP, filtering, spectral, connectivity (in progress) - ruv-neural-graph: Brain connectivity graph construction (in progress) - ruv-neural-mincut: Dynamic minimum cut topology analysis (in progress) - ruv-neural-embed: RuVector graph embeddings (in progress) - ruv-neural-memory: Persistent neural state memory + HNSW (compiles) - ruv-neural-decoder: Cognitive state classification + BCI (in progress) - ruv-neural-esp32: ESP32 edge sensor integration (compiles) - ruv-neural-wasm: WebAssembly browser bindings (in progress) - ruv-neural-viz: Visualization + ASCII rendering (in progress) - ruv-neural-cli: CLI tool (in progress) Agents still writing remaining modules. Next: fix compilation, tests, push. https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Fix ruv-neural crate compilation: all 12 crates build and 1200+ tests pass - Fix node2vec.rs type inference error (Vec<_> → Vec<Vec<f64>>) - Fix artifact.rs with full filter-based detection implementations - Fix signal crate ConnectivityMetric re-export and trait method names - Fix embed crate EmbeddingGenerator trait implementations - Complete spectral, topology, and node2vec embedders with tests - Complete preprocessing pipeline with sequential stage processing - All workspace crates compile cleanly, 0 test failures https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * Add ruv-neural-cli README https://claude.ai/code/session_01DGUAowNScGVp88bK2eiuRv * fix: convert desktop icons from RGB to RGBA for Tauri build Tauri's generate_context!() macro requires RGBA PNG icons. All 5 icon files (32x32.png, 128x128.png, 128x128@2x.png, icon.icns, icon.ico) were RGB-only, causing a proc macro panic on Linux builds. Fixes #200 Co-Authored-By: claude-flow <ruv@ruv.net> * Add Subcarrier Manifold and Vitals Oracle modules for 3D visualizations - Implemented Subcarrier Manifold to visualize amplitude data as a 3D surface with height and age attributes. - Created Vitals Oracle to represent vital signs using toroidal rings and particle trails, incorporating breathing and heart rate dynamics. - Both modules utilize Three.js for rendering and include custom shaders for visual effects. * feat: complete ruv-neural implementation — physics models, security, witness verification Replace all stubs/mocks with production physics-based signal models: - NV Diamond: ODMR Lorentzian dip, 1/f pink noise (Voss-McCartney), brain oscillations - OPM: SERF-mode, 50/60Hz powerline harmonics, full cross-talk compensation via Gaussian elimination with partial pivoting - EEG: 5 frequency bands, eye blink artifacts (Fp1/Fp2), muscle artifacts, impedance-based thermal noise floor - ESP32 ADC: ring-buffer reader with calibration signal generator, i16 clamp Security hardening (SEC-001 through SEC-005): - RVF bounded allocation (16MB metadata, 256MB payload) - sample_rate validation (>0, finite) - Signal NaN/Inf rejection - ADC resolution_bits overflow clamp - HNSW HashSet visited tracking + bounds checks Performance optimizations (PERF-001 through PERF-005): - 67x fewer FFTs via pre-computed analytic signals - VecDeque O(1) eviction in memory store - Thread-local FFT planner caching - BrainGraph::validate() for edge/weight integrity - Eigenvalue convergence early termination Ed25519 witness verification system: - 41 capability attestations across all 12 crates - SHA-256 digest + Ed25519 signature - CLI commands: `witness --output` and `witness --verify` README: ethics warning, hardware parts list (AliExpress), assembly instructions Co-Authored-By: claude-flow <ruv@ruv.net> * docs: add crates.io badges and install instructions to ruv-neural README Add version badges linking to each published crate on crates.io, cargo add instructions, and crate search link in the Crate Map table. Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com>
59 KiB
Research Document 10: RF Topological Sensing — System Architecture and Prototype
Date: 2026-03-08 Status: Draft Author: Research Agent Scope: End-to-end architecture for RF topological sensing using ESP32 mesh networks
Table of Contents
- End-to-End Architecture
- Existing Crate Integration
- New Module Design
- Real-Time Pipeline
- Prototype Phases
- Benchmark
- ADR-044 Draft
- Rust Trait Definitions
1. End-to-End Architecture
1.1 Core Concept
RF topological sensing treats a mesh of ESP32 nodes as a "radio nervous system." Every transmitter-receiver pair defines a graph edge. The Channel State Information (CSI) measured on each edge encodes how the radio environment between those two nodes has been perturbed — by walls, furniture, and most importantly, by human bodies. When a person stands between two nodes, the CSI coherence on that link drops. The collection of all such drops defines a cut in the graph that traces the physical boundary of the person.
The system does not estimate pose directly. Instead it answers a more fundamental question: where are the boundaries between occupied and unoccupied space? Pose estimation, activity recognition, and room segmentation are all downstream consumers of this boundary information.
1.2 Data Flow Summary
ESP32 Node A ──CSI──> Edge (A,B) ──weight──> Graph G ──mincut──> Boundaries ──render──> UI
ESP32 Node B ──CSI──> Edge (B,C) ──weight──> | | |
ESP32 Node C ──CSI──> Edge (A,C) ──weight──> | | |
... ... v v v
ESP32 Node N Edge (i,j) RfGraph CutBoundary WebSocket
1.3 Pipeline Diagram
+============================================================================+
| RF TOPOLOGICAL SENSING PIPELINE |
+============================================================================+
STAGE 1: CSI EXTRACTION STAGE 2: EDGE WEIGHT
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
+-------------+ +-------------+ +-----------------+
| ESP32 Node | | ESP32 Node | | Edge Weight |
| (TX) |--->| (RX) |--[ raw CSI ]->| Computation |
| ch_hop() | | extract() | | |
+-------------+ +-------------+ | - phase_align() |
| | | - coherence() |
| TDM slot | 52-subcarrier | - amplitude() |
| assignment | CSI frame | - temporal_avg |
v v +---------+-------+
+-------------+ +-------------+ |
| TDM | | CSI Frame | weight: f64
| Scheduler | | Buffer | [0.0 .. 1.0]
| (hardware) | | (ring buf) | |
+-------------+ +-------------+ v
STAGE 3: GRAPH CONSTRUCTION STAGE 4: DYNAMIC MINCUT
~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
+-----------------+ +------------------+
| RfGraph | | Mincut Solver |
| |<----[ edge weights ]---------| |
| - add_edge() | | - stoer_wagner() |
| - update_wt() | | or |
| - prune_stale() | | - karger() |
| - adjacency mat |----[ graph snapshot ]------->| - push_relabel() |
| | | |
+-----------------+ +--------+---------+
|
CutBoundary {
cut_edges,
cut_value,
partitions
}
|
v
STAGE 5: BOUNDARY VISUALIZATION
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+------------------+ +-------------------+ +----------------+
| Boundary | | Sensing Server | | Browser UI |
| Interpolation |------>| (Axum WebSocket) |------>| (Canvas/WebGL) |
| | | | | |
| - contour_from() | | - ws_broadcast() | | - draw_room() |
| - smooth() | | - /api/topology | | - draw_cuts() |
| - to_polygon() | | - /api/stream | | - animate() |
+------------------+ +-------------------+ +----------------+
1.4 Data Structures at Each Stage
Stage 1 Output: CsiFrame { tx_id, rx_id, subcarriers: [Complex<f32>; 52], timestamp_us }
Stage 2 Output: EdgeWeight { tx_id, rx_id, weight: f64, confidence: f64, updated_at }
Stage 3 Output: RfGraph { nodes: Vec<NodeId>, edges: HashMap<(NodeId,NodeId), EdgeWeight> }
Stage 4 Output: CutBoundary { cut_edges: Vec<(NodeId,NodeId)>, partitions: (Vec<NodeId>, Vec<NodeId>) }
Stage 5 Output: BoundaryPolygon { vertices: Vec<(f64,f64)>, confidence: f64 }
1.5 Communication Protocol
Nodes communicate using TDM (Time Division Multiplexing) as defined in ADR-028. Each node is assigned a transmit slot. During its slot, a node transmits on a known subcarrier pattern. All other nodes simultaneously receive and extract CSI. This yields N*(N-1)/2 unique edges for N nodes.
Time -->
Slot 0 Slot 1 Slot 2 Slot 3 Slot 0 Slot 1 ...
[Node A] [Node B] [Node C] [Node D] [Node A] [Node B]
TX TX TX TX TX TX
B,C,D RX A,C,D RX A,B,D RX A,B,C RX B,C,D RX A,C,D RX
One full cycle = N slots = one complete graph snapshot
At 1ms slots, 4-node cycle = 4ms, 16-node cycle = 16ms
2. Existing Crate Integration
2.1 Integration Map
+---------------------------+ +-----------------------------+
| wifi-densepose-hardware | | wifi-densepose-signal |
| (ESP32 TDM, CSI extract) | | (ruvsense modules) |
+------------+--------------+ +-------------+---------------+
| |
| CsiFrame | coherence, phase
v v
+------------------------------------------------------------------+
| rf_topology (NEW MODULE) |
| RfGraph, EdgeWeight, CutBoundary, TopologyEvent |
+------------------------------------------------------------------+
| |
| graph memory | boundary data
v v
+-----------------------------+ +-----------------------------+
| wifi-densepose-ruvector | | wifi-densepose-sensing- |
| (graph memory, attention) | | server (UI, WebSocket) |
+-----------------------------+ +-----------------------------+
2.2 wifi-densepose-signal / ruvsense
The signal crate contains the RuvSense modules that provide the mathematical foundation for edge weight computation.
coherence.rs — Z-score coherence scoring with DriftProfile. This module already computes a coherence metric between CSI frames. For RF topology, we use coherence as the primary edge weight: high coherence means the link is unobstructed, low coherence means something (a person) is in the path.
Usage in rf_topology:
- coherence::ZScoreCoherence::score(baseline_csi, current_csi) -> f64
- coherence::DriftProfile tracks long-term drift per edge
- coherence_gate::CoherenceGate decides if a measurement is reliable
phase_align.rs — Iterative LO phase offset estimation using circular mean. ESP32 local oscillators drift, which corrupts phase measurements. Phase alignment is a prerequisite for meaningful coherence computation.
Usage in rf_topology:
- phase_align::align_frames(tx_csi, rx_csi) -> AlignedCsiPair
- Must be called BEFORE coherence scoring
- Runs per-edge, per-frame
multiband.rs — Multi-band CSI frame fusion. When nodes operate on multiple WiFi channels (via channel hopping), this module fuses the measurements into a single coherent view.
Usage in rf_topology:
- multiband::fuse_channels(ch1_csi, ch5_csi, ch11_csi) -> FusedCsiFrame
- Increases spatial resolution of edge weights
- Optional: single-channel operation is sufficient for prototype
multistatic.rs — Attention-weighted fusion with geometric diversity. This module already performs multi-link fusion, which is conceptually close to what rf_topology needs. The key difference is that multistatic.rs fuses for pose estimation, while rf_topology fuses for boundary detection.
Usage in rf_topology:
- multistatic::GeometricDiversity provides link quality weighting
- Reuse attention weights for graph edge confidence scoring
adversarial.rs — Physically impossible signal detection. This module detects when CSI measurements violate physical constraints (e.g., signal strength increases when a person is blocking the path). Essential for filtering bad edges in the graph.
Usage in rf_topology:
- adversarial::PhysicsChecker::validate(edge_measurement) -> Result<(), Violation>
- Edges that fail validation are marked low-confidence
2.3 wifi-densepose-ruvector
The ruvector crate provides graph-based data structures and attention mechanisms that can be repurposed for RF topology.
viewpoint/attention.rs — CrossViewpointAttention with GeometricBias and softmax. The attention mechanism computes importance weights across multiple viewpoints. In RF topology, each TX-RX pair is a "viewpoint" and the attention mechanism can prioritize the most informative edges.
Usage in rf_topology:
- CrossViewpointAttention can weight edges by geometric diversity
- GeometricBias accounts for node placement geometry
- Softmax normalization produces valid probability distribution over edges
viewpoint/geometry.rs — GeometricDiversityIndex and Cramer-Rao bounds. This module quantifies how much geometric information a set of links provides. RF topology uses this to determine if the current node placement can resolve a boundary at a given location.
Usage in rf_topology:
- GeometricDiversityIndex tells us if we have enough angular coverage
- Cramer-Rao bound gives theoretical position error lower bound
- Fisher Information matrix guides optimal node placement
viewpoint/coherence.rs — Phase phasor coherence with hysteresis gate. Already provides a gating mechanism for coherence measurements. RF topology reuses this to prevent boundary flicker from noisy measurements.
Usage in rf_topology:
- Hysteresis gate prevents rapid edge weight oscillation
- Smooths boundary detection over time
viewpoint/fusion.rs — MultistaticArray aggregate root with domain events. This is a DDD aggregate root that manages a collection of multistatic links. RF topology can extend this pattern for graph-level aggregate management.
Usage in rf_topology:
- MultistaticArray pattern informs RfGraph aggregate design
- Domain events (LinkAdded, LinkDropped) map to TopologyEvent
2.4 wifi-densepose-hardware
The hardware crate manages ESP32 devices and the TDM protocol.
esp32/tdm.rs — Time Division Multiplexing scheduler. Assigns transmit slots to nodes, ensures collision-free CSI extraction.
Usage in rf_topology:
- TdmScheduler provides the frame timing that drives the pipeline
- Each TDM cycle produces one complete graph snapshot
- Cycle period = N_nodes * slot_duration
esp32/channel_hop.rs — Channel hopping firmware control. Allows nodes to measure CSI on multiple WiFi channels for improved spatial resolution.
Usage in rf_topology:
- Channel diversity increases edge weight accuracy
- Feeds into multiband.rs fusion
esp32/csi_extract.rs — Raw CSI extraction from ESP32 hardware registers. Produces CsiFrame structs that are the input to the entire pipeline.
Usage in rf_topology:
- CsiFrame is the fundamental input type
- 52 subcarriers per frame on 20MHz channels
- Timestamp synchronization via NTP or TDM slot timing
2.5 wifi-densepose-sensing-server
The sensing server provides the web UI and WebSocket streaming.
Usage in rf_topology:
- WebSocket endpoint broadcasts CutBoundary updates to browser
- REST endpoint /api/topology returns current graph state
- Static file serving for visualization JavaScript
- Axum router integrates new topology endpoints
2.6 Integration Summary Table
| Existing Module | What It Provides | How rf_topology Uses It |
|---|---|---|
| signal/ruvsense/coherence | Z-score coherence scoring | Primary edge weight metric |
| signal/ruvsense/phase_align | LO phase offset correction | Pre-processing for coherence |
| signal/ruvsense/multiband | Multi-channel fusion | Improved edge resolution |
| signal/ruvsense/multistatic | Geometric diversity weighting | Edge confidence scoring |
| signal/ruvsense/adversarial | Physics violation detection | Bad edge filtering |
| signal/ruvsense/coherence_gate | Hysteresis gating | Boundary flicker prevention |
| ruvector/viewpoint/attention | Cross-viewpoint attention | Edge importance weighting |
| ruvector/viewpoint/geometry | Geometric diversity index | Resolution analysis |
| ruvector/viewpoint/fusion | DDD aggregate root pattern | RfGraph aggregate design |
| hardware/esp32/tdm | TDM slot scheduling | Frame timing, cycle control |
| hardware/esp32/csi_extract | Raw CSI extraction | Pipeline input |
| sensing-server | Axum WebSocket + REST | Visualization delivery |
3. New Module Design
3.1 Module Location
rust-port/wifi-densepose-rs/crates/wifi-densepose-signal/src/ruvsense/
rf_topology.rs <-- New module (primary)
rf_topology/
graph.rs <-- RfGraph aggregate root
edge_weight.rs <-- EdgeWeight computation
mincut.rs <-- Dynamic mincut solver
boundary.rs <-- CutBoundary -> spatial polygon
events.rs <-- TopologyEvent domain events
mod.rs <-- Module re-exports
Alternatively, rf_topology could be a standalone crate:
rust-port/wifi-densepose-rs/crates/wifi-densepose-topology/
src/
lib.rs
graph.rs
edge_weight.rs
mincut.rs
boundary.rs
events.rs
Cargo.toml
The standalone crate approach is preferred because RF topology has distinct bounded-context semantics and its own aggregate root (RfGraph). It depends on wifi-densepose-signal for coherence computation and wifi-densepose-core for shared types.
3.2 Key Types
RfGraph — Aggregate Root
RfGraph is the central aggregate root. It owns the complete graph state: nodes, edges, weights, and metadata. All mutations go through RfGraph methods, which emit TopologyEvents for downstream consumers.
RfGraph {
id: GraphId,
nodes: HashMap<NodeId, NodeInfo>,
edges: HashMap<EdgeId, EdgeState>,
adjacency: AdjacencyMatrix,
epoch: u64, // incremented on each full TDM cycle
last_updated: Instant,
config: TopologyConfig,
}
Invariants enforced by RfGraph:
- No self-loops (tx_id != rx_id)
- Edge weights are in [0.0, 1.0]
- Stale edges (no update in N cycles) are pruned
- Graph is always connected (disconnected subgraphs trigger alert)
EdgeWeight — Value Object
EdgeWeight {
tx_id: NodeId,
rx_id: NodeId,
weight: f64, // 0.0 = fully obstructed, 1.0 = clear
raw_coherence: f64, // pre-normalization coherence
confidence: f64, // measurement quality [0.0, 1.0]
sample_count: u32, // number of CSI frames averaged
baseline_deviation: f64, // how far from calibrated baseline
updated_at: Instant,
}
EdgeWeight is a value object: immutable after creation. Each TDM cycle produces a new EdgeWeight for each edge, which replaces the previous one in RfGraph.
CutBoundary — Value Object
CutBoundary {
cut_edges: Vec<EdgeId>, // edges that cross the boundary
cut_value: f64, // total weight of cut edges
partition_a: Vec<NodeId>, // nodes on one side
partition_b: Vec<NodeId>, // nodes on the other side
spatial_boundary: Option<Polygon>, // interpolated physical boundary
confidence: f64, // based on edge confidences
detected_at: Instant,
}
CutBoundary represents the output of the mincut solver. Multiple CutBoundaries can exist simultaneously when multiple people are detected.
TopologyEvent — Domain Event
TopologyEvent {
id: EventId,
timestamp: Instant,
kind: TopologyEventKind,
}
enum TopologyEventKind {
NodeJoined { node_id: NodeId, position: (f64, f64) },
NodeLeft { node_id: NodeId, reason: LeaveReason },
EdgeWeightChanged { edge_id: EdgeId, old: f64, new: f64 },
BoundaryDetected { boundary: CutBoundary },
BoundaryMoved { boundary_id: BoundaryId, displacement: (f64, f64) },
BoundaryLost { boundary_id: BoundaryId },
GraphPartitioned { components: Vec<Vec<NodeId>> },
CalibrationRequired { reason: String },
}
Events are published to an event bus. The sensing server subscribes and forwards relevant events to the browser UI via WebSocket.
3.3 DDD Aggregate Root Design
+-------------------------------------------------------------------+
| RfGraph (Aggregate Root) |
| |
| +------------------+ +-----------------+ +---------------+ |
| | NodeRegistry | | EdgeRegistry | | CutSolver | |
| | | | | | | |
| | - register() | | - update_wt() | | - solve() | |
| | - deregister() | | - prune_stale() | | - track() | |
| | - get_position() | | - get_weight() | | - boundaries | |
| +------------------+ +-----------------+ +---------------+ |
| |
| Command Interface: |
| fn ingest_csi_frame(&mut self, frame: CsiFrame) -> Vec<Event> |
| fn tick(&mut self) -> Vec<Event> |
| fn calibrate(&mut self, baseline: &Baseline) -> Vec<Event> |
| fn add_node(&mut self, node: NodeInfo) -> Vec<Event> |
| fn remove_node(&mut self, node_id: NodeId) -> Vec<Event> |
| |
| Query Interface: |
| fn current_boundaries(&self) -> &[CutBoundary] |
| fn edge_weight(&self, a: NodeId, b: NodeId) -> Option<f64> |
| fn graph_snapshot(&self) -> GraphSnapshot |
| fn node_count(&self) -> usize |
| fn is_connected(&self) -> bool |
+-------------------------------------------------------------------+
|
| emits
v
Vec<TopologyEvent>
|
v
+---------------------+
| Event Bus |
| (tokio broadcast) |
+---------------------+
| |
v v
Sensing Server Pose Tracker
(WebSocket) (ruvsense)
3.4 Module Responsibilities
| File | Responsibility | LOC Estimate |
|---|---|---|
| graph.rs | RfGraph aggregate, node/edge registry | ~200 |
| edge_weight.rs | Weight computation from CSI coherence | ~120 |
| mincut.rs | Stoer-Wagner and incremental mincut | ~180 |
| boundary.rs | Cut-to-polygon interpolation | ~150 |
| events.rs | TopologyEvent types and bus | ~80 |
| mod.rs | Public API re-exports | ~30 |
| Total | ~760 |
All files stay under the 500-line limit by splitting graph.rs if needed.
4. Real-Time Pipeline
4.1 Latency Budget
The system must produce updated boundary estimates within 100ms of a CSI frame arrival. This enables responsive real-time visualization and is sufficient for human-speed movement tracking.
+============================================================================+
| LATENCY BUDGET: 100ms TOTAL |
+============================================================================+
Stage Budget Actual Target Notes
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~ ~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~
1. CSI Extraction 5 ms 3-5 ms ESP32 hardware, fixed
2. Phase Alignment 3 ms 1-2 ms Per-edge, parallelizable
3. Edge Weight Comp 10 ms 5-8 ms Coherence + normalization
4. Graph Update 2 ms 0.5-1 ms HashMap insert/update
5. Mincut Solver 5 ms 2-5 ms Stoer-Wagner on N<64
6. Boundary Interp 5 ms 2-3 ms Polygon from cut edges
7. Serialization 2 ms 0.5-1 ms serde_json or bincode
8. WebSocket TX 3 ms 1-2 ms Local network
9. Browser Render 20 ms 10-16 ms Canvas 2D at 60fps
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~ ~~~~~~~~~~~~~~
TOTAL 55 ms 26-43 ms ~50ms headroom
Margin for safety: 45 ms Absorbs GC, jitter, WiFi
4.2 Stage Details
Stage 1: CSI Extraction (5ms budget)
The ESP32 extracts CSI from each received packet. This happens in firmware and is bounded by the WiFi hardware. The output is a 52-element complex vector plus metadata (RSSI, noise floor, timestamp).
Input: WiFi packet on air
Output: CsiFrame { subcarriers: [Complex<f32>; 52], rssi: i8, ... }
Cost: Fixed by hardware. ~3ms on ESP32-S3, ~5ms on ESP32.
Stage 2: Phase Alignment (3ms budget)
Phase alignment corrects for local oscillator drift between TX and RX nodes. Uses the circular mean algorithm from ruvsense/phase_align.rs. This runs once per edge per frame.
Input: CsiFrame pair (TX reference, RX measurement)
Output: AlignedCsiPair with corrected phase
Cost: ~50us per edge. For 16 nodes (120 edges): 6ms sequential, <1ms parallel
Note: Embarrassingly parallel across edges. Use rayon par_iter.
Stage 3: Edge Weight Computation (10ms budget)
Compute coherence between current CSI and baseline CSI. Apply temporal averaging (exponential moving average over last K frames). Normalize to [0.0, 1.0] range. Apply adversarial physics check.
Input: AlignedCsiPair + baseline reference
Output: EdgeWeight { weight, confidence, ... }
Cost: ~80us per edge. For 120 edges: 9.6ms sequential, <2ms parallel
Pipeline:
1. coherence::ZScoreCoherence::score() ~30us
2. temporal_average() ~10us
3. adversarial::PhysicsChecker::validate() ~20us
4. normalize_and_gate() ~20us
Stage 4: Graph Update (2ms budget)
Insert new edge weights into RfGraph. Prune stale edges. Check connectivity. This is a simple HashMap operation.
Input: Vec<EdgeWeight> from current TDM cycle
Output: Updated RfGraph, list of changed edges
Cost: O(E) where E = number of edges. <1ms for E < 500.
Stage 5: Mincut Solver (5ms budget)
Run Stoer-Wagner minimum cut on the weighted graph. For small graphs (N < 64), Stoer-Wagner runs in O(V * E + V^2 * log V) which is well within budget.
Input: RfGraph adjacency matrix with weights
Output: CutBoundary (minimum cut edges + partitions)
Cost: 4-node: ~0.1ms
16-node: ~2ms
64-node: ~15ms (exceeds budget -- use incremental solver)
For graphs larger than ~40 nodes, use incremental mincut: only recompute the cut in the neighborhood of changed edges. This keeps the cost under 5ms regardless of total graph size.
Stage 6: Boundary Interpolation (5ms budget)
Convert the cut edges into a spatial polygon by interpolating between the known positions of the nodes on either side of the cut.
Input: CutBoundary + node positions
Output: BoundaryPolygon { vertices: Vec<(f64, f64)> }
Cost: Convex hull + smoothing. <3ms for typical boundaries.
Stage 7-9: Serialization, Transport, Render (25ms budget)
Serialize boundary polygon to JSON, send over WebSocket, render in browser.
Serialization: serde_json::to_string(&boundary) -- <1ms
WebSocket TX: axum tungstenite broadcast -- <2ms local
Browser render: Canvas 2D path drawing -- 10-16ms at 60fps
4.3 Timing Diagram
Time (ms) 0 5 10 15 20 25 30 35 40 45 50
| | | | | | | | | | |
[CSI ]
[Phs][ Edge Weight ]
[GU][Cut ]
[Bnd][Ser][WS]
[Render....]
|<-- ESP32 firmware --|<------ Rust pipeline -------->|<-- Browser ->|
| 5ms | ~25ms | ~16ms |
|<---------------------- Total: ~46ms ------------------------------>|
4.4 Parallelism Strategy
+-- rayon thread pool (4 threads on server, 1 on ESP32) --+
| |
| Edge 0: [phase_align] -> [coherence] -> [weight] |
| Edge 1: [phase_align] -> [coherence] -> [weight] |
| Edge 2: [phase_align] -> [coherence] -> [weight] |
| ... |
| Edge N: [phase_align] -> [coherence] -> [weight] |
| |
+-- barrier: all edges complete --------+ |
| |
[graph_update] (single thread) |
[mincut_solve] (single thread) |
[boundary_interp] (single thread) |
[serialize + broadcast] |
+----------------------------------------------------------+
Edge weight computation is embarrassingly parallel and dominates the pipeline cost. Using rayon reduces this from O(E * cost_per_edge) to O(E * cost_per_edge / num_threads).
5. Prototype Phases
5.1 Phase 1: 4-Node Proof of Concept
Goal: Detect a single person entering a square region bounded by 4 ESP32 nodes.
Node A ─────────── Node B
| \ / |
| \ / |
| \ / |
| [X] | X = person standing here
| / \ |
| / \ |
| / \ |
Node D ─────────── Node C
Edges: A-B, A-C, A-D, B-C, B-D, C-D (6 total)
Room size: 3m x 3m
Setup:
- 4x ESP32-S3 DevKitC boards
- Nodes at corners of a 3m x 3m room
- Single WiFi channel (channel 6, 2.437 GHz)
- TDM with 1ms slots = 4ms cycle = 250 Hz update rate
Success Criteria:
- Detect person presence within 500ms of entering the room
- Correctly identify which quadrant the person is in
- No false positives when room is empty (over 10-minute test)
- Mincut correctly separates the person from at least one node
Deliverables:
- Working TDM firmware on 4 ESP32 boards
- Rust pipeline processing CSI in real-time
- Web UI showing graph with highlighted cut edges
- Calibration procedure documented
Timeline: 4 weeks
Week 1: TDM firmware bring-up, CSI extraction verified
Week 2: Edge weight pipeline, baseline calibration
Week 3: Mincut integration, boundary detection logic
Week 4: Web UI, end-to-end test, benchmark
5.2 Phase 2: 16-Node Room Scale
Goal: Track the spatial boundaries of 1-3 people moving through a room.
A ── B ── C ── D
| \ | /\ | /\ |
E ── F ── G ── H
| / | \/ | \/ |
I ── J ── K ── L
| \ | /\ | /\ |
M ── N ── O ── P
16 nodes, 4x4 grid, 1.5m spacing
Edges: up to 120 (each node connects to all others within range)
Room size: 4.5m x 4.5m
New Capabilities:
- Multi-person detection via multi-way mincut (k-cut)
- Boundary tracking across frames (temporal association)
- Adaptive baseline recalibration (furniture changes)
- Channel hopping for improved resolution
Success Criteria:
- Track 1-3 people simultaneously
- Boundary position error < 50cm (compared to ground truth)
- Update rate >= 30 Hz (33ms per cycle)
- Handle person entry/exit without false boundaries
- Recover from node failure (1 of 16 goes offline)
Deliverables:
- Scalable TDM scheduler for 16 nodes
- Multi-cut solver with temporal tracking
- Boundary tracking with ID assignment
- Performance dashboard showing latency breakdown
- Comparison against camera ground truth
Timeline: 8 weeks
Week 1-2: Scale TDM to 16 nodes, test reliability
Week 3-4: Multi-cut solver, k-way partitioning
Week 5-6: Temporal tracking, boundary ID persistence
Week 7: Channel hopping, multi-band fusion
Week 8: Benchmark suite, ground truth comparison
5.3 Phase 3: Multi-Room Mesh
Goal: Extend to multi-room deployment with hierarchical graph structure.
+------------------+ +------------------+
| Room A (16 nodes)| | Room B (16 nodes)|
| | | |
| Local RfGraph | | Local RfGraph |
| | | |
+--------+---------+ +--------+---------+
| |
| gateway edges | gateway edges
| |
+--------+-------------------------+--------+
| Hallway (8 nodes) |
| Corridor RfGraph |
+--------+-------------------------+--------+
| |
+--------+---------+ +--------+---------+
| Room C (16 nodes)| | Room D (16 nodes)|
| | | |
+------------------+ +------------------+
Total: 72 nodes across 5 zones
Hierarchical mincut: local cuts + cross-zone cuts
New Capabilities:
- Hierarchical graph: room-level graphs with inter-room gateway edges
- Cross-room person tracking (handoff between local graphs)
- Distributed processing: each room runs its own mincut, global coordinator merges boundaries
- Environment fingerprinting (reuse ruvsense/cross_room.rs)
- Fault tolerance: room operates independently if gateway fails
Success Criteria:
- Track people across room transitions
- Latency < 100ms even with 72 nodes (via hierarchical decomposition)
- Handle node failures gracefully (degrade, don't crash)
- Boundary accuracy < 50cm within rooms, < 1m across transitions
Timeline: 16 weeks
5.4 Phase Summary
Phase Nodes Edges People Accuracy Update Rate Duration
~~~~~~ ~~~~~~ ~~~~~~ ~~~~~~~ ~~~~~~~~~ ~~~~~~~~~~~ ~~~~~~~~
1 4 6 1 Quadrant 250 Hz 4 weeks
2 16 120 1-3 < 50cm 30 Hz 8 weeks
3 72 ~500 5-10 < 50cm 30 Hz 16 weeks
6. Benchmark
6.1 Primary Benchmark: Person Moving Through Room
Scenario: A single person walks a known path through the 16-node room (Phase 2 setup). Ground truth is captured by an overhead camera with ArUco markers on the person's shoulders.
A ── B ── C ── D
| | | |
E ── F ── G ── H
| | | | Person path: start at (+), walk to (*),
I ── J ── K ── L then to (#), then exit
| | | |
M ── N ── O ── P
Path: (+) near F
|
v
(*) near K
|
v
(#) near O
|
v
exit past P
6.2 Setup
Hardware:
- 16x ESP32-S3 DevKitC, mounted at 1.2m height on stands
- Grid spacing: 1.5m
- Room dimensions: 4.5m x 4.5m, cleared of furniture for baseline
- 1x overhead USB camera, 30fps, for ground truth
- 4x ArUco markers on person (shoulders, hips)
Software:
- TDM cycle: 16ms (16 nodes x 1ms slots)
- Update rate: 62.5 Hz
- Mincut solver: Stoer-Wagner
- Edge weight: exponential moving average, alpha = 0.3
- Baseline: 60 seconds of empty room calibration
Environment:
- Standard office room, concrete walls
- WiFi channel 6 (2.437 GHz), no other AP on same channel
- Temperature: 20-25C (stable)
- Test duration: 5 minutes per run, 10 runs total
6.3 Metrics
| Metric | Definition | Target |
|---|---|---|
| Boundary Position Error | Distance from detected boundary centroid to GT position | < 50cm |
| Detection Latency | Time from person entering room to first boundary detect | < 500ms |
| Tracking Continuity | % of frames where boundary is detected while person present | > 95% |
| False Positive Rate | Boundaries detected per minute when room is empty | < 0.1/min |
| Pipeline Latency (P95) | 95th percentile CSI-to-boundary time | < 100ms |
| Pipeline Latency (P50) | Median CSI-to-boundary time | < 50ms |
| Update Throughput | Boundary updates delivered to UI per second | > 30/s |
| Node Failure Recovery | Time to stable operation after 1 node goes offline | < 5s |
6.4 Success Criteria
The benchmark PASSES if ALL of the following hold over 10 runs:
- Mean boundary position error < 50cm
- 95th percentile boundary position error < 75cm
- Detection latency < 500ms in 9/10 runs
- Tracking continuity > 95% in 9/10 runs
- Zero false positives in empty room (10-minute test)
- Pipeline latency P95 < 100ms in all runs
- No crashes or hangs during any run
6.5 Data Collection
Output files per run:
benchmark_run_{N}/
csi_raw/ # Raw CSI frames, timestamped
edge_weights/ # Computed weights per edge per frame
boundaries/ # Detected boundaries with timestamps
ground_truth/ # Camera-derived positions with timestamps
latency_log.csv # Per-frame pipeline timing breakdown
summary.json # Aggregate metrics for this run
6.6 Analysis
Post-benchmark analysis computes:
- Error distribution: Histogram of boundary position errors
- Error vs. position: Heat map of error across the room (corner vs. center)
- Latency breakdown: Stacked bar chart of pipeline stages
- Temporal stability: Boundary position over time vs. ground truth
- Edge weight visualization: Animation of edge weights during walk
Expected failure modes:
- Higher error near room edges (fewer surrounding nodes)
- Brief detection gaps during fast movement
- Increased error when person is exactly between two nodes (ambiguous cut)
7. ADR-044 Draft
ADR-044: RF Topological Sensing
Status: Proposed
Date: 2026-03-08
Context
The wifi-densepose system currently estimates human pose by processing CSI data through neural network models (wifi-densepose-nn). This approach requires training data, GPU inference, and per-environment calibration of the neural model. The RuvSense multistatic sensing mode (ADR-029) improved robustness through multi-link fusion but still treats each link independently before fusion.
A fundamentally different approach is possible: treat the entire ESP32 mesh as a graph where TX-RX pairs are edges and CSI coherence determines edge weights. A minimum cut of this graph reveals physical boundaries — the locations where radio propagation is disrupted by human bodies. This is "RF topological sensing."
Key motivations:
- No training data required: The mincut is a pure graph algorithm, not a learned model. It works out of the box after baseline calibration.
- Physics-grounded: The approach directly exploits the physical fact that human bodies attenuate and scatter radio waves.
- Graceful degradation: If nodes fail, the graph simply has fewer edges. The mincut still works, with reduced resolution.
- Complementary to neural approach: Topological boundaries can provide spatial priors to the neural pose estimator, improving accuracy.
Decision
We will implement RF topological sensing as a new module in the workspace. The module will:
-
Define an RfGraph aggregate root that maintains a weighted graph of all TX-RX links in the mesh.
-
Compute edge weights from CSI coherence using existing ruvsense modules (coherence.rs, phase_align.rs).
-
Run dynamic minimum cut to detect physical boundaries in real time.
-
Expose boundaries via the sensing server WebSocket for visualization.
-
Publish TopologyEvents that downstream modules (pose_tracker, intention) can consume for spatial priors.
The implementation will proceed in three phases:
- Phase 1: 4-node proof of concept (detect person presence)
- Phase 2: 16-node room scale (track boundaries with < 50cm error)
- Phase 3: Multi-room mesh with hierarchical graph decomposition
Consequences
Positive:
- Enables WiFi sensing without neural network inference or training data
- Provides spatial boundary information that is complementary to pose estimation
- Reuses existing ruvsense modules for coherence and phase alignment
- Follows DDD patterns established in ruvector/viewpoint/fusion.rs
- Gracefully degrades under node failure
- Sub-100ms latency enables real-time applications
Negative:
- Requires minimum 4 ESP32 nodes (higher hardware cost than single-link)
- Mincut provides boundaries, not poses — pose still requires neural inference or additional geometric reasoning
- Stoer-Wagner complexity O(V*E + V^2 log V) limits scalability beyond ~40 nodes without incremental solver
- Additional firmware complexity for TDM synchronization across many nodes
- New testing infrastructure needed for graph algorithms
Neutral:
- Does not replace existing neural pose estimation; supplements it
- Phase 1 can validate the approach before committing to full implementation
- May inform future ADRs on distributed sensing architecture
References
- ADR-029: RuvSense multistatic sensing mode
- ADR-028: ESP32 capability audit
- ADR-014: SOTA signal processing
- Research Doc 10: This document
8. Rust Trait Definitions
8.1 Core Traits
/// Unique identifier for a node in the RF mesh.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct NodeId(pub u16);
/// Unique identifier for an edge (ordered pair of nodes).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EdgeId {
pub tx: NodeId,
pub rx: NodeId,
}
impl EdgeId {
/// Create a canonical edge ID where tx < rx to avoid duplicates.
pub fn canonical(a: NodeId, b: NodeId) -> Self {
if a.0 <= b.0 {
Self { tx: a, rx: b }
} else {
Self { tx: b, rx: a }
}
}
}
/// Physical position of a node in 2D space (meters).
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct Position2D {
pub x: f64,
pub y: f64,
}
/// Information about a node in the mesh.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeInfo {
pub id: NodeId,
pub position: Position2D,
pub mac_address: [u8; 6],
pub tdm_slot: u8,
pub joined_at: u64, // unix timestamp ms
}
8.2 Edge Weight Trait
/// Trait for computing edge weights from CSI measurements.
pub trait EdgeWeightComputer: Send + Sync {
/// Compute the weight for an edge given current and baseline CSI.
fn compute(
&self,
current: &CsiFrame,
baseline: &CsiFrame,
config: &EdgeWeightConfig,
) -> Result<EdgeWeight, TopologyError>;
/// Update the temporal average for an edge.
fn update_average(
&self,
previous: &EdgeWeight,
new_sample: &EdgeWeight,
alpha: f64,
) -> EdgeWeight;
}
/// Configuration for edge weight computation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EdgeWeightConfig {
/// Exponential moving average smoothing factor.
pub ema_alpha: f64,
/// Minimum confidence to accept a measurement.
pub min_confidence: f64,
/// Number of subcarriers to use (0 = all).
pub subcarrier_count: usize,
/// Enable adversarial physics check.
pub physics_check: bool,
}
impl Default for EdgeWeightConfig {
fn default() -> Self {
Self {
ema_alpha: 0.3,
min_confidence: 0.5,
subcarrier_count: 0,
physics_check: true,
}
}
}
8.3 Graph Trait
/// Trait for the RF topology graph.
pub trait TopologyGraph: Send + Sync {
/// Add a node to the graph.
fn add_node(&mut self, node: NodeInfo) -> Result<Vec<TopologyEvent>, TopologyError>;
/// Remove a node and all its edges.
fn remove_node(&mut self, id: NodeId) -> Result<Vec<TopologyEvent>, TopologyError>;
/// Update the weight of an edge. Creates the edge if it doesn't exist.
fn update_edge(
&mut self,
edge: EdgeId,
weight: EdgeWeight,
) -> Result<Vec<TopologyEvent>, TopologyError>;
/// Remove edges that haven't been updated in `max_age` duration.
fn prune_stale(&mut self, max_age: std::time::Duration) -> Vec<TopologyEvent>;
/// Get the current weight of an edge.
fn edge_weight(&self, edge: EdgeId) -> Option<&EdgeWeight>;
/// Get all edges as (EdgeId, weight) pairs.
fn edges(&self) -> Vec<(EdgeId, f64)>;
/// Get the number of nodes.
fn node_count(&self) -> usize;
/// Get the number of edges.
fn edge_count(&self) -> usize;
/// Check if the graph is connected.
fn is_connected(&self) -> bool;
/// Get a snapshot of the adjacency matrix for mincut computation.
fn adjacency_matrix(&self) -> AdjacencyMatrix;
}
8.4 Mincut Solver Trait
/// Result of a minimum cut computation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinCutResult {
/// Edges that form the minimum cut.
pub cut_edges: Vec<EdgeId>,
/// Total weight of the cut.
pub cut_value: f64,
/// Nodes in partition A.
pub partition_a: Vec<NodeId>,
/// Nodes in partition B.
pub partition_b: Vec<NodeId>,
}
/// Trait for minimum cut solvers.
pub trait MinCutSolver: Send + Sync {
/// Compute the global minimum cut of the graph.
fn min_cut(&self, graph: &AdjacencyMatrix) -> Result<MinCutResult, TopologyError>;
/// Compute a k-way minimum cut (for multi-person detection).
fn k_cut(
&self,
graph: &AdjacencyMatrix,
k: usize,
) -> Result<Vec<MinCutResult>, TopologyError>;
/// Incrementally update the cut after edge weight changes.
/// Returns None if the cut topology hasn't changed.
fn incremental_update(
&self,
previous_cut: &MinCutResult,
changed_edges: &[(EdgeId, f64, f64)], // (edge, old_weight, new_weight)
graph: &AdjacencyMatrix,
) -> Result<Option<MinCutResult>, TopologyError>;
}
/// Stoer-Wagner implementation of MinCutSolver.
pub struct StoerWagnerSolver {
/// Cache the last contraction order for incremental updates.
last_contraction: Option<Vec<(NodeId, NodeId)>>,
}
impl MinCutSolver for StoerWagnerSolver {
fn min_cut(&self, graph: &AdjacencyMatrix) -> Result<MinCutResult, TopologyError> {
// Stoer-Wagner algorithm:
// 1. Start with arbitrary node
// 2. Repeatedly add "most tightly connected" node
// 3. Last two nodes define a cut candidate
// 4. Merge last two nodes, repeat
// 5. Return minimum cut found across all phases
todo!("Implement Stoer-Wagner")
}
fn k_cut(
&self,
graph: &AdjacencyMatrix,
k: usize,
) -> Result<Vec<MinCutResult>, TopologyError> {
// Recursive approach:
// 1. Find global mincut -> 2 partitions
// 2. Recursively find mincut in larger partition
// 3. Repeat until k partitions
todo!("Implement recursive k-cut")
}
fn incremental_update(
&self,
previous_cut: &MinCutResult,
changed_edges: &[(EdgeId, f64, f64)],
graph: &AdjacencyMatrix,
) -> Result<Option<MinCutResult>, TopologyError> {
// Heuristic: if no changed edge crosses the previous cut,
// and no weight changed by more than threshold, keep previous cut.
let cut_edge_set: std::collections::HashSet<_> =
previous_cut.cut_edges.iter().collect();
let significant_change = changed_edges.iter().any(|(edge, old, new)| {
let delta = (new - old).abs();
cut_edge_set.contains(edge) && delta > 0.1
});
if !significant_change {
return Ok(None); // Cut unchanged
}
// Recompute full mincut
self.min_cut(graph).map(Some)
}
}
8.5 Boundary Interpolation Trait
/// A polygon representing a physical boundary in 2D space.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoundaryPolygon {
/// Vertices of the boundary polygon (meters, room coordinates).
pub vertices: Vec<Position2D>,
/// Confidence of this boundary (0.0 to 1.0).
pub confidence: f64,
/// Unique ID for tracking across frames.
pub boundary_id: u64,
/// Timestamp of detection.
pub detected_at_ms: u64,
}
/// Trait for converting graph cuts into spatial boundaries.
pub trait BoundaryInterpolator: Send + Sync {
/// Convert a minimum cut result into a spatial boundary polygon.
fn interpolate(
&self,
cut: &MinCutResult,
node_positions: &std::collections::HashMap<NodeId, Position2D>,
) -> Result<BoundaryPolygon, TopologyError>;
/// Smooth a boundary using previous frame's boundary (temporal filtering).
fn smooth(
&self,
current: &BoundaryPolygon,
previous: &BoundaryPolygon,
alpha: f64,
) -> BoundaryPolygon;
}
/// Midpoint interpolation: boundary passes through midpoints of cut edges.
pub struct MidpointInterpolator;
impl BoundaryInterpolator for MidpointInterpolator {
fn interpolate(
&self,
cut: &MinCutResult,
node_positions: &std::collections::HashMap<NodeId, Position2D>,
) -> Result<BoundaryPolygon, TopologyError> {
let mut midpoints: Vec<Position2D> = Vec::new();
for edge in &cut.cut_edges {
let pos_a = node_positions
.get(&edge.tx)
.ok_or(TopologyError::NodeNotFound(edge.tx))?;
let pos_b = node_positions
.get(&edge.rx)
.ok_or(TopologyError::NodeNotFound(edge.rx))?;
midpoints.push(Position2D {
x: (pos_a.x + pos_b.x) / 2.0,
y: (pos_a.y + pos_b.y) / 2.0,
});
}
// Order midpoints to form a non-self-intersecting polygon
// using angular sort around centroid
let cx: f64 = midpoints.iter().map(|p| p.x).sum::<f64>() / midpoints.len() as f64;
let cy: f64 = midpoints.iter().map(|p| p.y).sum::<f64>() / midpoints.len() as f64;
midpoints.sort_by(|a, b| {
let angle_a = (a.y - cy).atan2(a.x - cx);
let angle_b = (b.y - cy).atan2(b.x - cx);
angle_a.partial_cmp(&angle_b).unwrap()
});
Ok(BoundaryPolygon {
vertices: midpoints,
confidence: 1.0 - cut.cut_value, // lower cut value = more confident
boundary_id: 0, // assigned by tracker
detected_at_ms: 0, // set by caller
})
}
fn smooth(
&self,
current: &BoundaryPolygon,
previous: &BoundaryPolygon,
alpha: f64,
) -> BoundaryPolygon {
// Simple vertex-wise EMA when vertex counts match
if current.vertices.len() != previous.vertices.len() {
return current.clone();
}
let smoothed: Vec<Position2D> = current
.vertices
.iter()
.zip(previous.vertices.iter())
.map(|(c, p)| Position2D {
x: alpha * c.x + (1.0 - alpha) * p.x,
y: alpha * c.y + (1.0 - alpha) * p.y,
})
.collect();
BoundaryPolygon {
vertices: smoothed,
confidence: alpha * current.confidence + (1.0 - alpha) * previous.confidence,
boundary_id: current.boundary_id,
detected_at_ms: current.detected_at_ms,
}
}
}
8.6 Pipeline Orchestrator
/// The main pipeline that ties all stages together.
pub struct TopologyPipeline {
graph: Box<dyn TopologyGraph>,
weight_computer: Box<dyn EdgeWeightComputer>,
mincut_solver: Box<dyn MinCutSolver>,
boundary_interpolator: Box<dyn BoundaryInterpolator>,
event_tx: tokio::sync::broadcast::Sender<TopologyEvent>,
config: PipelineConfig,
baselines: std::collections::HashMap<EdgeId, CsiFrame>,
last_cut: Option<MinCutResult>,
last_boundary: Option<BoundaryPolygon>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineConfig {
/// Maximum age before an edge is pruned.
pub stale_edge_timeout_ms: u64,
/// Edge weight computation config.
pub edge_weight: EdgeWeightConfig,
/// Minimum cut value change to trigger boundary update.
pub cut_change_threshold: f64,
/// Temporal smoothing factor for boundary polygon.
pub boundary_smoothing_alpha: f64,
/// Maximum number of simultaneous boundaries to track.
pub max_boundaries: usize,
}
impl TopologyPipeline {
/// Process a batch of CSI frames from one TDM cycle.
///
/// This is the main entry point, called once per TDM cycle.
/// Returns all topology events generated during processing.
pub async fn process_cycle(
&mut self,
frames: Vec<CsiFrame>,
) -> Result<Vec<TopologyEvent>, TopologyError> {
let mut all_events = Vec::new();
// Stage 2-3: Compute edge weights and update graph (parallel)
let weights: Vec<(EdgeId, EdgeWeight)> = frames
.par_iter()
.filter_map(|frame| {
let edge = EdgeId::canonical(
NodeId(frame.tx_id),
NodeId(frame.rx_id),
);
let baseline = self.baselines.get(&edge)?;
let weight = self.weight_computer
.compute(frame, baseline, &self.config.edge_weight)
.ok()?;
Some((edge, weight))
})
.collect();
// Stage 3: Update graph
let mut changed_edges = Vec::new();
for (edge_id, weight) in &weights {
let old_weight = self.graph
.edge_weight(*edge_id)
.map(|w| w.weight)
.unwrap_or(1.0);
let events = self.graph.update_edge(*edge_id, weight.clone())?;
changed_edges.push((*edge_id, old_weight, weight.weight));
all_events.extend(events);
}
// Prune stale edges
let stale_timeout =
std::time::Duration::from_millis(self.config.stale_edge_timeout_ms);
let prune_events = self.graph.prune_stale(stale_timeout);
all_events.extend(prune_events);
// Stage 4: Mincut
let adjacency = self.graph.adjacency_matrix();
let cut_result = if let Some(ref prev_cut) = self.last_cut {
self.mincut_solver
.incremental_update(prev_cut, &changed_edges, &adjacency)?
.unwrap_or_else(|| prev_cut.clone())
} else {
self.mincut_solver.min_cut(&adjacency)?
};
self.last_cut = Some(cut_result.clone());
// Stage 5: Boundary interpolation
let node_positions = self.node_position_map();
let mut boundary = self
.boundary_interpolator
.interpolate(&cut_result, &node_positions)?;
// Temporal smoothing
if let Some(ref prev_boundary) = self.last_boundary {
boundary = self.boundary_interpolator.smooth(
&boundary,
prev_boundary,
self.config.boundary_smoothing_alpha,
);
}
self.last_boundary = Some(boundary.clone());
// Emit boundary event
all_events.push(TopologyEvent {
id: EventId::new(),
timestamp: std::time::Instant::now(),
kind: TopologyEventKind::BoundaryDetected {
boundary: CutBoundary {
cut_edges: cut_result.cut_edges,
cut_value: cut_result.cut_value,
partition_a: cut_result.partition_a,
partition_b: cut_result.partition_b,
spatial_boundary: Some(boundary),
confidence: cut_result.cut_value,
detected_at: std::time::Instant::now(),
},
},
});
// Broadcast events
for event in &all_events {
let _ = self.event_tx.send(event.clone());
}
Ok(all_events)
}
fn node_position_map(&self) -> std::collections::HashMap<NodeId, Position2D> {
// Build from graph's node registry
todo!("Extract node positions from graph")
}
}
8.7 Error Types
/// Errors that can occur in the topology pipeline.
#[derive(Debug, thiserror::Error)]
pub enum TopologyError {
#[error("Node not found: {0:?}")]
NodeNotFound(NodeId),
#[error("Edge not found: {0:?} -> {1:?}")]
EdgeNotFound(NodeId, NodeId),
#[error("Graph is disconnected: {0} components")]
GraphDisconnected(usize),
#[error("Insufficient nodes for mincut: need >= 2, have {0}")]
InsufficientNodes(usize),
#[error("Baseline not available for edge {0:?}")]
NoBaseline(EdgeId),
#[error("CSI frame invalid: {0}")]
InvalidCsiFrame(String),
#[error("Mincut solver failed: {0}")]
SolverError(String),
#[error("Calibration required: {0}")]
CalibrationRequired(String),
}
8.8 Adjacency Matrix
/// Dense adjacency matrix for mincut computation.
///
/// Uses a flat Vec<f64> for cache-friendly access. Indexed as
/// matrix[row * dimension + col].
#[derive(Debug, Clone)]
pub struct AdjacencyMatrix {
/// Node IDs in index order.
pub nodes: Vec<NodeId>,
/// Flat weight matrix (dimension x dimension).
pub weights: Vec<f64>,
/// Matrix dimension (= nodes.len()).
pub dimension: usize,
}
impl AdjacencyMatrix {
pub fn new(nodes: Vec<NodeId>) -> Self {
let dim = nodes.len();
Self {
nodes,
weights: vec![0.0; dim * dim],
dimension: dim,
}
}
pub fn get(&self, row: usize, col: usize) -> f64 {
self.weights[row * self.dimension + col]
}
pub fn set(&mut self, row: usize, col: usize, value: f64) {
self.weights[row * self.dimension + col] = value;
self.weights[col * self.dimension + row] = value; // symmetric
}
/// Find the index of a node, or None if not present.
pub fn node_index(&self, id: NodeId) -> Option<usize> {
self.nodes.iter().position(|n| *n == id)
}
}
Appendix A: Glossary
| Term | Definition |
|---|---|
| CSI | Channel State Information -- per-subcarrier complex amplitude |
| TDM | Time Division Multiplexing -- collision-free TX scheduling |
| Mincut | Minimum cut -- partition of graph that minimizes total edge weight |
| Stoer-Wagner | Deterministic O(VE + V^2 log V) mincut algorithm |
| Edge weight | Coherence metric on a TX-RX link; low = obstructed |
| Boundary | Spatial region where mincut edges intersect physical space |
| Aggregate root | DDD pattern -- single entry point for a consistency boundary |
| EMA | Exponential Moving Average -- temporal smoothing filter |
Appendix B: Related ADRs
| ADR | Title | Relevance |
|---|---|---|
| 014 | SOTA signal processing | Coherence and phase algorithms |
| 028 | ESP32 capability audit | Hardware constraints and TDM |
| 029 | RuvSense multistatic sensing | Multi-link fusion architecture |
| 030 | RuvSense persistent field model | Baseline calibration approach |
| 031 | RuView sensing-first RF mode | UI integration pattern |
| 044 | RF Topological Sensing (this doc) | Architecture decision |
Appendix C: Open Questions
-
Stoer-Wagner vs. Push-Relabel: Which mincut algorithm is better for incremental updates? Push-relabel may allow warm-starting from previous flow solution.
-
Multi-person disambiguation: When k-cut finds multiple boundaries, how do we associate boundaries across frames? Nearest-neighbor in spatial coordinates? Hungarian algorithm on boundary centroids?
-
3D extension: The current design is 2D (nodes at fixed height). Can we extend to 3D by placing nodes at multiple heights? How does this affect mincut interpretation?
-
Furniture vs. people: Both attenuate CSI. Baseline calibration handles static furniture, but what about moved chairs? Adaptive baseline with slow drift tracking (ruvsense/longitudinal.rs) may help.
-
Optimal node placement: Given a room geometry, where should N nodes be placed to maximize boundary resolution? This is related to sensor placement optimization and Fisher Information from ruvector/viewpoint/geometry.rs.
-
Latency at scale: The 100ms budget assumes local processing. If graph data must traverse a network (multi-room, Phase 3), how do we maintain latency? Hierarchical decomposition with local mincut per room is the current proposal.
End of Research Document 10