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

> Discover how RuView implements the ADR-048 adaptive classifier in pure Rust. Learn about its 15-dimensional feature extraction and multinomial logistic regression model without external ML dependencies.

- Repository: [rUv/RuView](https://github.com/ruvnet/RuView)
- Tags: internals
- Published: 2026-03-08

---

**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`](https://github.com/ruvnet/RuView/blob/main/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:

```rust
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]`:

```rust
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:

```rust
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`](https://github.com/ruvnet/RuView/blob/main/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

```rust
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`](https://github.com/ruvnet/RuView/blob/main/src/main.rs) (lines 11-12) as a shared state component:

```rust
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

```rust
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

```rust
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

```bash
cargo run --bin sensing-server -- --train

```

The `--train` flag (parsed in [`main.rs`](https://github.com/ruvnet/RuView/blob/main/main.rs) lines 14-15) executes the training pipeline, writes [`data/adaptive_model.json`](https://github.com/ruvnet/RuView/blob/main/data/adaptive_model.json), and exits.

## Summary

- RuView implements ADR-048 as a pure-Rust module in [`adaptive_classifier.rs`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/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`](https://github.com/ruvnet/RuView/blob/main/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.