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_lora instantiates the projection head with attached adapters
  • merge_lora folds the low-rank deltas into the base weight matrix for zero-overhead inference
  • flatten_lora and unflatten_lora serialize 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 collapse
  • Combined: Balances contrastive and entropy objectives using a lambda_ent weighting 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:

  1. Bootstrap: Load the base model (backbone plus projection head) from embedding.rs
  2. Embedding Extraction: Process CSI windows through CsiAugmenter to generate stochastic views, then compute AETHER embeddings via the projection head
  3. Calibration Trigger: Accumulate min_calibration_frames (default 200) into the RapidAdaptation buffer
  4. Adapter Generation: Execute RapidAdaptation::adapt() to learn environment-specific LoRA deltas
  5. Deployment: Instantiate LoraAdapter from the results, attach via ProjectionHead::with_lora, and merge into base weights using merge_lora()
  6. 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 RapidAdaptation engine 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →