mirror of
https://github.com/ruvnet/RuView
synced 2026-08-01 19:01:42 +00:00
29de574e63
* docs(research): add RuView beyond-SOTA system review (00) First document of the beyond-SOTA research series: capability audit of the current RuView engine with role-to-crate maturity matrix, ruvsense module inventory, gap analysis, and risk register. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA architecture design (02, in progress) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): finalize beyond-SOTA architecture (02) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add benchmark/validation methodology snapshot (03) https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * docs(research): add beyond-SOTA series index with validation results; changelog README index ties the 5 research docs together with the session's measured validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), and paired pre/post criterion CIR benchmarks. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): precompute CIR warm-start system; hoist tomography solver allocs Exact, determinism-safe optimizations (bit-identical float results): - cir.rs: diag(PhiH Phi)+lambda*I and its CSR matrix depend only on Phi and lambda (fixed at CirEstimator::new) but were rebuilt every frame (O(K*G) pass + CSR allocation). Now built once in new() via build_warm_start_system; summation order unchanged. - tomography.rs: ISTA gradient buffer hoisted out of the 100-iteration loop (fill(0.0) reset) and the Frobenius Lipschitz bound moved from per-reconstruct to construction. Verified: signal 456 tests green; engine 11/11 green including cycle_is_deterministic and witness-stability tests. Criterion paired pre/post: cir_estimate/he40 -3.9% (p<0.01), multiband -1.2/-1.4%. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(worldgraph): bound SemanticState growth with deterministic retention StreamingEngine::process_cycle appended one SemanticState belief per cycle with no eviction — ~1.7M nodes/day at 20 Hz (beyond-SOTA roadmap finding #6). Add WorldGraph::prune_semantic_states(max): deterministic eviction of the oldest beliefs by (valid_from_unix_ms, id); structural nodes (rooms, zones, sensors, anchors, tracks, events) are never eligible. Wire it into the engine after each belief append (DEFAULT_SEMANTIC_RETENTION = 7,200, ~6 min at 20 Hz; set_semantic_retention to tune). The WorldGraph holds current beliefs; durable history is the recorder's job, so no audit data is lost. 3 new tests: end-to-end bounded growth, oldest-only eviction, deterministic equal-timestamp tie-break. Workspace gate: 2,865 passed, 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(sensing-server): route live frames through the governed StreamingEngine Closes the live-trust-path gap (ADR-136 section 8, beyond-SOTA system review): the running server fused live CSI with the bare MultistaticFuser, while the privacy/provenance/witness control plane (ADR-135..146) only ever ran on synthetic in-test frames. The privacy control plane was therefore bypassable on the real path. New engine_bridge module drives StreamingEngine::process_cycle from the server's live NodeState map, reusing the existing NodeState -> MultiBandCsiFrame conversion. It lazily wires each contributing node as a WorldGraph sensor (idempotent), bounds belief growth via the retention cap, and forwards explicit timestamps/calibration ids so the path stays deterministic and replayable. Wired additively into both live ESP32/WiFi fusion sites in main.rs via a split-borrow off the write guard, so person-count behavior is unchanged; the latest BLAKE3 witness is stored on AppState. Every published belief now carries evidence + model + calibration + privacy decision and a deterministic witness. Adds wifi-densepose-engine/-worldgraph/-bfld/-geo deps. 6 new bridge tests (witnessed belief with full provenance, cross-run determinism, idempotent node registration, retention bound, privacy-mode propagation). sensing-server suite 430+128 green; workspace gate 2,904 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(train): falsifiable occupancy benchmark with anti-overfitting gate Makes the presence/person-count "beyond SOTA" claim falsifiable in code instead of aspirational (the unfalsifiability gap from the beyond-SOTA system review). occupancy_bench grades predictions vs ground truth and gates a SOTA claim behind one claim_allowed invariant requiring ALL of: - DataProvenance::Measured — synthetic/mock data is scorable for regression but never claimable (anti-mock-contamination; the CLAUDE.md Kconfig-bug lesson made structural). - A leak-free EvalSplit — validate() refuses any split where a subject OR environment id appears in both train and test (subject leakage / per-environment overfitting). - n_test >= min_test_samples (small-N guard). - Presence F1 whose bootstrap-CI lower bound (deterministic seeded splitmix64) clears the threshold — not the point estimate. - Count MAE within threshold. The claim string is unreadable except through the gate (NO_CLAIM otherwise), same discipline as the ruview-gamma acceptance gate. What remains is data, not method: a frozen, SHA-pinned, subject/environment-disjoint measured replay set turns the claim into a passing/failing test. Lives in wifi-densepose-train (the eval bounded context, alongside ablation/ eval/metrics). 10 tests cover each refusal path; warning-clean under the crate's missing_docs lint. Workspace gate 2,914 passed / 0 failed. Doc 03 updated. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): per-room adapter provenance + drift-to-recalibration advisor Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 section 3.4) could silently change inference without the witness noticing: provenance carried only "rfenc-v<N>" with no notion of adapter identity. - StreamingEngine::set_room_adapter(AdapterInfo): pins the adapter's content-derived id into provenance model_version ("rfenc-v1+adapter:<id>") — and therefore into the BLAKE3 witness — so swapping or clearing adapter weights always shifts the witness. Engine test proves base -> adapter -> other-adapter -> cleared all witness differently and cleared == base. - RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline / refitting the room adapter on sustained low fusion coherence (streak threshold, default 60 cycles ~ 3 s at 20 Hz) or an ADR-142 change-point. Surfaced as TrustedOutput::recalibration_recommended, stored on the sensing-server AppState alongside the witness at both live fusion sites. - Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} + live-path test that the adapter id flows into the live witness. Scope note (honest): this is the deployable provenance/trigger half of the "retrained model" roadmap item. Fitting the adapter itself runs in the existing external calibration service (aether-arena/calibration/); a trained RF-encoder checkpoint still does not exist in-tree. Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix(mat): gate api module behind its feature — standalone no-default-features builds pub mod api was unconditional while its only dependency, serde, is optional behind the 'api' feature, so any build without default features failed with 101 unresolved-serde errors (masked in --workspace runs by feature unification). The api module and its create_router/AppState re-export are now cfg(feature = "api")-gated with docsrs annotations. All combos compile: bare --no-default-features (was 101 errors, now 0), --no-default-features --features api, and full default (177 tests pass). Workspace gate: 2,918 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * perf(signal): opt-in FFT operator for the CIR ISTA solver (8-14x measured) Phi is a sub-DFT, so each ISTA mat-vec can run as one length-G FFT (O(G log G)) instead of a dense O(K*G) product — the dominant-latency-hazard finding from the beyond-SOTA optimization roadmap. New CirConfig::fft_operator, default FALSE: the dense path stays the bit-exact witness default. The FFT evaluates the same sums in a different order, so enabling it shifts float results in the last bits and requires regenerating any pinned witness — strictly opt-in per deployment. FftOperator (rustfft, planned once at CirEstimator::new, scratch buffers reused across the ISTA loop) dispatches inside ista_solve: Phi x = scale * forward-FFT(x) sampled at bins (k_idx mod G) Phi^H v = scale * unnormalised inverse-FFT of v scattered into those bins Warm-start and Lipschitz estimation stay dense at construction. Measured (criterion, same run, same machine): ht20: 2.22 ms -> 265 us (8.4x) ht40: 10.26 ms -> 717 us (14.3x) The real HE40 grid (K=484, G=1452) scales further per the O(K*G)/O(G log G) ratio. 3 new tests: FFT<->dense matvec equivalence to float tolerance on ht20 and he40 grids; end-to-end dominant-tap agreement on a single-path frame; all default configs keep FFT off. New cir_estimate_fft bench group. Workspace gate: 2,921 passed / 0 failed (default path bit-exact, witnesses unchanged). https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(core): canonical frame decoder — capture-to-claim replay (ADR-136) The encode half of the ADR-136 frame contract existed (ComplexSample, to_canonical_bytes, witness_hash) but there was no decoder: a captured canonical frame could be witnessed but never reconstructed, blocking replay-from-capture. CsiFrame::from_canonical_bytes is the exact inverse: same id, metadata, complex payload, and witness hash (tested as the round-trip law AC7 — the replayed frame re-encodes byte-identically). Amplitude/phase are recomputed from the payload (projections, not independent state). Every malformed-input class fails closed (AC8): header truncation -> Truncated, payload truncation -> PayloadMismatch, unknown discriminants, non-UTF-8 device id, trailing bytes. Nil calibration uuid decodes as None per the documented encoding. Core: 36 tests pass. Workspace gate: 2,937 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): dynamic min-cut mesh partition guard (ruvector-mincut) Maintains an exact min-cut over the live mesh coupling graph — nodes are sensing nodes, coupling is the product of fusion attention weights — and surfaces per cycle, as TrustedOutput::mesh: - cut value: the global "how close is the array to partitioning" number, a structural measure per-node heuristics miss; - weak side: which specific nodes would split off (failure/jamming triage, feeds ADR-032 posture); - at-risk flag: counts as a structural event for the drift->recalibration advisor (alongside ADR-142 change-points). Degenerate cases fail toward risk: a node with zero coupling is reported as already partitioned (cut 0, that node as the weak side). Measured cost policy (criterion, 12-node mesh — the honest part): - weights quantized (1/64) + change-gated: steady-state cycles do ZERO graph work and reuse the cached cut (~7.3 us, ~23x cheaper than building); - on any real change a full exact rebuild (~171 us) is used, because ONE DynamicMinCut delete+insert measured ~240 us — the subpolynomial machinery amortizes on much larger graphs, so rebuild-on-change is the measured optimum at mesh scale (one-edge case -28% after switching policy); - full process_cycle with the guard: ~33 us for 4 nodes vs the 50 ms budget. 9 mesh_guard tests (weak-node detection, steady-state zero updates, sub-quantum gating, join/drop rebuild, determinism, disconnection) + an engine-level wiring test (down-weighted node -> weak side -> recalibration). Engine 24 tests; workspace gate 2,946 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * feat(engine): mesh partition risk demotes privacy + enters the witness (ADR-032) Completes the mesh-guard integration: its at_risk signal was advisory-only (fed the recalibration advisor). It now also contributes to the ADR-141 privacy demotion alongside fusion- and array-level contradictions — a mesh close to partitioning makes the fused belief less trustworthy, so the cycle emits at a more restricted class (monotonic; information only removed). Because effective_class feeds the BLAKE3 witness, a fragmenting array now shifts the witness: partition risk is auditable, not just logged. The mesh computation moved ahead of the demotion step in process_cycle; mesh_guard_mut exposes risk-threshold tuning. Test: a forced-risk 3-node cycle demotes PrivateHome Anonymous->Restricted and shifts the witness vs a clean baseline. Engine 25 tests; workspace gate 2,947 passed / 0 failed. https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH * fix: public-PR review findings — privacy-path honesty, gate holes, mesh-guard cliff - sensing-server: engine errors logged+counted (no silent swallow), trust state exposed via status surface, privacy-demotion claims aligned with the actual parallel-audit-path behavior - occupancy_bench: vacuous-F1 hole closed (degenerate test sets fail with their own criterion); CI-lower-bound test made probative - mesh_guard: quantization scaled to observed coupling range — >=65-node balanced meshes no longer permanently at_risk (regression test) - engine: both wiring tests made probative (same-topology witness compare, deterministic risk-crossing fixture) - mat: axum/tokio optional behind api; real serde feature (api enables it) - core: canonical decoder strict (non-zero reserved bytes and nil UUID rejected — injective on accepted domain, forged-bytes tests) - CHANGELOG: un-spliced the FFT/adapter bullet mangle Co-Authored-By: claude-flow <ruv@ruv.net> * chore: strip private-track references for public PR Reword the occupancy-benchmark changelog bullet to drop a cross-reference to the private research track, and restore the WorldGraph retention bullet header that was glued onto the preceding MAT bullet. Co-Authored-By: claude-flow <ruv@ruv.net> * chore: lockfile refresh for cherry-picked feature set Co-Authored-By: claude-flow <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com>
677 lines
22 KiB
Rust
677 lines
22 KiB
Rust
//! # WiFi-DensePose MAT (Mass Casualty Assessment Tool)
|
|
//!
|
|
//! A modular extension for WiFi-based disaster survivor detection and localization.
|
|
//!
|
|
//! This crate provides capabilities for detecting human survivors trapped in rubble,
|
|
//! debris, or collapsed structures using WiFi Channel State Information (CSI) analysis.
|
|
//!
|
|
//! ## Features
|
|
//!
|
|
//! - **Vital Signs Detection**: Breathing patterns, heartbeat signatures, and movement
|
|
//! - **Survivor Localization**: 3D position estimation through debris
|
|
//! - **Triage Classification**: Automatic START protocol-compatible triage
|
|
//! - **Real-time Alerting**: Priority-based alert generation and dispatch
|
|
//!
|
|
//! ## Use Cases
|
|
//!
|
|
//! - Earthquake search and rescue
|
|
//! - Building collapse response
|
|
//! - Avalanche victim location
|
|
//! - Flood rescue operations
|
|
//! - Mine collapse detection
|
|
//!
|
|
//! ## Architecture
|
|
//!
|
|
//! The crate follows Domain-Driven Design (DDD) principles with clear bounded contexts:
|
|
//!
|
|
//! ```text
|
|
//! ┌─────────────────────────────────────────────────────────┐
|
|
//! │ wifi-densepose-mat │
|
|
//! ├─────────────────────────────────────────────────────────┤
|
|
//! │ ┌───────────┐ ┌─────────────┐ ┌─────────────────┐ │
|
|
//! │ │ Detection │ │Localization │ │ Alerting │ │
|
|
//! │ │ Context │ │ Context │ │ Context │ │
|
|
//! │ └─────┬─────┘ └──────┬──────┘ └────────┬────────┘ │
|
|
//! │ └───────────────┼──────────────────┘ │
|
|
//! │ │ │
|
|
//! │ ┌─────────▼─────────┐ │
|
|
//! │ │ Integration │ │
|
|
//! │ │ Layer │ │
|
|
//! │ └───────────────────┘ │
|
|
//! └─────────────────────────────────────────────────────────┘
|
|
//! ```
|
|
//!
|
|
//! ## Example
|
|
//!
|
|
//! ```rust,no_run
|
|
//! use wifi_densepose_mat::{
|
|
//! DisasterResponse, DisasterConfig, DisasterType,
|
|
//! ScanZone, ZoneBounds,
|
|
//! };
|
|
//!
|
|
//! #[tokio::main]
|
|
//! async fn main() -> anyhow::Result<()> {
|
|
//! // Initialize disaster response system
|
|
//! let config = DisasterConfig::builder()
|
|
//! .disaster_type(DisasterType::Earthquake)
|
|
//! .sensitivity(0.8)
|
|
//! .build();
|
|
//!
|
|
//! let mut response = DisasterResponse::new(config);
|
|
//!
|
|
//! // Define scan zone
|
|
//! let zone = ScanZone::new(
|
|
//! "Building A - North Wing",
|
|
//! ZoneBounds::rectangle(0.0, 0.0, 50.0, 30.0),
|
|
//! );
|
|
//! response.add_zone(zone)?;
|
|
//!
|
|
//! // Start scanning
|
|
//! response.start_scanning().await?;
|
|
//!
|
|
//! Ok(())
|
|
//! }
|
|
//! ```
|
|
|
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
#![warn(missing_docs)]
|
|
#![warn(rustdoc::missing_crate_level_docs)]
|
|
|
|
pub mod alerting;
|
|
/// REST API surface (Axum). Requires the `api` feature — its DTOs derive
|
|
/// serde, which is an optional dependency gated behind that feature.
|
|
#[cfg(feature = "api")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "api")))]
|
|
pub mod api;
|
|
pub mod detection;
|
|
pub mod domain;
|
|
pub mod integration;
|
|
pub mod localization;
|
|
pub mod ml;
|
|
pub mod tracking;
|
|
|
|
// Re-export main types
|
|
pub use domain::{
|
|
alert::{Alert, AlertId, AlertPayload, Priority},
|
|
coordinates::{Coordinates3D, DepthEstimate, LocationUncertainty},
|
|
disaster_event::{DisasterEvent, DisasterEventId, DisasterType, EventStatus},
|
|
events::{
|
|
AlertEvent, DetectionEvent, DomainEvent, EventStore, InMemoryEventStore, TrackingEvent,
|
|
},
|
|
scan_zone::{ScanParameters, ScanZone, ScanZoneId, ZoneBounds, ZoneStatus},
|
|
survivor::{Survivor, SurvivorId, SurvivorMetadata, SurvivorStatus},
|
|
triage::{TriageCalculator, TriageStatus},
|
|
vital_signs::{
|
|
BreathingPattern, BreathingType, HeartbeatSignature, MovementProfile, MovementType,
|
|
VitalSignsReading,
|
|
},
|
|
};
|
|
|
|
pub use detection::{
|
|
BreathingDetector, BreathingDetectorConfig, DetectionConfig, DetectionPipeline,
|
|
EnsembleClassifier, EnsembleConfig, EnsembleResult, HeartbeatDetector, HeartbeatDetectorConfig,
|
|
MovementClassifier, MovementClassifierConfig, VitalSignsDetector,
|
|
};
|
|
|
|
pub use localization::{
|
|
DepthEstimator, DepthEstimatorConfig, LocalizationService, PositionFuser, TriangulationConfig,
|
|
Triangulator,
|
|
};
|
|
|
|
pub use alerting::{
|
|
AlertConfig, AlertDispatcher, AlertGenerator, PriorityCalculator, TriageService,
|
|
};
|
|
|
|
pub use integration::{
|
|
AdapterError, HardwareAdapter, IntegrationConfig, NeuralAdapter, SignalAdapter,
|
|
};
|
|
|
|
#[cfg(feature = "api")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "api")))]
|
|
pub use api::{create_router, AppState};
|
|
|
|
pub use ml::{
|
|
AttenuationPrediction,
|
|
BreathingClassification,
|
|
ClassifierOutput,
|
|
DebrisClassification,
|
|
DebrisFeatureExtractor,
|
|
DebrisFeatures,
|
|
DebrisModel,
|
|
DebrisModelConfig,
|
|
// Debris penetration model
|
|
DebrisPenetrationModel,
|
|
DepthEstimate as MlDepthEstimate,
|
|
HeartbeatClassification,
|
|
MaterialType,
|
|
MlDetectionConfig,
|
|
MlDetectionPipeline,
|
|
MlDetectionResult,
|
|
// Core ML types
|
|
MlError,
|
|
MlResult,
|
|
UncertaintyEstimate,
|
|
// Vital signs classifier
|
|
VitalSignsClassifier,
|
|
VitalSignsClassifierConfig,
|
|
};
|
|
|
|
pub use tracking::{
|
|
AssociationResult, CsiFingerprint, DetectionObservation, KalmanState, SurvivorTracker, TrackId,
|
|
TrackLifecycle, TrackState, TrackedSurvivor, TrackerConfig,
|
|
};
|
|
|
|
/// Library version
|
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
|
|
/// Common result type for MAT operations
|
|
pub type Result<T> = std::result::Result<T, MatError>;
|
|
|
|
/// Unified error type for MAT operations
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum MatError {
|
|
/// Detection error
|
|
#[error("Detection error: {0}")]
|
|
Detection(String),
|
|
|
|
/// Localization error
|
|
#[error("Localization error: {0}")]
|
|
Localization(String),
|
|
|
|
/// Alerting error
|
|
#[error("Alerting error: {0}")]
|
|
Alerting(String),
|
|
|
|
/// Integration error
|
|
#[error("Integration error: {0}")]
|
|
Integration(#[from] AdapterError),
|
|
|
|
/// Configuration error
|
|
#[error("Configuration error: {0}")]
|
|
Config(String),
|
|
|
|
/// Domain invariant violation
|
|
#[error("Domain error: {0}")]
|
|
Domain(String),
|
|
|
|
/// Repository error
|
|
#[error("Repository error: {0}")]
|
|
Repository(String),
|
|
|
|
/// Signal processing error
|
|
#[error("Signal processing error: {0}")]
|
|
Signal(#[from] wifi_densepose_signal::SignalError),
|
|
|
|
/// I/O error
|
|
#[error("I/O error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
|
|
/// Machine learning error
|
|
#[error("ML error: {0}")]
|
|
Ml(#[from] ml::MlError),
|
|
}
|
|
|
|
/// Configuration for the disaster response system
|
|
#[derive(Debug, Clone)]
|
|
pub struct DisasterConfig {
|
|
/// Type of disaster event
|
|
pub disaster_type: DisasterType,
|
|
/// Detection sensitivity (0.0-1.0)
|
|
pub sensitivity: f64,
|
|
/// Minimum confidence threshold for survivor detection
|
|
pub confidence_threshold: f64,
|
|
/// Maximum depth to scan (meters)
|
|
pub max_depth: f64,
|
|
/// Scan interval in milliseconds
|
|
pub scan_interval_ms: u64,
|
|
/// Enable continuous monitoring
|
|
pub continuous_monitoring: bool,
|
|
/// Alert configuration
|
|
pub alert_config: AlertConfig,
|
|
}
|
|
|
|
impl Default for DisasterConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
disaster_type: DisasterType::Unknown,
|
|
sensitivity: 0.8,
|
|
confidence_threshold: 0.5,
|
|
max_depth: 5.0,
|
|
scan_interval_ms: 500,
|
|
continuous_monitoring: true,
|
|
alert_config: AlertConfig::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DisasterConfig {
|
|
/// Create a new configuration builder
|
|
pub fn builder() -> DisasterConfigBuilder {
|
|
DisasterConfigBuilder::default()
|
|
}
|
|
}
|
|
|
|
/// Builder for DisasterConfig
|
|
#[derive(Debug, Default)]
|
|
pub struct DisasterConfigBuilder {
|
|
config: DisasterConfig,
|
|
}
|
|
|
|
impl DisasterConfigBuilder {
|
|
/// Set disaster type
|
|
pub fn disaster_type(mut self, disaster_type: DisasterType) -> Self {
|
|
self.config.disaster_type = disaster_type;
|
|
self
|
|
}
|
|
|
|
/// Set detection sensitivity
|
|
pub fn sensitivity(mut self, sensitivity: f64) -> Self {
|
|
self.config.sensitivity = sensitivity.clamp(0.0, 1.0);
|
|
self
|
|
}
|
|
|
|
/// Set confidence threshold
|
|
pub fn confidence_threshold(mut self, threshold: f64) -> Self {
|
|
self.config.confidence_threshold = threshold.clamp(0.0, 1.0);
|
|
self
|
|
}
|
|
|
|
/// Set maximum scan depth
|
|
pub fn max_depth(mut self, depth: f64) -> Self {
|
|
self.config.max_depth = depth.max(0.0);
|
|
self
|
|
}
|
|
|
|
/// Set scan interval
|
|
pub fn scan_interval_ms(mut self, interval: u64) -> Self {
|
|
self.config.scan_interval_ms = interval.max(100);
|
|
self
|
|
}
|
|
|
|
/// Enable/disable continuous monitoring
|
|
pub fn continuous_monitoring(mut self, enabled: bool) -> Self {
|
|
self.config.continuous_monitoring = enabled;
|
|
self
|
|
}
|
|
|
|
/// Build the configuration
|
|
pub fn build(self) -> DisasterConfig {
|
|
self.config
|
|
}
|
|
}
|
|
|
|
/// Main disaster response coordinator
|
|
pub struct DisasterResponse {
|
|
config: DisasterConfig,
|
|
event: Option<DisasterEvent>,
|
|
detection_pipeline: DetectionPipeline,
|
|
localization_service: LocalizationService,
|
|
alert_dispatcher: AlertDispatcher,
|
|
event_store: std::sync::Arc<dyn domain::events::EventStore>,
|
|
ensemble_classifier: EnsembleClassifier,
|
|
tracker: tracking::SurvivorTracker,
|
|
running: std::sync::atomic::AtomicBool,
|
|
}
|
|
|
|
impl DisasterResponse {
|
|
/// Create a new disaster response system
|
|
pub fn new(config: DisasterConfig) -> Self {
|
|
let detection_config = DetectionConfig::from_disaster_config(&config);
|
|
let detection_pipeline = DetectionPipeline::new(detection_config);
|
|
|
|
let localization_service = LocalizationService::new();
|
|
let alert_dispatcher = AlertDispatcher::new(config.alert_config.clone());
|
|
let event_store: std::sync::Arc<dyn domain::events::EventStore> =
|
|
std::sync::Arc::new(InMemoryEventStore::new());
|
|
let ensemble_classifier = EnsembleClassifier::new(EnsembleConfig::default());
|
|
|
|
Self {
|
|
config,
|
|
event: None,
|
|
detection_pipeline,
|
|
localization_service,
|
|
alert_dispatcher,
|
|
event_store,
|
|
ensemble_classifier,
|
|
tracker: tracking::SurvivorTracker::with_defaults(),
|
|
running: std::sync::atomic::AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
/// Create with a custom event store (e.g. for persistence or testing)
|
|
pub fn with_event_store(
|
|
config: DisasterConfig,
|
|
event_store: std::sync::Arc<dyn domain::events::EventStore>,
|
|
) -> Self {
|
|
let detection_config = DetectionConfig::from_disaster_config(&config);
|
|
let detection_pipeline = DetectionPipeline::new(detection_config);
|
|
let localization_service = LocalizationService::new();
|
|
let alert_dispatcher = AlertDispatcher::new(config.alert_config.clone());
|
|
let ensemble_classifier = EnsembleClassifier::new(EnsembleConfig::default());
|
|
|
|
Self {
|
|
config,
|
|
event: None,
|
|
detection_pipeline,
|
|
localization_service,
|
|
alert_dispatcher,
|
|
event_store,
|
|
ensemble_classifier,
|
|
tracker: tracking::SurvivorTracker::with_defaults(),
|
|
running: std::sync::atomic::AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
/// Push CSI data into the detection pipeline for processing.
|
|
///
|
|
/// This is the primary data ingestion point. Call this with real CSI
|
|
/// amplitude and phase readings from hardware (ESP32, Intel 5300, etc).
|
|
/// Returns an error string if data is invalid.
|
|
pub fn push_csi_data(&self, amplitudes: &[f64], phases: &[f64]) -> Result<()> {
|
|
if amplitudes.len() != phases.len() {
|
|
return Err(MatError::Detection(
|
|
"Amplitude and phase arrays must have equal length".into(),
|
|
));
|
|
}
|
|
if amplitudes.is_empty() {
|
|
return Err(MatError::Detection("CSI data cannot be empty".into()));
|
|
}
|
|
self.detection_pipeline.add_data(amplitudes, phases);
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the event store for querying domain events
|
|
pub fn event_store(&self) -> &std::sync::Arc<dyn domain::events::EventStore> {
|
|
&self.event_store
|
|
}
|
|
|
|
/// Get the ensemble classifier
|
|
pub fn ensemble_classifier(&self) -> &EnsembleClassifier {
|
|
&self.ensemble_classifier
|
|
}
|
|
|
|
/// Get the detection pipeline (for direct buffer inspection / data push)
|
|
pub fn detection_pipeline(&self) -> &DetectionPipeline {
|
|
&self.detection_pipeline
|
|
}
|
|
|
|
/// Get the survivor tracker
|
|
pub fn tracker(&self) -> &tracking::SurvivorTracker {
|
|
&self.tracker
|
|
}
|
|
|
|
/// Get mutable access to the tracker (for integration in scan_cycle)
|
|
pub fn tracker_mut(&mut self) -> &mut tracking::SurvivorTracker {
|
|
&mut self.tracker
|
|
}
|
|
|
|
/// Initialize a new disaster event
|
|
pub fn initialize_event(
|
|
&mut self,
|
|
location: geo::Point<f64>,
|
|
description: &str,
|
|
) -> Result<&DisasterEvent> {
|
|
let event = DisasterEvent::new(self.config.disaster_type.clone(), location, description);
|
|
self.event = Some(event);
|
|
self.event
|
|
.as_ref()
|
|
.ok_or_else(|| MatError::Domain("Failed to create event".into()))
|
|
}
|
|
|
|
/// Add a scan zone to the current event
|
|
pub fn add_zone(&mut self, zone: ScanZone) -> Result<()> {
|
|
let event = self
|
|
.event
|
|
.as_mut()
|
|
.ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
|
|
event.add_zone(zone);
|
|
Ok(())
|
|
}
|
|
|
|
/// Start the scanning process
|
|
pub async fn start_scanning(&mut self) -> Result<()> {
|
|
use std::sync::atomic::Ordering;
|
|
|
|
self.running.store(true, Ordering::SeqCst);
|
|
|
|
while self.running.load(Ordering::SeqCst) {
|
|
self.scan_cycle().await?;
|
|
|
|
if !self.config.continuous_monitoring {
|
|
break;
|
|
}
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(
|
|
self.config.scan_interval_ms,
|
|
))
|
|
.await;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stop the scanning process
|
|
pub fn stop_scanning(&self) {
|
|
use std::sync::atomic::Ordering;
|
|
self.running.store(false, Ordering::SeqCst);
|
|
}
|
|
|
|
/// Execute a single scan cycle.
|
|
///
|
|
/// Processes all active zones, runs detection pipeline on buffered CSI data,
|
|
/// applies ensemble classification, emits domain events to the EventStore,
|
|
/// and dispatches alerts for newly detected survivors.
|
|
async fn scan_cycle(&mut self) -> Result<()> {
|
|
let scan_start = std::time::Instant::now();
|
|
|
|
// Collect detections first to avoid borrowing issues
|
|
let mut detections = Vec::new();
|
|
|
|
{
|
|
let event = self
|
|
.event
|
|
.as_ref()
|
|
.ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
|
|
|
|
for zone in event.zones() {
|
|
if zone.status() != &ZoneStatus::Active {
|
|
continue;
|
|
}
|
|
|
|
// Process buffered CSI data through the detection pipeline
|
|
let detection_result = self.detection_pipeline.process_zone(zone).await?;
|
|
|
|
if let Some(vital_signs) = detection_result {
|
|
// Run ensemble classifier to combine breathing + heartbeat + movement
|
|
let ensemble_result = self.ensemble_classifier.classify(&vital_signs);
|
|
|
|
// Only proceed if ensemble confidence meets threshold
|
|
if ensemble_result.confidence >= self.config.confidence_threshold {
|
|
// Attempt localization
|
|
let location = self
|
|
.localization_service
|
|
.estimate_position(&vital_signs, zone);
|
|
|
|
detections.push((
|
|
zone.id().clone(),
|
|
zone.name().to_string(),
|
|
vital_signs,
|
|
location,
|
|
ensemble_result,
|
|
));
|
|
}
|
|
}
|
|
|
|
// Emit zone scan completed event
|
|
let scan_duration = scan_start.elapsed();
|
|
let _ = self.event_store.append(DomainEvent::Zone(
|
|
domain::events::ZoneEvent::ZoneScanCompleted {
|
|
zone_id: zone.id().clone(),
|
|
detections_found: detections.len() as u32,
|
|
scan_duration_ms: scan_duration.as_millis() as u64,
|
|
timestamp: chrono::Utc::now(),
|
|
},
|
|
));
|
|
}
|
|
}
|
|
|
|
// Now process detections with mutable access
|
|
let event = self
|
|
.event
|
|
.as_mut()
|
|
.ok_or_else(|| MatError::Domain("No active disaster event".into()))?;
|
|
|
|
for (zone_id, _zone_name, vital_signs, location, _ensemble) in detections {
|
|
let survivor =
|
|
event.record_detection(zone_id.clone(), vital_signs.clone(), location.clone())?;
|
|
|
|
// Emit SurvivorDetected domain event
|
|
let _ =
|
|
self.event_store
|
|
.append(DomainEvent::Detection(DetectionEvent::SurvivorDetected {
|
|
survivor_id: survivor.id().clone(),
|
|
zone_id,
|
|
vital_signs,
|
|
location,
|
|
timestamp: chrono::Utc::now(),
|
|
}));
|
|
|
|
// Generate and dispatch alert if needed
|
|
if survivor.should_alert() {
|
|
let alert = self.alert_dispatcher.generate_alert(survivor)?;
|
|
let alert_id = alert.id().clone();
|
|
let priority = alert.priority();
|
|
let survivor_id = alert.survivor_id().clone();
|
|
|
|
// Emit AlertGenerated domain event
|
|
let _ = self
|
|
.event_store
|
|
.append(DomainEvent::Alert(AlertEvent::AlertGenerated {
|
|
alert_id,
|
|
survivor_id,
|
|
priority,
|
|
timestamp: chrono::Utc::now(),
|
|
}));
|
|
|
|
self.alert_dispatcher.dispatch(alert).await?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the current disaster event
|
|
pub fn event(&self) -> Option<&DisasterEvent> {
|
|
self.event.as_ref()
|
|
}
|
|
|
|
/// Get all detected survivors
|
|
pub fn survivors(&self) -> Vec<&Survivor> {
|
|
self.event
|
|
.as_ref()
|
|
.map(|e| e.survivors())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Get survivors by triage status
|
|
pub fn survivors_by_triage(&self, status: TriageStatus) -> Vec<&Survivor> {
|
|
self.survivors()
|
|
.into_iter()
|
|
.filter(|s| s.triage_status() == &status)
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// Prelude module for convenient imports
|
|
pub mod prelude {
|
|
pub use crate::{
|
|
Alert,
|
|
// Alerting
|
|
AlertDispatcher,
|
|
AlertEvent,
|
|
AssociationResult,
|
|
BreathingPattern,
|
|
Coordinates3D,
|
|
DebrisClassification,
|
|
DebrisModel,
|
|
DetectionEvent,
|
|
DetectionObservation,
|
|
// Detection
|
|
DetectionPipeline,
|
|
DisasterConfig,
|
|
DisasterConfigBuilder,
|
|
DisasterEvent,
|
|
DisasterResponse,
|
|
DisasterType,
|
|
// Event sourcing
|
|
DomainEvent,
|
|
EnsembleClassifier,
|
|
EnsembleConfig,
|
|
EnsembleResult,
|
|
EventStore,
|
|
HeartbeatSignature,
|
|
InMemoryEventStore,
|
|
// Localization
|
|
LocalizationService,
|
|
MatError,
|
|
MaterialType,
|
|
// ML types
|
|
MlDetectionConfig,
|
|
MlDetectionPipeline,
|
|
MlDetectionResult,
|
|
Priority,
|
|
Result,
|
|
ScanZone,
|
|
// Domain types
|
|
Survivor,
|
|
SurvivorId,
|
|
// Tracking
|
|
SurvivorTracker,
|
|
TrackId,
|
|
TrackerConfig,
|
|
TrackingEvent,
|
|
TriageStatus,
|
|
UncertaintyEstimate,
|
|
VitalSignsClassifier,
|
|
VitalSignsDetector,
|
|
VitalSignsReading,
|
|
ZoneBounds,
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_config_builder() {
|
|
let config = DisasterConfig::builder()
|
|
.disaster_type(DisasterType::Earthquake)
|
|
.sensitivity(0.9)
|
|
.confidence_threshold(0.6)
|
|
.max_depth(10.0)
|
|
.build();
|
|
|
|
assert!(matches!(config.disaster_type, DisasterType::Earthquake));
|
|
assert!((config.sensitivity - 0.9).abs() < f64::EPSILON);
|
|
assert!((config.confidence_threshold - 0.6).abs() < f64::EPSILON);
|
|
assert!((config.max_depth - 10.0).abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_sensitivity_clamping() {
|
|
let config = DisasterConfig::builder().sensitivity(1.5).build();
|
|
|
|
assert!((config.sensitivity - 1.0).abs() < f64::EPSILON);
|
|
|
|
let config = DisasterConfig::builder().sensitivity(-0.5).build();
|
|
|
|
assert!(config.sensitivity.abs() < f64::EPSILON);
|
|
}
|
|
|
|
#[test]
|
|
fn test_version() {
|
|
assert!(VERSION.contains('.'), "VERSION should be a semver string");
|
|
}
|
|
}
|