Overview
The crate exposes a set of focused modules selected by use case. Everything public is re-exported at the crate root (opensmell::*), so you import one path, not internals. Data flows through two core types: SensorReading (one time-slice of channel values) and Baseline (per-channel R0 calibration).
opensmell = { git = "https://github.com/OpenSmell/opensmell-rs" }A SensorReading carries its channel values, a timestamp, and its active channels (finite, non-zero) — so downstream code can skip dead channels without re-deriving them. A Baseline is built from the leading 15% of samples per channel (median R0 plus per-channel std). RawData::from_csv(path) loads a CSV into rows-of-samples with an optional sampling rate and channel names.
Crate-root API
The following is the complete public surface re-exported at the crate root:
| Module | Public items |
|---|---|
| features | FeatureGroup · extract_features · extract_window_features · feature_names |
| anomaly | AnomalyDetector · AnomalyScore · AnomalyMethod |
| calibration | Calibrator · CalibrationProfile · CrossDeviceCalibrator |
| health | HealthMonitor · SensorHealth · HealthStatus · FleetHealth · fisher_discriminant_ratio · pairwise_fdr · euclidean_distance · cosine_similarity · similarity_warning |
| protocol | OsmProtocol · OsmMessage |
| preprocessing | RawData · BaselineCorrection · BaselineMethod · SignalFilter · FilterType · WindowExtractor · DataValidator |
| adaptive | AdaptiveAnomalyDetector · AdaptiveThreshold · FailSafeSystem · LabelingSystem · DetectionResult · AccuracyImprovement · DetectorState · LabelingStats · FailSafeResult · LabelRecord |
| poisoning | PoisoningDetector · SensorHealthConfig · SensorHealthStatus · SensorMetrics · DegradationType |
| quality | compute_quality · ChannelSeries · QualityParams · QualityReport |
| training | train_classifier · TrainOptions · TrainingReport · ClassifierModel · ModelCard · ConfusionCell · PairSimilarity · LabeledRecording · paradigm_window_features · extract_window_features_by_mode · feature_length_for · framework_feature_len · extract_training_windows · compute_warning · DEFAULT_WINDOW_SIZE · TRAIN_STRIDE · PythonModelExport · PythonScalerExport · PythonLrExport · PythonMetadataExport |
| framework | framework_window_features · compute_multi_exp_decay |
| live | LiveClassifier · LiveSnapshot · Prediction · ROLLING_WINDOW · LOCK_THRESHOLD · LOCK_CONSECUTIVE · UNKNOWN_THRESHOLD · UNKNOWN_CONSECUTIVE |
Feature extraction
FeatureGroup is an enum of seven groups, each a family of features and a use case. You select only the groups you need — the crate will not compute features you do not ask for.
| Group | Features | Best for |
|---|---|---|
| Anomaly | Drift rate, stability, noise floor, fractional derivatives | Monitoring, spoilage/leak detection, cold-chain |
| Classification | Absolute resistance, calibrated concentration | Substance identification, fingerprinting |
| Health | Hysteresis, sensitivity decay, thermal profile, ADC noise | Predicting sensor failure, scheduling maintenance |
| Kinetics | Rise time, decay time, multi-exponential decay parameters | Adsorption/desorption dynamics |
| Selectivity | Cross-channel ratios | Gas discrimination, understanding sensor overlap |
| Temporal | High-frequency transients, oscillation, response latency | Rapid events (gas leaks, spoilage onset) |
| Hardware | Circuit response, thermal profile, ADC noise | Diagnosing hardware issues, quality control |
use opensmell::{SensorReading, Baseline, FeatureGroup, extract_features, feature_names};
let reading = SensorReading::new(vec![120.0, 95.0, 300.0, 401.0, 25.0, 210.0], 0.0);
let baseline = /* from a clean-air window */;
let groups = &[FeatureGroup::Kinetics, FeatureGroup::Selectivity];
let names = feature_names(groups, 6);
let feats = extract_features(&reading, &baseline, groups)?;extract_features(reading, baseline, groups) returns a flat Vec<f64>; feature_names(groups, n_channels) returns the matching ordered names; extract_window_features runs per-window for streaming buffers. The framework module provides the canonical model — framework_window_features and compute_multi_exp_decay — used by classifier training.
The canonical framework's length is a function of channel count: framework_feature_len(n_channels) = 28·c + c(c−1)/2 + 4. That is 187 at the canonical 6-channel rig, but it recomputes for any c ( feature_length_for(n_channels, mode) dispatches by feature mode). This is the sensor-count-agnostic statement of the same model that reads 187 at 6 channels elsewhere in the stack.
Preprocessing
RawData::from_csv— load CSV into rows-of-samples with optional rate/names/timestamps.BaselineCorrection— estimate R0;BaselineMethodchooses median, mean, EWMA, or percentile.SignalFilter/FilterType— median, moving average, Savitzky-Golay, or high-pass.WindowExtractor— slide a window over a buffer.DataValidator— reject or flag malformed data before processing.
BaselineCorrection defaults to median baseline over the first 15% with a minimum of 30 samples.
Quality
compute_quality(time, channels, params) scores a recording and returns a QualityReport. ChannelSeries carries per-channel data; QualityParams sets the scoring context (ADC max, sampling rate, session role, baseline source). The role and baseline-source are honored — signal/recovery sub-scores are suppressed for non-exposure roles — and the report tracks the same flags as the Python scorer (used-default-ADC, used-median-rate, no-baseline, non-finite, dead sensors, unsorted rows).
Anomaly, adaptive & poisoning
Three layers protect live systems. AnomalyDetector (with AnomalyScore/AnomalyMethod) flags out-of-distribution readings. The adaptive module adds online learning — AdaptiveAnomalyDetector, AdaptiveThreshold, a FailSafeSystem, and a LabelingSystem that feeds AccuracyImprovement tracking. The poisoning module detects data poisoning via PoisoningDetector against per-sensor health metrics.
Calibration
Calibrator fits and inverts the MOX power law; CalibrationProfile is the persisted result. CrossDeviceCalibrator handles the harder — and honestly-reported — case of transferring a calibration across devices.
Cross-device calibration is a real limitation, not a solved claim: affine calibration degrades on real cross-device data (47% → 33%), and the SDK never presents a calibration as an absolute physical truth.
Health & fleet
HealthMonitor tracks per-sensor SensorHealth and a HealthStatus; FleetHealth aggregates across devices. Helper scores — fisher_discriminant_ratio, pairwise_fdr, euclidean_distance, cosine_similarity, similarity_warning — quantify how separable or confusable channels and substances are.
OSM protocol
OsmProtocol parses the serial framing used by connected e-nose boards. Lines begin with OSM and carry comma-separated readings; protocol is 6-channel max and split off bootloader noise. OsmProtocol::new(expected_channels) and parse_line(line, host_timestamp) convert a line into an OsmMessage. The protocol module also ships a generate_arduino_sketch helper for producing firmware.
Classifier training & evaluation
train_classifier(recordings, name, options) trains on a set of LabeledRecordings and returns a TrainingReport. Training is device-agnostic on sensor count (windows are validated for hardware sufficiency), quality-gated, and evaluated by leave-one-recording-out (LORO). The report carries a ModelCard and per-class confusion cells and pair similarities.
| Item | Role |
|---|---|
TrainOptions | window_size, n_sensors (3–6), min_quality, stride, feature_mode, sampling rate. |
LabeledRecording | A recording plus its substance label. |
TrainingReport | Results: model, LORO metrics, warnings, quality notes. |
ModelCard | Provenance: name, classes, feature mode, hardware gate, evaluation. |
Python*Export | Serialize the trained classifier to Python (model / scaler / logistic-regression / metadata) for cross-language consumption. |
Training windowing is exposed through paradigm_window_features, extract_window_features_by_mode, feature_length_for, framework_feature_len, and extract_training_windows.
A model trained with six channels is never silently run on five: the hardware gate checks the rig's effective dimensionality against the model's requirement and warns (or refuses) rather than padding a dead channel with a mean.
Live classification
LiveClassifier loads a trained ClassifierModel and produces Predictions from live buffers via a rolling window. It implements a deliberate lock/unknown state machine — constants ROLLING_WINDOW, LOCK_THRESHOLD, LOCK_CONSECUTIVE, UNKNOWN_THRESHOLD, UNKNOWN_CONSECUTIVE — that refuses to emit confident predictions until the window is large and stable enough, and honestly reports an unknown state otherwise. LiveSnapshot captures the state for UI polling.
Module reference
All modules are pub: features, anomaly, calibration, health, protocol, preprocessing, framework, adaptive, poisoning, quality, training, live. The crate depends on ndarray, num-traits, serde, thiserror, csv, chrono, and log.
The Rust crate is the compute core, not a full-stack SDK. It operates on CSV and JSON (serde) because the numeric pipeline — feature extraction, classification, anomaly detection — is the hot path, and the crate is designed to run on embedded targets (ESP32 firmware) and real-time desktop streams without a Python runtime or ZIP dependency. The portable .osmell container, typed descriptors, and the ingest chain live in the Python SDK, web platform, and the desktop app's Tauri backend, which handle the container I/O the crate deliberately skips.
