- CHANGELOG: add MERIDIAN (ADR-027) to Unreleased section - README: add "Works Everywhere" to Intelligence features, update How It Works - ADR-002: status → Superseded by ADR-016/017 - ADR-004: status → Partially realized by ADR-024, extended by ADR-027 - ADR-005: status → Partially realized by ADR-023, extended by ADR-027 - ADR-006: status → Partially realized by ADR-023, extended by ADR-027 Co-Authored-By: claude-flow <ruv@ruv.net>
264 lines
13 KiB
Markdown
264 lines
13 KiB
Markdown
# ADR-006: GNN-Enhanced CSI Pattern Recognition
|
||
|
||
## Status
|
||
Partially realized in [ADR-023](ADR-023-trained-densepose-model-ruvector-pipeline.md); extended by [ADR-027](ADR-027-cross-environment-domain-generalization.md)
|
||
|
||
> **Note:** ADR-023 implements a 2-layer GCN on the COCO skeleton graph for spatial reasoning. ADR-027 (MERIDIAN) adds domain-adversarial regularization via a gradient reversal layer that forces the GCN to learn environment-invariant graph features, shedding room-specific multipath patterns.
|
||
|
||
## Date
|
||
2026-02-28
|
||
|
||
## Context
|
||
|
||
### Limitations of Independent Vector Search
|
||
|
||
ADR-004 introduces HNSW-based similarity search for CSI pattern matching. While HNSW provides fast nearest-neighbor retrieval, it treats each vector independently. CSI patterns, however, have rich relational structure:
|
||
|
||
1. **Temporal adjacency**: CSI frames captured 10ms apart are more related than frames 10s apart. Sequential patterns reveal motion trajectories.
|
||
|
||
2. **Spatial correlation**: CSI readings from adjacent subcarriers are highly correlated due to frequency proximity. Antenna pairs capture different spatial perspectives.
|
||
|
||
3. **Cross-session similarity**: The "walking to kitchen" pattern from Tuesday should inform Wednesday's recognition, but the environment baseline may have shifted.
|
||
|
||
4. **Multi-person entanglement**: When multiple people are present, CSI patterns are superpositions. Disentangling requires understanding which pattern fragments co-occur.
|
||
|
||
Standard HNSW cannot capture these relationships. Each query returns neighbors based solely on vector distance, ignoring the graph structure of how patterns relate to each other.
|
||
|
||
### RuVector's GNN Enhancement
|
||
|
||
RuVector implements a Graph Neural Network layer that sits on top of the HNSW index:
|
||
|
||
```
|
||
Standard HNSW: Query → Distance-based neighbors → Results
|
||
GNN-Enhanced: Query → Distance-based neighbors → GNN refinement → Improved results
|
||
```
|
||
|
||
The GNN performs three operations in <1ms:
|
||
1. **Message passing**: Each node aggregates information from its HNSW neighbors
|
||
2. **Attention weighting**: Multi-head attention identifies which neighbors are most relevant for the current query context
|
||
3. **Representation update**: Node embeddings are refined based on neighborhood context
|
||
|
||
Additionally, **temporal learning** tracks query sequences to discover:
|
||
- Vectors that frequently appear together in sessions
|
||
- Temporal ordering patterns (A usually precedes B)
|
||
- Session context that changes relevance rankings
|
||
|
||
## Decision
|
||
|
||
We will integrate RuVector's GNN layer to enhance CSI pattern recognition with three core capabilities: relational search, temporal sequence modeling, and multi-person disentanglement.
|
||
|
||
### GNN Architecture for CSI
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ GNN-Enhanced CSI Pattern Graph │
|
||
├─────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Layer 1: HNSW Spatial Graph │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ Nodes = CSI feature vectors │ │
|
||
│ │ Edges = HNSW neighbor connections (distance-based) │ │
|
||
│ │ Node features = [amplitude | phase | doppler | PSD] │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ Layer 2: Temporal Edges │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ Additional edges between temporally adjacent vectors │ │
|
||
│ │ Edge weight = 1/Δt (closer in time = stronger) │ │
|
||
│ │ Direction = causal (past → future) │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ Layer 3: GNN Message Passing (2 rounds) │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ Round 1: h_i = σ(W₁·h_i + Σⱼ α_ij · W₂·h_j) │ │
|
||
│ │ Round 2: h_i = σ(W₃·h_i + Σⱼ α'_ij · W₄·h_j) │ │
|
||
│ │ α_ij = softmax(LeakyReLU(a^T[W·h_i || W·h_j])) │ │
|
||
│ │ (Graph Attention Network mechanism) │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ Layer 4: Refined Representations │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ Updated vectors incorporate neighborhood context │ │
|
||
│ │ Re-rank search results using refined distances │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Three Integration Modes
|
||
|
||
#### Mode 1: Query-Time Refinement (Default)
|
||
|
||
GNN refines HNSW results after retrieval. No modifications to stored vectors.
|
||
|
||
```rust
|
||
pub struct GnnQueryRefiner {
|
||
/// GNN weights (small: ~50K parameters)
|
||
gnn_weights: GnnModel,
|
||
|
||
/// Number of message passing rounds
|
||
num_rounds: usize, // 2
|
||
|
||
/// Attention heads for neighbor weighting
|
||
num_heads: usize, // 4
|
||
|
||
/// How many HNSW neighbors to consider in GNN
|
||
neighborhood_size: usize, // 20 (retrieve 20, GNN selects best 5)
|
||
}
|
||
|
||
impl GnnQueryRefiner {
|
||
/// Refine HNSW results using graph context
|
||
pub fn refine(&self, query: &[f32], hnsw_results: &[SearchResult]) -> Vec<SearchResult> {
|
||
// Build local subgraph from query + HNSW results
|
||
let subgraph = self.build_local_subgraph(query, hnsw_results);
|
||
|
||
// Run message passing
|
||
let refined = self.message_pass(&subgraph, self.num_rounds);
|
||
|
||
// Re-rank based on refined representations
|
||
self.rerank(query, &refined)
|
||
}
|
||
}
|
||
```
|
||
|
||
**Latency**: +0.2ms on top of HNSW search (total <1.5ms for 100K vectors).
|
||
|
||
#### Mode 2: Temporal Sequence Recognition
|
||
|
||
Tracks CSI vector sequences to recognize activity patterns that span multiple frames:
|
||
|
||
```rust
|
||
/// Temporal pattern recognizer using GNN edges
|
||
pub struct TemporalPatternRecognizer {
|
||
/// Sliding window of recent query vectors
|
||
window: VecDeque<TimestampedVector>,
|
||
|
||
/// Maximum window size (in frames)
|
||
max_window: usize, // 100 (10 seconds at 10 Hz)
|
||
|
||
/// Temporal edge decay factor
|
||
decay: f64, // 0.95 (edges weaken with time)
|
||
|
||
/// Known activity sequences (learned from data)
|
||
activity_templates: HashMap<String, Vec<Vec<f32>>>,
|
||
}
|
||
|
||
impl TemporalPatternRecognizer {
|
||
/// Feed new CSI vector and check for activity pattern matches
|
||
pub fn observe(&mut self, vector: &[f32], timestamp: f64) -> Vec<ActivityMatch> {
|
||
self.window.push_back(TimestampedVector { vector: vector.to_vec(), timestamp });
|
||
|
||
// Build temporal subgraph from window
|
||
let temporal_graph = self.build_temporal_graph();
|
||
|
||
// GNN aggregates temporal context
|
||
let sequence_embedding = self.gnn_aggregate(&temporal_graph);
|
||
|
||
// Match against known activity templates
|
||
self.match_activities(&sequence_embedding)
|
||
}
|
||
}
|
||
```
|
||
|
||
**Activity patterns detectable**:
|
||
|
||
| Activity | Frames Needed | CSI Signature |
|
||
|----------|--------------|---------------|
|
||
| Walking | 10-30 | Periodic Doppler oscillation |
|
||
| Falling | 5-15 | Sharp amplitude spike → stillness |
|
||
| Sitting down | 10-20 | Gradual descent in reflection height |
|
||
| Breathing (still) | 30-100 | Micro-periodic phase variation |
|
||
| Gesture (wave) | 5-15 | Localized high-frequency amplitude variation |
|
||
|
||
#### Mode 3: Multi-Person Disentanglement
|
||
|
||
When N>1 people are present, CSI is a superposition. The GNN learns to cluster pattern fragments:
|
||
|
||
```rust
|
||
/// Multi-person CSI disentanglement using GNN clustering
|
||
pub struct MultiPersonDisentangler {
|
||
/// Maximum expected simultaneous persons
|
||
max_persons: usize, // 10
|
||
|
||
/// GNN-based spectral clustering
|
||
cluster_gnn: GnnModel,
|
||
|
||
/// Per-person tracking state
|
||
person_tracks: Vec<PersonTrack>,
|
||
}
|
||
|
||
impl MultiPersonDisentangler {
|
||
/// Separate CSI features into per-person components
|
||
pub fn disentangle(&mut self, features: &CsiFeatures) -> Vec<PersonFeatures> {
|
||
// Decompose CSI into subcarrier groups using GNN attention
|
||
let subcarrier_graph = self.build_subcarrier_graph(features);
|
||
|
||
// GNN clusters subcarriers by person contribution
|
||
let clusters = self.cluster_gnn.cluster(&subcarrier_graph, self.max_persons);
|
||
|
||
// Extract per-person features from clustered subcarriers
|
||
clusters.iter().map(|c| self.extract_person_features(features, c)).collect()
|
||
}
|
||
}
|
||
```
|
||
|
||
### GNN Learning Loop
|
||
|
||
The GNN improves with every query through RuVector's built-in learning:
|
||
|
||
```
|
||
Query → HNSW retrieval → GNN refinement → User action (click/confirm/reject)
|
||
│
|
||
▼
|
||
Update GNN weights via:
|
||
1. Positive: confirmed results get higher attention
|
||
2. Negative: rejected results get lower attention
|
||
3. Temporal: successful sequences reinforce edges
|
||
```
|
||
|
||
For WiFi-DensePose, "user action" is replaced by:
|
||
- **Temporal consistency**: If frame N+1 confirms frame N's detection, reinforce
|
||
- **Multi-AP agreement**: If two APs agree on detection, reinforce both
|
||
- **Physical plausibility**: If pose satisfies skeletal constraints, reinforce
|
||
|
||
### Performance Budget
|
||
|
||
| Component | Parameters | Memory | Latency (per query) |
|
||
|-----------|-----------|--------|-------------------|
|
||
| GNN weights (2 layers, 4 heads) | 52K | 208 KB | 0.15 ms |
|
||
| Temporal graph (100-frame window) | N/A | ~130 KB | 0.05 ms |
|
||
| Multi-person clustering | 18K | 72 KB | 0.3 ms |
|
||
| **Total GNN overhead** | **70K** | **410 KB** | **0.5 ms** |
|
||
|
||
## Consequences
|
||
|
||
### Positive
|
||
- **Context-aware search**: Results account for temporal and spatial relationships, not just vector distance
|
||
- **Activity recognition**: Temporal GNN enables sequence-level pattern matching
|
||
- **Multi-person support**: GNN clustering separates overlapping CSI patterns
|
||
- **Self-improving**: Every query provides learning signal to refine attention weights
|
||
- **Lightweight**: 70K parameters, 410 KB memory, 0.5ms latency overhead
|
||
|
||
### Negative
|
||
- **Training data needed**: GNN weights require initial training on CSI pattern graphs
|
||
- **Complexity**: Three modes increase testing and debugging surface
|
||
- **Graph maintenance**: Temporal edges must be pruned to prevent unbounded growth
|
||
- **Approximation**: GNN clustering for multi-person is approximate; may merge/split incorrectly
|
||
|
||
### Interaction with Other ADRs
|
||
- **ADR-004** (HNSW): GNN operates on HNSW graph structure; depends on HNSW being available
|
||
- **ADR-005** (SONA): GNN weights can be adapted via SONA LoRA for environment-specific tuning
|
||
- **ADR-003** (RVF): GNN weights stored in model container alongside inference weights
|
||
- **ADR-010** (Witness): GNN weight updates recorded in witness chain
|
||
|
||
## References
|
||
|
||
- [Graph Attention Networks (GAT)](https://arxiv.org/abs/1710.10903)
|
||
- [Temporal Graph Networks](https://arxiv.org/abs/2006.10637)
|
||
- [Spectral Clustering with Graph Neural Networks](https://arxiv.org/abs/1907.00481)
|
||
- [WiFi-based Multi-Person Sensing](https://dl.acm.org/doi/10.1145/3534592)
|
||
- [RuVector GNN Implementation](https://github.com/ruvnet/ruvector)
|
||
- ADR-004: HNSW Vector Search for Signal Fingerprinting
|