docs: add RuvSense sensing-first RF mode architecture

Research, ADR, and DDD specification for multistatic WiFi DensePose
with coherence-gated tracking and complete ruvector integration.

- docs/research/ruvsense-multistatic-fidelity-architecture.md:
  SOTA research covering bandwidth/frequency/viewpoint fidelity levers,
  ESP32 multistatic mesh design, coherence gating, AETHER embedding
  integration, and full ruvector crate mapping

- docs/adr/ADR-029-ruvsense-multistatic-sensing-mode.md:
  Architecture decision for sensing-first RF mode on existing ESP32
  silicon. GOAP integration plan (9 actions, 4 phases, 36 cost units).
  TDMA schedule for 20 Hz update rate from 4-node mesh.
  IEEE 802.11bf forward-compatible design.

- docs/ddd/ruvsense-domain-model.md:
  Domain-Driven Design with 3 bounded contexts (Multistatic Sensing,
  Coherence, Pose Tracking), aggregate roots, domain events, context
  map, anti-corruption layers, and repository interfaces.

Acceptance test: 2 people, 20 Hz, 10 min stable tracks, zero ID swaps,
<30mm torso keypoint jitter.

https://claude.ai/code/session_01QTX772SDsGVSPnaphoNgNY
This commit is contained in:
Claude
2026-03-02 00:17:30 +00:00
parent 08a6d5a7f1
commit 25b005a0d6
3 changed files with 1870 additions and 0 deletions

View File

@@ -0,0 +1,593 @@
# RuvSense Domain Model
## Domain-Driven Design Specification
### Ubiquitous Language
| Term | Definition |
|------|------------|
| **Sensing Cycle** | One complete TDMA round (all nodes TX once): 50ms at 20 Hz |
| **Link** | A single TX-RX pair; with N nodes there are N×(N-1) directed links |
| **Multi-Band Frame** | Fused CSI from one node hopping across multiple channels in one dwell cycle |
| **Fused Sensing Frame** | Aggregated observation from all nodes at one sensing cycle, ready for inference |
| **Coherence Score** | 0.0-1.0 metric quantifying consistency of current CSI with reference template |
| **Coherence Gate** | Decision rule that accepts, inflates noise, rejects, or triggers recalibration |
| **Pose Track** | A temporally persistent per-person 17-keypoint trajectory with Kalman state |
| **Track Lifecycle** | State machine: Tentative → Active → Lost → Terminated |
| **Re-ID Embedding** | 128-dim AETHER contrastive vector encoding body identity |
| **Node** | An ESP32-S3 device acting as both TX and RX in the multistatic mesh |
| **Aggregator** | Central device (ESP32/RPi/x86) that collects CSI from all nodes and runs fusion |
| **Sensing Schedule** | TDMA slot assignment: which node transmits when |
| **Channel Hop** | Switching the ESP32 radio to a different WiFi channel for multi-band sensing |
| **Person Cluster** | A subset of links whose CSI variations are correlated (attributed to one person) |
---
## Bounded Contexts
### 1. Multistatic Sensing Context
**Responsibility:** Collect, normalize, and fuse CSI from multiple ESP32 nodes across multiple channels into a single coherent sensing frame per cycle.
```
┌──────────────────────────────────────────────────────────┐
│ Multistatic Sensing Context │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Link Buffer │ │ Multi-Band │ │
│ │ Collector │ │ Fuser │ │
│ │ (per-link │ │ (per-node │ │
│ │ ring buf) │ │ channel │ │
│ └───────┬───────┘ │ fusion) │ │
│ │ └───────┬───────┘ │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Phase Aligner │ │
│ │ (cross-chan │ │
│ │ correction) │ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Multistatic │ │
│ │ Fuser │──▶ FusedSensingFrame │
│ │ (cross-node │ │
│ │ attention) │ │
│ └────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `FusedSensingFrame` (Aggregate Root)
**Value Objects:**
- `MultiBandCsiFrame`
- `LinkGeometry` (tx_pos, rx_pos, distance, angle)
- `SensingSchedule`
- `ChannelHopConfig`
**Domain Services:**
- `PhaseAlignmentService` — Corrects LO-induced phase rotation between channels
- `MultiBandFusionService` — Merges per-channel CSI into wideband virtual frame
- `MultistaticFusionService` — Attention-based fusion of N nodes into one frame
**RuVector Integration:**
- `ruvector-solver` → Phase alignment (NeumannSolver)
- `ruvector-attention` → Cross-channel feature weighting
- `ruvector-attn-mincut` → Cross-node spectrogram attention gating
- `ruvector-temporal-tensor` → Per-link compressed ring buffers
---
### 2. Coherence Context
**Responsibility:** Monitor temporal consistency of CSI observations and gate downstream updates to reject drift, transient interference, and environmental changes.
```
┌──────────────────────────────────────────────────────────┐
│ Coherence Context │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Reference │ │ Coherence │ │
│ │ Template │ │ Calculator │ │
│ │ (EMA of │ │ (z-score per │ │
│ │ static CSI) │ │ subcarrier) │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Static/Dynamic│ │
│ │ Decomposer │ │
│ │ (separate env │ │
│ │ vs. body) │ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Gate Policy │──▶ GateDecision │
│ │ (Accept / │ (Accept / PredictOnly / │
│ │ Reject / │ Reject / Recalibrate) │
│ │ Recalibrate) │ │
│ └────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `CoherenceState` (Aggregate Root) — Maintains reference template and gate state
**Value Objects:**
- `CoherenceScore` (0.0-1.0)
- `GateDecision` (Accept / PredictOnly / Reject / Recalibrate)
- `ReferenceTemplate` (EMA of static-period CSI)
- `DriftProfile` (Stable / Linear / StepChange)
**Domain Services:**
- `CoherenceCalculatorService` — Computes per-subcarrier z-score coherence
- `StaticDynamicDecomposerService` — Separates environmental drift from body motion
- `GatePolicyService` — Applies threshold-based gating rules
**RuVector Integration:**
- `ruvector-solver` → Coherence matrix decomposition (static vs. dynamic)
- `ruvector-attn-mincut` → Gate which subcarriers contribute to template update
---
### 3. Pose Tracking Context
**Responsibility:** Track multiple people as persistent 17-keypoint skeletons across time, with Kalman-smoothed trajectories, lifecycle management, and identity preservation via re-ID.
```
┌──────────────────────────────────────────────────────────┐
│ Pose Tracking Context │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Person │ │ Detection │ │
│ │ Separator │ │ -to-Track │ │
│ │ (min-cut on │ │ Assigner │ │
│ │ link corr) │ │ (Hungarian+ │ │
│ └───────┬───────┘ │ embedding) │ │
│ │ └───────┬───────┘ │
│ │ │ │
│ └────────┬───────────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Kalman Filter │ │
│ │ (17-keypoint │ │
│ │ 6D state ×17)│ │
│ └────────┬───────┘ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Lifecycle │ │
│ │ Manager │──▶ TrackedPose │
│ │ (Tentative → │ │
│ │ Active → │ │
│ │ Lost) │ │
│ └────────┬───────┘ │
│ │ │
│ ┌────────▼───────┐ │
│ │ Embedding │ │
│ │ Identifier │ │
│ │ (AETHER re-ID)│ │
│ └────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
```
**Aggregates:**
- `PoseTrack` (Aggregate Root)
**Entities:**
- `KeypointState` — Per-keypoint Kalman state (x,y,z,vx,vy,vz) with covariance
**Value Objects:**
- `TrackedPose` — Immutable snapshot: 17 keypoints + confidence + track_id + lifecycle
- `PersonCluster` — Subset of links attributed to one person
- `AssignmentCost` — Combined Mahalanobis + embedding distance
- `TrackLifecycleState` (Tentative / Active / Lost / Terminated)
**Domain Services:**
- `PersonSeparationService` — Min-cut partitioning of cross-link correlation graph
- `TrackAssignmentService` — Bipartite matching of detections to existing tracks
- `KalmanPredictionService` — Predict step at 20 Hz (decoupled from measurement rate)
- `KalmanUpdateService` — Gated measurement update (subject to coherence gate)
- `EmbeddingIdentifierService` — AETHER cosine similarity for re-ID
**RuVector Integration:**
- `ruvector-mincut` → Person separation (DynamicMinCut on correlation graph)
- `ruvector-mincut` → Detection-to-track assignment (DynamicPersonMatcher)
- `ruvector-attention` → Embedding similarity via ScaledDotProductAttention
---
## Core Domain Entities
### FusedSensingFrame (Value Object)
```rust
pub struct FusedSensingFrame {
/// Timestamp of this sensing cycle
pub timestamp_us: u64,
/// Fused multi-band spectrogram from all nodes
/// Shape: [n_velocity_bins x n_time_frames]
pub fused_bvp: Vec<f32>,
pub n_velocity_bins: usize,
pub n_time_frames: usize,
/// Per-node multi-band frames (preserved for geometry)
pub node_frames: Vec<MultiBandCsiFrame>,
/// Node positions (from deployment config)
pub node_positions: Vec<[f32; 3]>,
/// Number of active nodes contributing
pub active_nodes: usize,
/// Cross-node coherence (higher = more agreement)
pub cross_node_coherence: f32,
}
```
### PoseTrack (Aggregate Root)
```rust
pub struct PoseTrack {
/// Unique track identifier
pub id: TrackId,
/// Per-keypoint Kalman state
pub keypoints: [KeypointState; 17],
/// Track lifecycle state
pub lifecycle: TrackLifecycleState,
/// Running-average AETHER embedding for re-ID
pub embedding: Vec<f32>, // [128]
/// Frames since creation
pub age: u64,
/// Frames since last successful measurement update
pub time_since_update: u64,
/// Creation timestamp
pub created_at: u64,
/// Last update timestamp
pub updated_at: u64,
}
```
### KeypointState (Entity)
```rust
pub struct KeypointState {
/// State vector [x, y, z, vx, vy, vz]
pub state: [f32; 6],
/// 6x6 covariance matrix (upper triangle, row-major)
pub covariance: [f32; 21],
/// Confidence (0.0-1.0) from DensePose model
pub confidence: f32,
}
```
### CoherenceState (Aggregate Root)
```rust
pub struct CoherenceState {
/// Per-subcarrier reference amplitude (EMA)
pub reference: Vec<f32>,
/// Per-subcarrier variance over recent window
pub variance: Vec<f32>,
/// EMA decay rate for reference update
pub decay: f32,
/// Current coherence score
pub score: f32,
/// Frames since last accepted update
pub stale_count: u64,
/// Current drift profile classification
pub drift_profile: DriftProfile,
}
```
---
## Domain Events
### Sensing Events
```rust
pub enum SensingEvent {
/// New fused sensing frame available
FrameFused {
timestamp_us: u64,
active_nodes: usize,
cross_node_coherence: f32,
},
/// Node joined or left the mesh
MeshTopologyChanged {
node_id: u8,
change: TopologyChange, // Joined / Left / Degraded
active_nodes: usize,
},
/// Channel hop completed on a node
ChannelHopCompleted {
node_id: u8,
from_channel: u8,
to_channel: u8,
gap_us: u32,
},
}
```
### Coherence Events
```rust
pub enum CoherenceEvent {
/// Coherence dropped below accept threshold
CoherenceLost {
score: f32,
threshold: f32,
timestamp_us: u64,
},
/// Coherence recovered above accept threshold
CoherenceRestored {
score: f32,
stale_duration_ms: u64,
timestamp_us: u64,
},
/// Recalibration triggered (>10s low coherence)
RecalibrationTriggered {
stale_duration_ms: u64,
timestamp_us: u64,
},
/// Recalibration completed via SONA TTT
RecalibrationCompleted {
adaptation_loss: f32,
timestamp_us: u64,
},
/// Environmental drift detected
DriftDetected {
drift_type: DriftProfile,
magnitude: f32,
timestamp_us: u64,
},
}
```
### Tracking Events
```rust
pub enum TrackingEvent {
/// New person detected (track born)
PersonDetected {
track_id: TrackId,
position: [f32; 3], // centroid
confidence: f32,
timestamp_us: u64,
},
/// Person pose updated
PoseUpdated {
track_id: TrackId,
keypoints: [[f32; 4]; 17], // [x, y, z, conf] per keypoint
jitter_mm: f32, // RMS jitter at torso
timestamp_us: u64,
},
/// Person lost (signal dropout)
PersonLost {
track_id: TrackId,
last_position: [f32; 3],
last_embedding: Vec<f32>,
timestamp_us: u64,
},
/// Person re-identified after loss
PersonReidentified {
track_id: TrackId,
previous_track_id: TrackId,
similarity: f32,
gap_duration_ms: u64,
timestamp_us: u64,
},
/// Track terminated (exceeded max lost duration)
TrackTerminated {
track_id: TrackId,
reason: TerminationReason,
total_duration_ms: u64,
timestamp_us: u64,
},
}
pub enum TerminationReason {
/// Exceeded max_lost_frames without re-acquisition
SignalTimeout,
/// Confidence below minimum for too long
LowConfidence,
/// Determined to be false positive
FalsePositive,
/// System shutdown
SystemShutdown,
}
```
---
## Context Map
```
┌──────────────────────────────────────────────────────────────────┐
│ RuvSense System │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ FusedFrame ┌──────────────────┐ │
│ │ Multistatic │──────────────▶│ Pose Tracking │ │
│ │ Sensing │ │ Context │ │
│ │ Context │ │ │ │
│ └────────┬─────────┘ └────────┬───────────┘ │
│ │ │ │
│ │ Publishes │ Publishes │
│ │ SensingEvent │ TrackingEvent │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Event Bus (Domain Events) │ │
│ └────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────▼───────────┐ │
│ │ Coherence Context │ │
│ │ (subscribes to │ │
│ │ SensingEvent; │ │
│ │ publishes │ │
│ │ CoherenceEvent; │ │
│ │ gates Tracking │ │
│ │ updates) │ │
│ └───────────────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ UPSTREAM (Conformist) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │wifi-densepose│ │wifi-densepose│ │wifi-densepose│ │
│ │ -hardware │ │ -nn │ │ -signal │ │
│ │ (CsiFrame │ │ (DensePose │ │ (SOTA algs │ │
│ │ parser) │ │ model) │ │ per link) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
├──────────────────────────────────────────────────────────────────┤
│ SIBLING (Partnership) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ AETHER │ │ MERIDIAN │ │ MAT │ │
│ │ (ADR-024) │ │ (ADR-027) │ │ (ADR-001) │ │
│ │ embeddings │ │ geometry │ │ triage │ │
│ │ for re-ID │ │ encoding │ │ lifecycle │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
**Relationship Types:**
- Multistatic Sensing → Pose Tracking: **Customer/Supplier** (Sensing produces FusedFrames; Tracking consumes)
- Coherence → Multistatic Sensing: **Subscriber** (monitors frame quality)
- Coherence → Pose Tracking: **Gate/Interceptor** (controls measurement updates)
- RuvSense → Upstream crates: **Conformist** (adapts to their types)
- RuvSense → AETHER/MERIDIAN/MAT: **Partnership** (shared embedding/geometry/tracking abstractions)
---
## Anti-Corruption Layer
### Hardware Adapter (Multistatic Sensing → wifi-densepose-hardware)
```rust
/// Adapts raw ESP32 CsiFrame to RuvSense MultiBandCsiFrame
pub struct MultiBandAdapter {
/// Group frames by (node_id, channel) within time window
window_ms: u32,
/// Hardware normalizer (from MERIDIAN, ADR-027)
normalizer: HardwareNormalizer,
}
impl MultiBandAdapter {
/// Collect raw CsiFrames from one TDMA cycle and produce
/// one MultiBandCsiFrame per node.
pub fn adapt_cycle(
&self,
raw_frames: &[CsiFrame],
) -> Vec<MultiBandCsiFrame>;
}
```
### Model Adapter (Pose Tracking → wifi-densepose-nn)
```rust
/// Adapts DensePose model output to tracking-compatible detections
pub struct PoseDetectionAdapter;
impl PoseDetectionAdapter {
/// Convert model output (heatmaps + offsets) to detected poses
/// with keypoint positions and AETHER embeddings.
pub fn adapt(
&self,
model_output: &ModelOutput,
fused_frame: &FusedSensingFrame,
) -> Vec<PoseDetection>;
}
pub struct PoseDetection {
pub keypoints: [[f32; 4]; 17], // [x, y, z, confidence]
pub embedding: Vec<f32>, // [128] AETHER embedding
pub person_cluster: PersonCluster,
}
```
### MAT Adapter (Pose Tracking → wifi-densepose-mat)
```rust
/// Adapts RuvSense TrackedPose to MAT Survivor entity
/// for disaster response scenarios.
pub struct SurvivorAdapter;
impl SurvivorAdapter {
/// Convert a RuvSense TrackedPose to a MAT Survivor
/// with vital signs extracted from small-motion analysis.
pub fn to_survivor(
&self,
track: &PoseTrack,
vital_signs: Option<&VitalSignsReading>,
) -> Survivor;
}
```
---
## Repository Interfaces
```rust
/// Persists and retrieves pose tracks
pub trait PoseTrackRepository {
fn save(&self, track: &PoseTrack);
fn find_by_id(&self, id: &TrackId) -> Option<PoseTrack>;
fn find_active(&self) -> Vec<PoseTrack>;
fn find_lost(&self) -> Vec<PoseTrack>;
fn remove(&self, id: &TrackId);
}
/// Persists coherence state for long-term analysis
pub trait CoherenceRepository {
fn save_snapshot(&self, state: &CoherenceState, timestamp_us: u64);
fn load_latest(&self) -> Option<CoherenceState>;
fn load_history(&self, duration_ms: u64) -> Vec<(u64, f32)>;
}
/// Persists mesh topology and node health
pub trait MeshRepository {
fn save_node(&self, node_id: u8, position: [f32; 3], health: NodeHealth);
fn load_topology(&self) -> Vec<(u8, [f32; 3], NodeHealth)>;
fn save_schedule(&self, schedule: &SensingSchedule);
fn load_schedule(&self) -> Option<SensingSchedule>;
}
```
---
## Invariants
### Multistatic Sensing
- At least 2 nodes must be active for multistatic fusion (fallback to single-node mode otherwise)
- Channel hop sequence must contain at least 1 non-overlapping channel
- TDMA cycle period must be ≤50ms for 20 Hz output
- Guard interval must be ≥2× clock drift budget (≥1ms for 50ms cycle)
### Coherence
- Reference template must be recalculated every 10 minutes during quiet periods
- Gate threshold must be calibrated per-environment (initial defaults: accept=0.85, drift=0.5)
- Stale count must not exceed max_stale (200 frames = 10s) without triggering recalibration
- Static/dynamic decomposition must preserve energy: ||S|| + ||D|| ≈ ||C||
### Pose Tracking
- Exactly one Kalman predict step per output frame (20 Hz, regardless of measurement availability)
- Birth gate: track not promoted to Active until 2 consecutive measurement updates
- Loss threshold: track marked Lost after 5 consecutive missed measurements
- Re-ID window: Lost tracks eligible for re-identification for 5 seconds
- Embedding EMA decay: 0.95 (slow adaptation preserves identity across environmental changes)
- Joint assignment cost must use both position (60%) and embedding (40%) terms