mirror of
https://github.com/ruvnet/RuView
synced 2026-07-22 17:23:19 +00:00
fix(adr-185): feature-gate MAT's ONNX ml module to cut [mat] wheel 8.4MB->2.0MB
Root cause (investigated, not assumed): the heavy deps in a [mat] wheel were NOT on the survivor-detection/triage path the Python binding uses. `wifi-densepose-mat` pulled `wifi-densepose-nn` as a NON-optional dep, and nn's default `onnx` feature drags in `ort` (ONNX Runtime) + its download/reqwest/hyper stack. But nn is used ONLY by mat's `src/ml/` module (debris + vital-signs ONNX classifiers), which is an OPTIONAL runtime enhancement: `DetectionPipeline.ml_pipeline` is `Option<_>`, created only when `DetectionConfig::enable_ml` is set, and `from_disaster_config` (the DisasterResponse path) never enables it. So the ONNX stack was compiled-and-linked but never executed by the binding. Fix = feature-gating (not a hoist — a hoist was unnecessary): - `wifi-densepose-nn` made optional; new `ml` Cargo feature gates it. - `default = [..., "ml"]` so existing consumers are unchanged; `api` implies `ml` (the REST surface exposes `ml_ready`). - `#[cfg(feature = "ml")]` on the `ml` module, its crate-root + prelude re-exports, the `MatError::Ml` variant, and every ml touchpoint in detection/pipeline.rs (ml_config/ml_pipeline fields, constructor, process_zone enhancement block, enhance_with_ml/get_ml_results/ initialize_ml/ml_ready/ml_pipeline accessors, update_config). - Fixed a latent feature-unification bug surfaced by dropping nn: integration/hardware_adapter.rs uses `tokio::select!`, which needs `tokio/macros`. That was only satisfied transitively via nn; now declared explicitly on mat's own tokio dep so --no-default-features builds compile. The ADR-185 [mat] wheel already depended on mat with `default-features = false, features = ["std"]`, so no python/ change was needed — dropping `ml` from the default now actually removes ort. Measured (maturin build --release --features mat --strip): BEFORE (ml/ort in wheel): 8.4 MB (over the ADR-117 §5.4 <=5 MB budget) AFTER (ml gated off): 2.0 MB (under budget) — 76% smaller Verified: cargo build -p wifi-densepose-mat (default+ml) OK cargo build -p wifi-densepose-mat --no-default-features --features std OK cargo test --features mat --test mat_parity 2/2 pass maturin develop --features mat + pytest tests/test_mat.py 7/7 pass (same detection result: 1 survivor, triage Delayed — behavior-transparent)
This commit is contained in:
Generated
-1
@@ -4474,7 +4474,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"uuid",
|
||||
"wifi-densepose-core",
|
||||
"wifi-densepose-nn",
|
||||
"wifi-densepose-signal",
|
||||
]
|
||||
|
||||
|
||||
@@ -12,15 +12,25 @@ categories = ["science", "algorithms"]
|
||||
readme = "README.md"
|
||||
|
||||
[features]
|
||||
default = ["std", "api", "ruvector"]
|
||||
default = ["std", "api", "ruvector", "ml"]
|
||||
ruvector = ["dep:ruvector-solver", "dep:ruvector-temporal-tensor"]
|
||||
std = []
|
||||
# ONNX-backed ML detection (debris + vital-signs classifiers). Pulls
|
||||
# `wifi-densepose-nn` (and, via its default `onnx` feature, the `ort`
|
||||
# ONNX Runtime + its download/reqwest stack) ONLY when enabled. The
|
||||
# survivor-detection/triage pipeline works without it (ML is an optional
|
||||
# enhancement, off unless `DetectionConfig::enable_ml`), so consumers that
|
||||
# don't need ONNX — e.g. the ADR-185 `wifi-densepose-py` `[mat]` wheel —
|
||||
# build `--no-default-features` and drop the entire ort/reqwest tree,
|
||||
# keeping the wheel within the ADR-117 §5.4 budget.
|
||||
ml = ["dep:wifi-densepose-nn"]
|
||||
# REST/WebSocket surface. Pulls the web stack (axum, futures-util) only when
|
||||
# enabled, and enables the `serde` FEATURE (not just `dep:serde`) so the
|
||||
# `cfg_attr(feature = "serde", ...)` derives on domain types are actually
|
||||
# active when the API is on (review finding 5: `api = ["dep:serde"]` enabled
|
||||
# the dependency but left every `feature = "serde"` cfg dead).
|
||||
api = ["serde", "dep:axum", "dep:futures-util"]
|
||||
# The REST surface exposes ML status (`ml_ready`), so `api` implies `ml`.
|
||||
api = ["ml", "serde", "dep:axum", "dep:futures-util"]
|
||||
# Real ESP32 serial CSI ingest. Pulls the native `serialport` crate (libudev on
|
||||
# Linux) only when enabled, so the default/no-default appliance build stays free
|
||||
# of native serial deps. With the feature OFF, the ESP32 serial *parser* still
|
||||
@@ -36,14 +46,18 @@ serde = ["dep:serde", "chrono/serde", "geo/use-serde"]
|
||||
# Workspace dependencies
|
||||
wifi-densepose-core = { version = "0.3.0", path = "../wifi-densepose-core" }
|
||||
wifi-densepose-signal = { version = "0.3.0", path = "../wifi-densepose-signal", default-features = false }
|
||||
wifi-densepose-nn = { version = "0.3.0", path = "../wifi-densepose-nn" }
|
||||
wifi-densepose-nn = { version = "0.3.0", path = "../wifi-densepose-nn", optional = true }
|
||||
ruvector-solver = { workspace = true, optional = true }
|
||||
ruvector-temporal-tensor = { workspace = true, optional = true }
|
||||
|
||||
# Async runtime — required by the core integration layer (UDP CSI receiver,
|
||||
# hardware adapter, scan loop in `DisasterResponse::start_scanning`), not just
|
||||
# the REST API, so it is deliberately NOT gated behind `api`.
|
||||
tokio = { version = "1.35", features = ["rt", "sync", "time"] }
|
||||
# `macros` is needed by `tokio::select!` in integration/hardware_adapter.rs.
|
||||
# It was previously satisfied only by feature-unification from the (now
|
||||
# optional) `wifi-densepose-nn` dep; declare it explicitly so a
|
||||
# `--no-default-features` build (the ADR-185 [mat] wheel) still compiles.
|
||||
tokio = { version = "1.35", features = ["rt", "sync", "time", "macros"] }
|
||||
async-trait = "0.1"
|
||||
|
||||
# Web framework (REST API) — only compiled with the `api` feature.
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::{
|
||||
MovementClassifier, MovementClassifierConfig,
|
||||
};
|
||||
use crate::domain::{ScanZone, VitalSignsReading};
|
||||
#[cfg(feature = "ml")]
|
||||
use crate::ml::{MlDetectionConfig, MlDetectionPipeline, MlDetectionResult};
|
||||
use crate::{DisasterConfig, MatError};
|
||||
|
||||
@@ -26,9 +27,10 @@ pub struct DetectionConfig {
|
||||
pub enable_heartbeat: bool,
|
||||
/// Minimum overall confidence to report detection
|
||||
pub min_confidence: f64,
|
||||
/// Enable ML-enhanced detection
|
||||
/// Enable ML-enhanced detection (requires the `ml` feature to have any effect)
|
||||
pub enable_ml: bool,
|
||||
/// ML detection configuration (if enabled)
|
||||
#[cfg(feature = "ml")]
|
||||
pub ml_config: Option<MlDetectionConfig>,
|
||||
}
|
||||
|
||||
@@ -42,6 +44,7 @@ impl Default for DetectionConfig {
|
||||
enable_heartbeat: false,
|
||||
min_confidence: 0.3,
|
||||
enable_ml: false,
|
||||
#[cfg(feature = "ml")]
|
||||
ml_config: None,
|
||||
}
|
||||
}
|
||||
@@ -64,6 +67,7 @@ impl DetectionConfig {
|
||||
}
|
||||
|
||||
/// Enable ML-enhanced detection with the given configuration
|
||||
#[cfg(feature = "ml")]
|
||||
pub fn with_ml(mut self, ml_config: MlDetectionConfig) -> Self {
|
||||
self.enable_ml = true;
|
||||
self.ml_config = Some(ml_config);
|
||||
@@ -71,6 +75,7 @@ impl DetectionConfig {
|
||||
}
|
||||
|
||||
/// Enable ML-enhanced detection with default configuration
|
||||
#[cfg(feature = "ml")]
|
||||
pub fn with_default_ml(mut self) -> Self {
|
||||
self.enable_ml = true;
|
||||
self.ml_config = Some(MlDetectionConfig::default());
|
||||
@@ -147,12 +152,14 @@ pub struct DetectionPipeline {
|
||||
movement_classifier: MovementClassifier,
|
||||
data_buffer: parking_lot::RwLock<CsiDataBuffer>,
|
||||
/// Optional ML detection pipeline
|
||||
#[cfg(feature = "ml")]
|
||||
ml_pipeline: Option<MlDetectionPipeline>,
|
||||
}
|
||||
|
||||
impl DetectionPipeline {
|
||||
/// Create a new detection pipeline
|
||||
pub fn new(config: DetectionConfig) -> Self {
|
||||
#[cfg(feature = "ml")]
|
||||
let ml_pipeline = if config.enable_ml {
|
||||
config.ml_config.clone().map(MlDetectionPipeline::new)
|
||||
} else {
|
||||
@@ -164,12 +171,14 @@ impl DetectionPipeline {
|
||||
heartbeat_detector: HeartbeatDetector::new(config.heartbeat.clone()),
|
||||
movement_classifier: MovementClassifier::new(config.movement.clone()),
|
||||
data_buffer: parking_lot::RwLock::new(CsiDataBuffer::new(config.sample_rate)),
|
||||
#[cfg(feature = "ml")]
|
||||
ml_pipeline,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize ML models asynchronously (if enabled)
|
||||
#[cfg(feature = "ml")]
|
||||
pub async fn initialize_ml(&mut self) -> Result<(), MatError> {
|
||||
if let Some(ref mut ml) = self.ml_pipeline {
|
||||
ml.initialize().await.map_err(MatError::from)?;
|
||||
@@ -178,6 +187,7 @@ impl DetectionPipeline {
|
||||
}
|
||||
|
||||
/// Check if ML pipeline is ready
|
||||
#[cfg(feature = "ml")]
|
||||
pub fn ml_ready(&self) -> bool {
|
||||
self.ml_pipeline.as_ref().is_none_or(|ml| ml.is_ready())
|
||||
}
|
||||
@@ -210,13 +220,23 @@ impl DetectionPipeline {
|
||||
// `buffer` guard dropped here
|
||||
};
|
||||
|
||||
// If ML is enabled and ready, enhance with ML predictions
|
||||
let enhanced_reading = if self.config.enable_ml && self.ml_ready() {
|
||||
// Snapshot the buffer under the lock, then drop the guard before await.
|
||||
let buffer_snapshot = { self.data_buffer.read().clone() };
|
||||
self.enhance_with_ml(reading, &buffer_snapshot).await?
|
||||
} else {
|
||||
reading
|
||||
// If ML is enabled and ready, enhance with ML predictions (only
|
||||
// compiled under the `ml` feature; the base build is signal-only).
|
||||
let enhanced_reading = {
|
||||
#[cfg(feature = "ml")]
|
||||
{
|
||||
if self.config.enable_ml && self.ml_ready() {
|
||||
// Snapshot the buffer under the lock, then drop the guard before await.
|
||||
let buffer_snapshot = { self.data_buffer.read().clone() };
|
||||
self.enhance_with_ml(reading, &buffer_snapshot).await?
|
||||
} else {
|
||||
reading
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "ml"))]
|
||||
{
|
||||
reading
|
||||
}
|
||||
};
|
||||
|
||||
// Check minimum confidence
|
||||
@@ -230,6 +250,7 @@ impl DetectionPipeline {
|
||||
}
|
||||
|
||||
/// Enhance detection results with ML predictions
|
||||
#[cfg(feature = "ml")]
|
||||
async fn enhance_with_ml(
|
||||
&self,
|
||||
traditional_reading: Option<VitalSignsReading>,
|
||||
@@ -262,6 +283,7 @@ impl DetectionPipeline {
|
||||
}
|
||||
|
||||
/// Get the latest ML detection results (if ML is enabled)
|
||||
#[cfg(feature = "ml")]
|
||||
pub async fn get_ml_results(&self) -> Option<MlDetectionResult> {
|
||||
let ml = match &self.ml_pipeline {
|
||||
Some(ml) => ml,
|
||||
@@ -346,6 +368,7 @@ impl DetectionPipeline {
|
||||
self.movement_classifier = MovementClassifier::new(config.movement.clone());
|
||||
|
||||
// Update ML pipeline if configuration changed
|
||||
#[cfg(feature = "ml")]
|
||||
if config.enable_ml != self.config.enable_ml || config.ml_config != self.config.ml_config {
|
||||
self.ml_pipeline = if config.enable_ml {
|
||||
config.ml_config.clone().map(MlDetectionPipeline::new)
|
||||
@@ -358,6 +381,7 @@ impl DetectionPipeline {
|
||||
}
|
||||
|
||||
/// Get the ML pipeline (if enabled)
|
||||
#[cfg(feature = "ml")]
|
||||
pub fn ml_pipeline(&self) -> Option<&MlDetectionPipeline> {
|
||||
self.ml_pipeline.as_ref()
|
||||
}
|
||||
|
||||
@@ -87,6 +87,10 @@ pub mod detection;
|
||||
pub mod domain;
|
||||
pub mod integration;
|
||||
pub mod localization;
|
||||
/// ONNX-backed ML detection. Requires the `ml` feature (pulls
|
||||
/// `wifi-densepose-nn` + `ort`). The core survivor-detection/triage
|
||||
/// pipeline works without it.
|
||||
#[cfg(feature = "ml")]
|
||||
pub mod ml;
|
||||
pub mod tracking;
|
||||
|
||||
@@ -130,6 +134,7 @@ pub use integration::{
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "api")))]
|
||||
pub use api::{create_router, AppState};
|
||||
|
||||
#[cfg(feature = "ml")]
|
||||
pub use ml::{
|
||||
AttenuationPrediction,
|
||||
BreathingClassification,
|
||||
@@ -207,6 +212,7 @@ pub enum MatError {
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// Machine learning error
|
||||
#[cfg(feature = "ml")]
|
||||
#[error("ML error: {0}")]
|
||||
Ml(#[from] ml::MlError),
|
||||
}
|
||||
@@ -592,8 +598,6 @@ pub mod prelude {
|
||||
AssociationResult,
|
||||
BreathingPattern,
|
||||
Coordinates3D,
|
||||
DebrisClassification,
|
||||
DebrisModel,
|
||||
DetectionEvent,
|
||||
DetectionObservation,
|
||||
// Detection
|
||||
@@ -614,11 +618,6 @@ pub mod prelude {
|
||||
// Localization
|
||||
LocalizationService,
|
||||
MatError,
|
||||
MaterialType,
|
||||
// ML types
|
||||
MlDetectionConfig,
|
||||
MlDetectionPipeline,
|
||||
MlDetectionResult,
|
||||
Priority,
|
||||
Result,
|
||||
ScanZone,
|
||||
@@ -631,12 +630,17 @@ pub mod prelude {
|
||||
TrackerConfig,
|
||||
TrackingEvent,
|
||||
TriageStatus,
|
||||
UncertaintyEstimate,
|
||||
VitalSignsClassifier,
|
||||
VitalSignsDetector,
|
||||
VitalSignsReading,
|
||||
ZoneBounds,
|
||||
};
|
||||
|
||||
// ONNX-backed ML types — only when the `ml` feature is enabled.
|
||||
#[cfg(feature = "ml")]
|
||||
pub use crate::{
|
||||
DebrisClassification, DebrisModel, MaterialType, MlDetectionConfig, MlDetectionPipeline,
|
||||
MlDetectionResult, UncertaintyEstimate, VitalSignsClassifier,
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user