diff --git a/python/Cargo.lock b/python/Cargo.lock index 05360457..c9f51a8e 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -4474,7 +4474,6 @@ dependencies = [ "tracing", "uuid", "wifi-densepose-core", - "wifi-densepose-nn", "wifi-densepose-signal", ] diff --git a/v2/crates/wifi-densepose-mat/Cargo.toml b/v2/crates/wifi-densepose-mat/Cargo.toml index 937a3d77..e393371b 100644 --- a/v2/crates/wifi-densepose-mat/Cargo.toml +++ b/v2/crates/wifi-densepose-mat/Cargo.toml @@ -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. diff --git a/v2/crates/wifi-densepose-mat/src/detection/pipeline.rs b/v2/crates/wifi-densepose-mat/src/detection/pipeline.rs index 7229ece3..0402a5b5 100644 --- a/v2/crates/wifi-densepose-mat/src/detection/pipeline.rs +++ b/v2/crates/wifi-densepose-mat/src/detection/pipeline.rs @@ -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, } @@ -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, /// Optional ML detection pipeline + #[cfg(feature = "ml")] ml_pipeline: Option, } 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, @@ -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 { 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() } diff --git a/v2/crates/wifi-densepose-mat/src/lib.rs b/v2/crates/wifi-densepose-mat/src/lib.rs index afb21a72..6a49c10b 100644 --- a/v2/crates/wifi-densepose-mat/src/lib.rs +++ b/v2/crates/wifi-densepose-mat/src/lib.rs @@ -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)]