How RuView's Self-Learning System Uses Contrastive CSI Embeddings and MicroLoRA Adapters
RuView achieves environment-specific adaptation in approximately 10 seconds by combining contrastive CSI embeddings (AETHER) with rank-4 MicroLoRA adapters trained via unsupervised test-time training, adding only ~1.8 KB of parameters per room.
The RuView repository (ruvnet/RuView) implements a continuous self-learning system that enables WiFi-based sensing to adapt to new environments without requiring labeled data. By integrating contrastive CSI embeddings with MicroLoRA adapters, the system performs rapid test-time training using only unlabeled Channel State Information (CSI) frames captured over roughly 10 seconds.
Contrastive CSI Embedding Architecture (AETHER)
The foundation of RuView's self-learning capability rests on the AETHER embedding model, defined in rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/embedding.rs. This architecture implements a projection head consisting of a 2-layer MLP built atop the CSI-to-pose transformer backbone.
The ProjectionHead struct supports optional LoRA adapters and produces 128-dimensional, L2-normalized embeddings:
pub struct ProjectionHead {
pub proj_1: Linear,
pub proj_2: Linear,
pub config: EmbeddingConfig,
pub lora_1: Option<LoraAdapter>,
pub lora_2: Option<LoraAdapter>,
}
During contrastive pre-training, the system applies InfoNCE loss to two augmented views of the same CSI window. The CsiAugmenter module (located in the same file) generates these views through temporal jitter, subcarrier masking, Gaussian noise injection, phase rotation, and amplitude scaling. The resulting L2-normalized embedding space enables efficient cosine similarity calculations for the on-device HNSW index.
According to ADR-024 (documented in docs/adr/ADR-024-contrastive-csi-embedding-model.md), this design ensures the backbone learns invariant features while the projection head remains adaptable to environment-specific multipath fingerprints.
MicroLoRA Adapter Design
MicroLoRA adapters provide the parameter-efficient mechanism for environment personalization. Each adapter consists of a rank-4 low-rank factorization (A·B) that introduces approximately 1,800 floating-point parameters (~1.8 KB) per room.
The LoraAdapter struct attaches parallel weight-delta matrices to the base linear layers. Key methods in embedding.rs handle the adapter lifecycle:
ProjectionHead::with_lorainstantiates the projection head with attached adaptersmerge_lorafolds the low-rank deltas into the base weight matrix for zero-overhead inferenceflatten_loraandunflatten_loraserialize adapters for storage in.rvf(RuView File) containers
As implemented in docs/adr/ADR-028-esp32-capability-audit.md, this approach allows the ESP32-compatible system to store multiple room profiles without exhausting limited flash memory.
Rapid Test-Time Training Engine
The RapidAdaptation engine in rust-port/wifi-densepose-rs/crates/wifi-densepose-train/src/rapid_adapt.rs orchestrates the unsupervised calibration process. This engine buffers incoming CSI frames and executes lightweight gradient descent to produce per-room LoRA weights.
The core struct definition specifies calibration parameters:
pub struct RapidAdaptation {
pub min_calibration_frames: usize, // default 200 (≈10 s @ 20 Hz)
pub lora_rank: usize, // typically 4
pub adaptation_loss: AdaptationLoss,
pub max_buffer_frames: usize,
calibration_buffer: Vec<Vec<f32>>,
}
The AdaptationLoss enum supports three optimization strategies:
ContrastiveTTT: Builds triplets from temporally adjacent frames (positive pairs) and distant frames (negative pairs)EntropyMin: Minimizes prediction entropy to prevent embedding collapseCombined: Balances contrastive and entropy objectives using alambda_entweighting factor
The adapt() method executes the training loop:
for _ in 0..epochs {
let mut grad = vec![0.0; lora_sz];
let loss = match &self.adaptation_loss {
AdaptationLoss::ContrastiveTTT { .. } => self.contrastive_step(&w, fdim, &mut grad),
AdaptationLoss::EntropyMin { .. } => self.entropy_step(&w, fdim, &mut grad),
AdaptationLoss::Combined { lambda_ent, .. } => {
let cl = self.contrastive_step(&w, fdim, &mut grad);
let mut eg = vec![0.0; lora_sz];
let el = self.entropy_step(&w, fdim, &mut eg);
for (g, eg_i) in grad.iter_mut().zip(eg.iter()) {
*g += lambda_ent * eg_i;
}
cl + lambda_ent * el
}
};
// SGD update
for (wi, gi) in w.iter_mut().zip(grad.iter()) {
*wi -= lr * gi;
}
}
Upon completion, the engine returns an AdaptationResult containing the trained lora_weights, final loss value, and frames consumed.
End-to-End Self-Learning Workflow
The complete integration pipeline operates across six stages:
- Bootstrap: Load the base model (backbone plus projection head) from
embedding.rs - Embedding Extraction: Process CSI windows through
CsiAugmenterto generate stochastic views, then compute AETHER embeddings via the projection head - Calibration Trigger: Accumulate
min_calibration_frames(default 200) into theRapidAdaptationbuffer - Adapter Generation: Execute
RapidAdaptation::adapt()to learn environment-specific LoRA deltas - Deployment: Instantiate
LoraAdapterfrom the results, attach viaProjectionHead::with_lora, and merge into base weights usingmerge_lora() - Inference: Run the merged model to produce room-specific embeddings for improved re-identification and pose estimation
Practical Implementation Examples
Initializing the Embedding Pipeline
use wifi_densepose_sensing_server::embedding::{
EmbeddingConfig, ProjectionHead, CsiAugmenter,
};
let cfg = EmbeddingConfig::default(); // d_model=64, d_proj=128
let mut proj = ProjectionHead::new(cfg.clone()); // base head without LoRA
let augmenter = CsiAugmenter::new(); // stochastic augmentations
Generating Contrastive Views
let csi_window: Vec<Vec<f32>> = acquire_csi_frames(10); // 10-frame window
let (view_a, view_b) = augmenter.augment_pair(&csi_window, 0xDEADBEEF);
let emb_a = proj.forward(&flatten(view_a));
let emb_b = proj.forward(&flatten(view_b));
Executing Rapid Adaptation
use wifi_densepose_train::rapid_adapt::{
RapidAdaptation, AdaptationLoss,
};
let loss = AdaptationLoss::Combined {
epochs: 5,
lr: 0.001,
lambda_ent: 0.5,
};
let mut ra = RapidAdaptation::new(200, 4, loss); // 200 frames ≈10s @20Hz
for frame in unlabeled_stream.take(200) {
ra.push_frame(&frame);
}
let result = ra.adapt().expect("adaptation failed");
// Apply learned weights
let lora = LoraAdapter::from_flat_weights(
cfg.d_model, cfg.d_proj, 4, result.lora_weights
);
proj.lora_1 = Some(lora.clone());
proj.lora_2 = Some(lora);
proj.merge_lora(); // zero-overhead inference
Persisting Room Profiles
use wifi_densepose_sensing_server::rvf_container::RvfBuilder;
let mut builder = RvfBuilder::new();
builder.add_lora_profile("conference-room-a", proj.flatten_lora());
let rvf_bytes = builder.build().expect("RVF serialization failed");
// Store rvf_bytes to flash or transmit to edge server
Summary
- Contrastive CSI embeddings (AETHER) provide a 128-dimensional, L2-normalized feature space learned via InfoNCE loss and stochastic augmentation in
embedding.rs - MicroLoRA adapters utilize rank-4 factorization (~1.8 KB per room) to enable environment-specific fine-tuning without modifying base model weights
- Rapid test-time training processes 200 unlabeled frames (~10 seconds) through the
RapidAdaptationengine to learn adapter weights via contrastive and entropy-based objectives - Zero-overhead inference is achieved by merging adapters into base linear layers using
merge_lora()after calibration completes - Persistent storage uses the RVF container format to serialize adapters for multi-room deployment scenarios
Frequently Asked Questions
What differentiates MicroLoRA from standard LoRA implementations?
MicroLoRA uses a fixed rank-4 decomposition specifically optimized for the projection head in RuView's CSI embedding pipeline, constraining each adapter to approximately 1,800 parameters. This aggressive compression enables storage of multiple room profiles on ESP32 devices with severe memory constraints, whereas standard LoRA might use ranks of 8-64 requiring significantly more storage.
How does contrastive learning function without ground-truth labels?
The system leverages temporal consistency inherent in CSI streams. The CsiAugmenter generates two perturbed views from the same physical time window, treating them as positive pairs, while samples from distant time windows serve as negatives. This self-supervised approach, implemented via AdaptationLoss::ContrastiveTTT, eliminates the need for manually annotated pose labels during environment adaptation.
Why is the calibration period limited to approximately 10 seconds?
The default min_calibration_frames value of 200 frames at 20 Hz sampling rate balances statistical sufficiency with user experience. As defined in rapid_adapt.rs, this duration captures enough multipath variation to characterize the room's radio fingerprint while remaining short enough for practical deployment scenarios where users cannot tolerate lengthy calibration procedures.
Can learned MicroLoRA adapters survive device reboots?
Yes. The flatten_lora method serializes adapter weights into portable byte vectors, which the RvfBuilder (located in rvf_container.rs) packages into .rvf files. These containers persist the learned parameters to flash storage, allowing the system to reload specific room profiles via model_manager::activate_lora() without repeating the 10-second calibration process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →