feat: Add wifi-densepose-mat disaster detection module
Implements WiFi-Mat (Mass Casualty Assessment Tool) for detecting and localizing survivors trapped in rubble, earthquakes, and natural disasters. Architecture: - Domain-Driven Design with bounded contexts (Detection, Localization, Alerting) - Modular Rust crate integrating with existing wifi-densepose-* crates - Event-driven architecture for audit trails and distributed deployments Features: - Breathing pattern detection from CSI amplitude variations - Heartbeat detection using micro-Doppler analysis - Movement classification (gross, fine, tremor, periodic) - START protocol-compatible triage classification - 3D position estimation via triangulation and depth estimation - Real-time alert generation with priority escalation Documentation: - ADR-001: Architecture Decision Record for wifi-Mat - DDD domain model specification
This commit is contained in:
@@ -0,0 +1,274 @@
|
||||
//! Movement classification from CSI signal variations.
|
||||
|
||||
use crate::domain::{MovementProfile, MovementType};
|
||||
|
||||
/// Configuration for movement classification
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MovementClassifierConfig {
|
||||
/// Threshold for detecting any movement
|
||||
pub movement_threshold: f64,
|
||||
/// Threshold for gross movement
|
||||
pub gross_movement_threshold: f64,
|
||||
/// Window size for variance calculation
|
||||
pub window_size: usize,
|
||||
/// Threshold for periodic movement detection
|
||||
pub periodicity_threshold: f64,
|
||||
}
|
||||
|
||||
impl Default for MovementClassifierConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
movement_threshold: 0.1,
|
||||
gross_movement_threshold: 0.5,
|
||||
window_size: 100,
|
||||
periodicity_threshold: 0.3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Classifier for movement types from CSI signals
|
||||
pub struct MovementClassifier {
|
||||
config: MovementClassifierConfig,
|
||||
}
|
||||
|
||||
impl MovementClassifier {
|
||||
/// Create a new movement classifier
|
||||
pub fn new(config: MovementClassifierConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
/// Create with default configuration
|
||||
pub fn with_defaults() -> Self {
|
||||
Self::new(MovementClassifierConfig::default())
|
||||
}
|
||||
|
||||
/// Classify movement from CSI signal
|
||||
pub fn classify(&self, csi_signal: &[f64], sample_rate: f64) -> MovementProfile {
|
||||
if csi_signal.len() < self.config.window_size {
|
||||
return MovementProfile::default();
|
||||
}
|
||||
|
||||
// Calculate signal statistics
|
||||
let variance = self.calculate_variance(csi_signal);
|
||||
let max_change = self.calculate_max_change(csi_signal);
|
||||
let periodicity = self.calculate_periodicity(csi_signal, sample_rate);
|
||||
|
||||
// Determine movement type
|
||||
let (movement_type, is_voluntary) = self.determine_movement_type(
|
||||
variance,
|
||||
max_change,
|
||||
periodicity,
|
||||
);
|
||||
|
||||
// Calculate intensity
|
||||
let intensity = self.calculate_intensity(variance, max_change);
|
||||
|
||||
// Calculate frequency of movement
|
||||
let frequency = self.calculate_movement_frequency(csi_signal, sample_rate);
|
||||
|
||||
MovementProfile {
|
||||
movement_type,
|
||||
intensity,
|
||||
frequency,
|
||||
is_voluntary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate signal variance
|
||||
fn calculate_variance(&self, signal: &[f64]) -> f64 {
|
||||
if signal.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
|
||||
let variance = signal.iter()
|
||||
.map(|x| (x - mean).powi(2))
|
||||
.sum::<f64>() / signal.len() as f64;
|
||||
|
||||
variance
|
||||
}
|
||||
|
||||
/// Calculate maximum change in signal
|
||||
fn calculate_max_change(&self, signal: &[f64]) -> f64 {
|
||||
if signal.len() < 2 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
signal.windows(2)
|
||||
.map(|w| (w[1] - w[0]).abs())
|
||||
.fold(0.0, f64::max)
|
||||
}
|
||||
|
||||
/// Calculate periodicity score using autocorrelation
|
||||
fn calculate_periodicity(&self, signal: &[f64], _sample_rate: f64) -> f64 {
|
||||
if signal.len() < 3 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Calculate autocorrelation
|
||||
let n = signal.len();
|
||||
let mean = signal.iter().sum::<f64>() / n as f64;
|
||||
let centered: Vec<f64> = signal.iter().map(|x| x - mean).collect();
|
||||
|
||||
let variance: f64 = centered.iter().map(|x| x * x).sum();
|
||||
if variance == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Find first peak in autocorrelation after lag 0
|
||||
let max_lag = n / 2;
|
||||
let mut max_corr = 0.0;
|
||||
|
||||
for lag in 1..max_lag {
|
||||
let corr: f64 = centered.iter()
|
||||
.take(n - lag)
|
||||
.zip(centered.iter().skip(lag))
|
||||
.map(|(a, b)| a * b)
|
||||
.sum();
|
||||
|
||||
let normalized_corr = corr / variance;
|
||||
if normalized_corr > max_corr {
|
||||
max_corr = normalized_corr;
|
||||
}
|
||||
}
|
||||
|
||||
max_corr.max(0.0)
|
||||
}
|
||||
|
||||
/// Determine movement type based on signal characteristics
|
||||
fn determine_movement_type(
|
||||
&self,
|
||||
variance: f64,
|
||||
max_change: f64,
|
||||
periodicity: f64,
|
||||
) -> (MovementType, bool) {
|
||||
// No significant movement
|
||||
if variance < self.config.movement_threshold * 0.5
|
||||
&& max_change < self.config.movement_threshold
|
||||
{
|
||||
return (MovementType::None, false);
|
||||
}
|
||||
|
||||
// Check for gross movement (large, purposeful)
|
||||
if max_change > self.config.gross_movement_threshold
|
||||
&& variance > self.config.movement_threshold
|
||||
{
|
||||
// Gross movement with low periodicity suggests voluntary
|
||||
let is_voluntary = periodicity < self.config.periodicity_threshold;
|
||||
return (MovementType::Gross, is_voluntary);
|
||||
}
|
||||
|
||||
// Check for periodic movement (breathing-related or tremor)
|
||||
if periodicity > self.config.periodicity_threshold {
|
||||
// High periodicity with low variance = breathing-related
|
||||
if variance < self.config.movement_threshold * 2.0 {
|
||||
return (MovementType::Periodic, false);
|
||||
}
|
||||
// High periodicity with higher variance = tremor
|
||||
return (MovementType::Tremor, false);
|
||||
}
|
||||
|
||||
// Fine movement (small but detectable)
|
||||
if variance > self.config.movement_threshold * 0.5 {
|
||||
// Fine movement might be voluntary if not very periodic
|
||||
let is_voluntary = periodicity < 0.2;
|
||||
return (MovementType::Fine, is_voluntary);
|
||||
}
|
||||
|
||||
(MovementType::None, false)
|
||||
}
|
||||
|
||||
/// Calculate movement intensity (0.0-1.0)
|
||||
fn calculate_intensity(&self, variance: f64, max_change: f64) -> f32 {
|
||||
// Combine variance and max change
|
||||
let variance_score = (variance / (self.config.gross_movement_threshold * 2.0)).min(1.0);
|
||||
let change_score = (max_change / self.config.gross_movement_threshold).min(1.0);
|
||||
|
||||
((variance_score * 0.6 + change_score * 0.4) as f32).min(1.0)
|
||||
}
|
||||
|
||||
/// Calculate movement frequency (movements per second)
|
||||
fn calculate_movement_frequency(&self, signal: &[f64], sample_rate: f64) -> f32 {
|
||||
if signal.len() < 3 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
// Count zero crossings (after removing mean)
|
||||
let mean = signal.iter().sum::<f64>() / signal.len() as f64;
|
||||
let centered: Vec<f64> = signal.iter().map(|x| x - mean).collect();
|
||||
|
||||
let zero_crossings: usize = centered.windows(2)
|
||||
.filter(|w| (w[0] >= 0.0) != (w[1] >= 0.0))
|
||||
.count();
|
||||
|
||||
// Each zero crossing is half a cycle
|
||||
let duration = signal.len() as f64 / sample_rate;
|
||||
let frequency = zero_crossings as f64 / (2.0 * duration);
|
||||
|
||||
frequency as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_no_movement() {
|
||||
let classifier = MovementClassifier::with_defaults();
|
||||
let signal: Vec<f64> = vec![1.0; 200];
|
||||
|
||||
let profile = classifier.classify(&signal, 100.0);
|
||||
assert!(matches!(profile.movement_type, MovementType::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gross_movement() {
|
||||
let classifier = MovementClassifier::with_defaults();
|
||||
|
||||
// Simulate large movement
|
||||
let mut signal: Vec<f64> = vec![0.0; 200];
|
||||
for i in 50..100 {
|
||||
signal[i] = 2.0;
|
||||
}
|
||||
for i in 150..180 {
|
||||
signal[i] = -1.5;
|
||||
}
|
||||
|
||||
let profile = classifier.classify(&signal, 100.0);
|
||||
assert!(matches!(profile.movement_type, MovementType::Gross));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_periodic_movement() {
|
||||
let classifier = MovementClassifier::with_defaults();
|
||||
|
||||
// Simulate periodic signal (like breathing)
|
||||
let signal: Vec<f64> = (0..1000)
|
||||
.map(|i| (2.0 * std::f64::consts::PI * i as f64 / 100.0).sin() * 0.3)
|
||||
.collect();
|
||||
|
||||
let profile = classifier.classify(&signal, 100.0);
|
||||
// Should detect periodic or fine movement
|
||||
assert!(!matches!(profile.movement_type, MovementType::None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intensity_calculation() {
|
||||
let classifier = MovementClassifier::with_defaults();
|
||||
|
||||
// Low intensity
|
||||
let low_signal: Vec<f64> = (0..200)
|
||||
.map(|i| (i as f64 * 0.1).sin() * 0.05)
|
||||
.collect();
|
||||
let low_profile = classifier.classify(&low_signal, 100.0);
|
||||
|
||||
// High intensity
|
||||
let high_signal: Vec<f64> = (0..200)
|
||||
.map(|i| (i as f64 * 0.1).sin() * 2.0)
|
||||
.collect();
|
||||
let high_profile = classifier.classify(&high_signal, 100.0);
|
||||
|
||||
assert!(high_profile.intensity > low_profile.intensity);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user