How the Adaptive Classifier (ADR-048) Is Implemented in Pure Rust in RuView

RuView implements the ADR-048 adaptive CSI activity classifier as a self-contained Rust module that extracts 15-dimensional feature vectors from Wi-Fi CSI frames and trains a multinomial logistic regression model via deterministic mini-batch SGD, eliminating external ML dependencies.

RuView embeds the ADR-048 adaptive classifier directly into its sensing server crate as pure Rust, enabling real-time activity recognition on ESP32 CSI data without requiring Python or TensorFlow. The implementation handles everything from JSON feature extraction to model serialization within rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/src/adaptive_classifier.rs, providing a portable, zero-dependency solution for adaptive threshold calibration.

Architecture of the Rust Implementation

The adaptive classifier organizes functionality into four distinct layers:

  • Feature Extraction: features_from_frame converts raw JSON CSI frames into fixed-size arrays
  • Statistical Helpers: subcarrier_stats computes distribution metrics across 56 sub-carriers
  • Model Representation: AdaptiveModel stores centroids, normalization parameters, and logistic weights
  • Training & Inference: train_from_recordings performs SGD optimization while AdaptiveModel::classify produces runtime predictions

Feature Extraction Pipeline

Converting JSON to 15-Dimensional Vectors

The features_from_frame function (lines 27-55) parses incoming frames containing server-computed metrics and raw sub-carrier amplitudes, outputting a [f64; 15] vector:

pub fn features_from_frame(frame: &serde_json::Value) -> [f64; N_FEATURES] {
    // Extract server features: variance, mbp, bbp, sp, df, cp, rssi
    // Extract raw amplitudes from "nodes" array
    let (amp_mean, amp_std, amp_skew, amp_kurt, amp_iqr,
         amp_entropy, amp_max, amp_range) = subcarrier_stats(&amps);
    [
        variance, mbp, bbp, sp, df, cp, rssi,
        amp_mean, amp_std, amp_skew, amp_kurt,
        amp_iqr, amp_entropy, amp_max, amp_range,
    ]
}

Statistical Descriptors for Sub-Carriers

The subcarrier_stats helper (lines 74-108) calculates eight robust statistics from the amplitude slice, including clamped standard deviation (minimum 1e-9) and entropy normalized to [0,1]:

fn subcarrier_stats(amps: &[f64]) -> (f64, f64, f64, f64, f64, f64, f64, f64) {
    // Computes: mean, std, skew, kurtosis, IQR, entropy, max, range
}

The AdaptiveModel Structure

Model Representation

Defined at lines 21-38, AdaptiveModel encapsulates everything needed for inference:

pub struct AdaptiveModel {
    pub class_stats: Vec<ClassStats>,               // Per-class mean/std
    pub weights: Vec<[f64; N_FEATURES + 1]>,        // Logistic weights with bias term
    pub global_mean: [f64; N_FEATURES],
    pub global_std:  [f64; N_FEATURES],
    pub trained_frames: usize,
    pub training_accuracy: f64,
    pub version: u32,
}

Per-Class Statistics

The ClassStats struct (lines 13-19) maintains per-class centroids that enable Mahalanobis-like distance calculations as a fallback mechanism, directly supporting the ADR-048 specification for class-specific normalization.

Training Pipeline Implementation

Loading Labeled Recordings

The train_from_recordings function (lines 41-78) scans data/recordings/ for train_*.jsonl files, assigning class indices based on filename patterns: empty → 0, still → 1, walking → 2, active → 3. Each JSON line undergoes feature extraction and normalization using global Z-score statistics computed across the entire training set.

Deterministic Mini-Batch SGD

Lines 124-179 implement a custom stochastic gradient descent optimizer with the following ADR-048 specifications:

  • Batch size: 32 samples
  • Epochs: 200 with linear learning rate decay (current_lr = lr * (1 - epoch/epochs))
  • Deterministic shuffling: Linear congruential generator (LCG) seeded with 42
  • Loss function: Cross-entropy against softmax probabilities

The algorithm accumulates gradients across batches, updates the weight matrix (including bias terms at index N_FEATURES), and serializes the final model to data/adaptive_model.json.

Real-Time Inference

At runtime, the server loads the JSON model and processes incoming CSI frames through AdaptiveModel::classify (lines 52-90):

  1. Z-score normalization: Applies global_mean and global_std to input features
  2. Logit computation: Matrix multiplication of weights against normalized features plus bias
  3. Softmax activation: Converts logits to class probabilities
  4. Argmax selection: Returns the activity label and confidence score
pub fn classify(&self, raw_features: &[f64; N_FEATURES]) -> (&'static str, f64) {
    // Normalise input using global statistics
    // Compute logits = weights · x + bias
    // Apply softmax to obtain probabilities
    // Return (label, confidence) for argmax class
}

Integration with the Axum Sensing Server

The classifier integrates into src/main.rs (lines 11-12) as a shared state component:

mod adaptive_classifier;
let adaptive_model = AdaptiveModel::load(&adaptive_classifier::model_path()).ok();
let state = SharedState { adaptive_model, ... };

When loaded, the adaptive_override helper (lines 940-956) replaces static threshold-based classification with the adaptive model. The server exposes three REST endpoints (lines 2492-2555):

  • POST /api/v1/adaptive/train: Triggers train_from_recordings on the recordings directory
  • GET /api/v1/adaptive/status: Returns JSON with loaded, accuracy, and per-class statistics
  • POST /api/v1/adaptive/unload: Clears the model and reverts to static thresholds

Code Examples

Training via HTTP API

use reqwest::blocking::Client;
use serde_json::json;

let client = Client::new();
let resp = client
    .post("http://127.0.0.1:8080/api/v1/adaptive/train")
    .json(&json!({ "recordings_dir": "data/recordings" }))
    .send()
    .expect("train request failed");

println!("Training response: {}", resp.text().unwrap());
// Output: {"success":true,"accuracy":0.86}

Classifying Frames Programmatically

use wifi_densepose_sensing_server::adaptive_classifier::{
    AdaptiveModel, features_from_frame,
};
use std::path::Path;

let model = AdaptiveModel::load(Path::new("data/adaptive_model.json"))
    .expect("Failed to load adaptive model");

let feature_vec = features_from_frame(&frame_json);
let (label, confidence) = model.classify(&feature_vec);

println!("Activity: {} ({:.2}% confidence)", label, confidence * 100.0);

Command-Line Training

cargo run --bin sensing-server -- --train

The --train flag (parsed in main.rs lines 14-15) executes the training pipeline, writes data/adaptive_model.json, and exits.

Summary

  • RuView implements ADR-048 as a pure-Rust module in adaptive_classifier.rs, eliminating external ML dependencies
  • The system extracts 15-dimensional feature vectors combining server-computed metrics with eight sub-carrier statistics (mean, std, skew, kurtosis, IQR, entropy, max, range)
  • Deterministic mini-batch SGD (32-sample batches, 200 epochs, LCG seeded shuffling) trains a multinomial logistic regression model
  • The AdaptiveModel struct stores per-class centroids, global Z-score parameters, and weight matrices with bias terms
  • Runtime inference applies Z-score normalization followed by softmax classification, integrated into the Axum server via adaptive_override

Frequently Asked Questions

What makes the RuView adaptive classifier "pure Rust"?

Unlike solutions that bridge to Python libraries or ONNX runtimes, RuView's ADR-048 implementation performs all matrix operations, statistics calculations, and gradient descent optimization using native Rust code in adaptive_classifier.rs. This eliminates foreign function interface overhead and ensures the classifier runs on resource-constrained edge devices alongside the ESP32 sensing stack.

How does the classifier handle different Wi-Fi environments?

The training pipeline computes global Z-score statistics across all training samples, then normalizes incoming frames using these environment-specific means and standard deviations. Per-class centroids stored in ClassStats enable the model to adapt thresholds to specific room geometries and hardware configurations without manual calibration.

Why does the implementation use a custom SGD instead of standard ML crates?

ADR-048 specifies deterministic behavior (LCG seeded with 42) and specific hyperparameters (linear LR decay, 32-sample batches) that must execute identically across platforms. The custom implementation in lines 124-179 of adaptive_classifier.rs ensures zero external dependencies while meeting the architectural requirement for reproducible training results across different Rust compiler versions.

Can the model classify activities in real-time on the ESP32?

No, the ESP32 streams raw CSI data via UDP to the RuView sensing server, which runs the Rust classifier. The ESP32 firmware remains lightweight, while the computational heavy lifting—feature extraction, normalization, and softmax inference—occurs on the server running the wifi-densepose-sensing-server crate.

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 →