//! Motion Detection Module //! //! This module provides motion detection and human presence detection //! capabilities based on CSI features. use crate::features::{AmplitudeFeatures, CorrelationFeatures, CsiFeatures, PhaseFeatures}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; /// Motion score with component breakdown #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MotionScore { /// Overall motion score (0.0 to 1.0) pub total: f64, /// Variance-based motion component pub variance_component: f64, /// Correlation-based motion component pub correlation_component: f64, /// Phase-based motion component pub phase_component: f64, /// Doppler-based motion component (if available) pub doppler_component: Option, } impl MotionScore { /// Create a new motion score pub fn new( variance_component: f64, correlation_component: f64, phase_component: f64, doppler_component: Option, ) -> Self { // Calculate weighted total let total = if let Some(doppler) = doppler_component { 0.3 * variance_component + 0.2 * correlation_component + 0.2 * phase_component + 0.3 * doppler } else { 0.4 * variance_component + 0.3 * correlation_component + 0.3 * phase_component }; Self { total: total.clamp(0.0, 1.0), variance_component, correlation_component, phase_component, doppler_component, } } /// Check if motion is detected above threshold pub fn is_motion_detected(&self, threshold: f64) -> bool { self.total >= threshold } } /// Motion analysis results #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MotionAnalysis { /// Motion score pub score: MotionScore, /// Temporal variance of motion pub temporal_variance: f64, /// Spatial variance of motion pub spatial_variance: f64, /// Estimated motion velocity (arbitrary units) pub estimated_velocity: f64, /// Motion direction estimate (radians, if available) pub motion_direction: Option, /// Confidence in the analysis pub confidence: f64, } /// Human detection result #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HumanDetectionResult { /// Whether a human was detected pub human_detected: bool, /// Detection confidence (0.0 to 1.0) pub confidence: f64, /// Motion score pub motion_score: f64, /// Raw (unsmoothed) confidence pub raw_confidence: f64, /// Timestamp of detection pub timestamp: DateTime, /// Detection threshold used pub threshold: f64, /// Detailed motion analysis pub motion_analysis: MotionAnalysis, /// Additional metadata #[serde(default)] pub metadata: DetectionMetadata, } /// Metadata for detection results #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct DetectionMetadata { /// Number of features used pub features_used: usize, /// Processing time in milliseconds pub processing_time_ms: Option, /// Whether Doppler was available pub doppler_available: bool, /// History length used pub history_length: usize, } /// Configuration for motion detector #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MotionDetectorConfig { /// Human detection threshold (0.0 to 1.0) pub human_detection_threshold: f64, /// Motion detection threshold (0.0 to 1.0) pub motion_threshold: f64, /// Temporal smoothing factor (0.0 to 1.0) /// Higher values give more weight to previous detections pub smoothing_factor: f64, /// Minimum amplitude indicator threshold pub amplitude_threshold: f64, /// Minimum phase indicator threshold pub phase_threshold: f64, /// History size for temporal analysis pub history_size: usize, /// Enable adaptive thresholding pub adaptive_threshold: bool, /// Weight for amplitude indicator pub amplitude_weight: f64, /// Weight for phase indicator pub phase_weight: f64, /// Weight for motion indicator pub motion_weight: f64, } impl Default for MotionDetectorConfig { fn default() -> Self { Self { human_detection_threshold: 0.8, motion_threshold: 0.3, smoothing_factor: 0.9, amplitude_threshold: 0.1, phase_threshold: 0.05, history_size: 100, adaptive_threshold: false, amplitude_weight: 0.4, phase_weight: 0.3, motion_weight: 0.3, } } } impl MotionDetectorConfig { /// Create a new builder pub fn builder() -> MotionDetectorConfigBuilder { MotionDetectorConfigBuilder::new() } } /// Builder for MotionDetectorConfig #[derive(Debug, Default)] pub struct MotionDetectorConfigBuilder { config: MotionDetectorConfig, } impl MotionDetectorConfigBuilder { /// Create new builder pub fn new() -> Self { Self { config: MotionDetectorConfig::default(), } } /// Set human detection threshold pub fn human_detection_threshold(mut self, threshold: f64) -> Self { self.config.human_detection_threshold = threshold; self } /// Set motion threshold pub fn motion_threshold(mut self, threshold: f64) -> Self { self.config.motion_threshold = threshold; self } /// Set smoothing factor pub fn smoothing_factor(mut self, factor: f64) -> Self { self.config.smoothing_factor = factor; self } /// Set amplitude threshold pub fn amplitude_threshold(mut self, threshold: f64) -> Self { self.config.amplitude_threshold = threshold; self } /// Set phase threshold pub fn phase_threshold(mut self, threshold: f64) -> Self { self.config.phase_threshold = threshold; self } /// Set history size pub fn history_size(mut self, size: usize) -> Self { self.config.history_size = size; self } /// Enable adaptive thresholding pub fn adaptive_threshold(mut self, enable: bool) -> Self { self.config.adaptive_threshold = enable; self } /// Set indicator weights pub fn weights(mut self, amplitude: f64, phase: f64, motion: f64) -> Self { self.config.amplitude_weight = amplitude; self.config.phase_weight = phase; self.config.motion_weight = motion; self } /// Build configuration pub fn build(self) -> MotionDetectorConfig { self.config } } /// Motion detector for human presence detection #[derive(Debug)] pub struct MotionDetector { config: MotionDetectorConfig, previous_confidence: f64, motion_history: VecDeque, detection_count: usize, total_detections: usize, baseline_variance: Option, } impl MotionDetector { /// Create a new motion detector pub fn new(config: MotionDetectorConfig) -> Self { Self { motion_history: VecDeque::with_capacity(config.history_size), config, previous_confidence: 0.0, detection_count: 0, total_detections: 0, baseline_variance: None, } } /// Create with default configuration pub fn default_config() -> Self { Self::new(MotionDetectorConfig::default()) } /// Get configuration pub fn config(&self) -> &MotionDetectorConfig { &self.config } /// Analyze motion patterns from CSI features pub fn analyze_motion(&self, features: &CsiFeatures) -> MotionAnalysis { // Calculate variance-based motion score let variance_score = self.calculate_variance_score(&features.amplitude); // Calculate correlation-based motion score let correlation_score = self.calculate_correlation_score(&features.correlation); // Calculate phase-based motion score let phase_score = self.calculate_phase_score(&features.phase); // Calculate Doppler-based score if available let doppler_score = features.doppler.as_ref().map(|d| { // Normalize Doppler magnitude to 0-1 range (d.mean_magnitude / 100.0).clamp(0.0, 1.0) }); let motion_score = MotionScore::new(variance_score, correlation_score, phase_score, doppler_score); // Calculate temporal and spatial variance let temporal_variance = self.calculate_temporal_variance(); let spatial_variance = features.amplitude.variance.iter().sum::() / features.amplitude.variance.len() as f64; // Estimate velocity from Doppler if available let estimated_velocity = features .doppler .as_ref() .map(|d| d.mean_magnitude) .unwrap_or(0.0); // Motion direction from phase gradient let motion_direction = if features.phase.gradient.len() > 0 { let mean_grad: f64 = features.phase.gradient.iter().sum::() / features.phase.gradient.len() as f64; Some(mean_grad.atan()) } else { None }; // Calculate confidence based on signal quality indicators let confidence = self.calculate_motion_confidence(features); MotionAnalysis { score: motion_score, temporal_variance, spatial_variance, estimated_velocity, motion_direction, confidence, } } /// Calculate variance-based motion score fn calculate_variance_score(&self, amplitude: &AmplitudeFeatures) -> f64 { let mean_variance = amplitude.variance.iter().sum::() / amplitude.variance.len() as f64; // Normalize using baseline if available if let Some(baseline) = self.baseline_variance { let ratio = mean_variance / (baseline + 1e-10); (ratio - 1.0).max(0.0).tanh() } else { // Use heuristic normalization (mean_variance / 0.5).clamp(0.0, 1.0) } } /// Calculate correlation-based motion score fn calculate_correlation_score(&self, correlation: &CorrelationFeatures) -> f64 { let n = correlation.matrix.dim().0; if n < 2 { return 0.0; } // Calculate mean deviation from identity matrix let mut deviation_sum = 0.0; let mut count = 0; for i in 0..n { for j in 0..n { let expected = if i == j { 1.0 } else { 0.0 }; deviation_sum += (correlation.matrix[[i, j]] - expected).abs(); count += 1; } } let mean_deviation = deviation_sum / count as f64; mean_deviation.clamp(0.0, 1.0) } /// Calculate phase-based motion score fn calculate_phase_score(&self, phase: &PhaseFeatures) -> f64 { // Use phase variance and coherence let mean_variance = phase.variance.iter().sum::() / phase.variance.len() as f64; let coherence_factor = 1.0 - phase.coherence.abs(); // Combine factors let score = 0.5 * (mean_variance / 0.5).clamp(0.0, 1.0) + 0.5 * coherence_factor; score.clamp(0.0, 1.0) } /// Calculate temporal variance from motion history fn calculate_temporal_variance(&self) -> f64 { if self.motion_history.len() < 2 { return 0.0; } let scores: Vec = self.motion_history.iter().map(|m| m.total).collect(); let mean: f64 = scores.iter().sum::() / scores.len() as f64; let variance: f64 = scores.iter().map(|s| (s - mean).powi(2)).sum::() / scores.len() as f64; variance.sqrt() } /// Calculate confidence in motion detection fn calculate_motion_confidence(&self, features: &CsiFeatures) -> f64 { let mut confidence = 0.0; let mut weight_sum = 0.0; // Amplitude quality indicator let amp_quality = (features.amplitude.dynamic_range / 2.0).clamp(0.0, 1.0); confidence += amp_quality * 0.3; weight_sum += 0.3; // Phase coherence indicator let phase_quality = features.phase.coherence.abs(); confidence += phase_quality * 0.3; weight_sum += 0.3; // Correlation consistency indicator let corr_quality = (1.0 - features.correlation.correlation_spread).clamp(0.0, 1.0); confidence += corr_quality * 0.2; weight_sum += 0.2; // Doppler quality if available if let Some(ref doppler) = features.doppler { let doppler_quality = (doppler.spread / doppler.mean_magnitude.max(1.0)).clamp(0.0, 1.0); confidence += (1.0 - doppler_quality) * 0.2; weight_sum += 0.2; } if weight_sum > 0.0 { confidence / weight_sum } else { 0.0 } } /// Calculate detection confidence from features and motion score fn calculate_detection_confidence(&self, features: &CsiFeatures, motion_score: f64) -> f64 { // Amplitude indicator let amplitude_mean = features.amplitude.mean.iter().sum::() / features.amplitude.mean.len() as f64; let amplitude_indicator = if amplitude_mean > self.config.amplitude_threshold { 1.0 } else { 0.0 }; // Phase indicator let phase_std = features.phase.variance.iter().sum::().sqrt() / features.phase.variance.len() as f64; let phase_indicator = if phase_std > self.config.phase_threshold { 1.0 } else { 0.0 }; // Motion indicator let motion_indicator = if motion_score > self.config.motion_threshold { 1.0 } else { 0.0 }; // Weighted combination let confidence = self.config.amplitude_weight * amplitude_indicator + self.config.phase_weight * phase_indicator + self.config.motion_weight * motion_indicator; confidence.clamp(0.0, 1.0) } /// Apply temporal smoothing (exponential moving average) fn apply_temporal_smoothing(&mut self, raw_confidence: f64) -> f64 { let smoothed = self.config.smoothing_factor * self.previous_confidence + (1.0 - self.config.smoothing_factor) * raw_confidence; self.previous_confidence = smoothed; smoothed } /// Detect human presence from CSI features pub fn detect_human(&mut self, features: &CsiFeatures) -> HumanDetectionResult { // Analyze motion let motion_analysis = self.analyze_motion(features); // Add to history if self.motion_history.len() >= self.config.history_size { self.motion_history.pop_front(); } self.motion_history.push_back(motion_analysis.score.clone()); // Calculate detection confidence let raw_confidence = self.calculate_detection_confidence(features, motion_analysis.score.total); // Apply temporal smoothing let smoothed_confidence = self.apply_temporal_smoothing(raw_confidence); // Get effective threshold (adaptive if enabled) let threshold = if self.config.adaptive_threshold { self.calculate_adaptive_threshold() } else { self.config.human_detection_threshold }; // Determine detection let human_detected = smoothed_confidence >= threshold; self.total_detections += 1; if human_detected { self.detection_count += 1; } let metadata = DetectionMetadata { features_used: 4, // amplitude, phase, correlation, psd processing_time_ms: None, doppler_available: features.doppler.is_some(), history_length: self.motion_history.len(), }; HumanDetectionResult { human_detected, confidence: smoothed_confidence, motion_score: motion_analysis.score.total, raw_confidence, timestamp: Utc::now(), threshold, motion_analysis, metadata, } } /// Calculate adaptive threshold based on recent history fn calculate_adaptive_threshold(&self) -> f64 { if self.motion_history.len() < 10 { return self.config.human_detection_threshold; } let scores: Vec = self.motion_history.iter().map(|m| m.total).collect(); let mean: f64 = scores.iter().sum::() / scores.len() as f64; let std: f64 = { let var: f64 = scores.iter().map(|s| (s - mean).powi(2)).sum::() / scores.len() as f64; var.sqrt() }; // Threshold is mean + 1 std deviation, clamped to reasonable range (mean + std).clamp(0.3, 0.95) } /// Update baseline variance (for calibration) pub fn calibrate(&mut self, features: &CsiFeatures) { let mean_variance = features.amplitude.variance.iter().sum::() / features.amplitude.variance.len() as f64; self.baseline_variance = Some(mean_variance); } /// Clear calibration pub fn clear_calibration(&mut self) { self.baseline_variance = None; } /// Get detection statistics pub fn get_statistics(&self) -> DetectionStatistics { DetectionStatistics { total_detections: self.total_detections, positive_detections: self.detection_count, detection_rate: if self.total_detections > 0 { self.detection_count as f64 / self.total_detections as f64 } else { 0.0 }, history_size: self.motion_history.len(), is_calibrated: self.baseline_variance.is_some(), } } /// Reset detector state pub fn reset(&mut self) { self.previous_confidence = 0.0; self.motion_history.clear(); self.detection_count = 0; self.total_detections = 0; } /// Get previous confidence value pub fn previous_confidence(&self) -> f64 { self.previous_confidence } } /// Detection statistics #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DetectionStatistics { /// Total number of detection attempts pub total_detections: usize, /// Number of positive detections pub positive_detections: usize, /// Detection rate (0.0 to 1.0) pub detection_rate: f64, /// Current history size pub history_size: usize, /// Whether detector is calibrated pub is_calibrated: bool, } #[cfg(test)] mod tests { use super::*; use crate::csi_processor::CsiData; use crate::features::FeatureExtractor; use ndarray::Array2; fn create_test_csi_data(motion_level: f64) -> CsiData { let amplitude = Array2::from_shape_fn((4, 64), |(i, j)| { 1.0 + motion_level * 0.5 * ((i + j) as f64 * 0.1).sin() }); let phase = Array2::from_shape_fn((4, 64), |(i, j)| { motion_level * 0.3 * ((i + j) as f64 * 0.15).sin() }); CsiData::builder() .amplitude(amplitude) .phase(phase) .frequency(5.0e9) .bandwidth(20.0e6) .snr(25.0) .build() .unwrap() } fn create_test_features(motion_level: f64) -> CsiFeatures { let csi_data = create_test_csi_data(motion_level); let extractor = FeatureExtractor::default_config(); extractor.extract(&csi_data) } #[test] fn test_motion_score() { let score = MotionScore::new(0.5, 0.6, 0.4, None); assert!(score.total > 0.0 && score.total <= 1.0); assert_eq!(score.variance_component, 0.5); assert_eq!(score.correlation_component, 0.6); assert_eq!(score.phase_component, 0.4); } #[test] fn test_motion_score_with_doppler() { let score = MotionScore::new(0.5, 0.6, 0.4, Some(0.7)); assert!(score.total > 0.0 && score.total <= 1.0); assert_eq!(score.doppler_component, Some(0.7)); } #[test] fn test_motion_detector_creation() { let config = MotionDetectorConfig::default(); let detector = MotionDetector::new(config); assert_eq!(detector.previous_confidence(), 0.0); } #[test] fn test_motion_analysis() { let detector = MotionDetector::default_config(); let features = create_test_features(0.5); let analysis = detector.analyze_motion(&features); assert!(analysis.score.total >= 0.0 && analysis.score.total <= 1.0); assert!(analysis.confidence >= 0.0 && analysis.confidence <= 1.0); } #[test] fn test_human_detection() { let config = MotionDetectorConfig::builder() .human_detection_threshold(0.5) .smoothing_factor(0.5) .build(); let mut detector = MotionDetector::new(config); let features = create_test_features(0.8); let result = detector.detect_human(&features); assert!(result.confidence >= 0.0 && result.confidence <= 1.0); assert!(result.motion_score >= 0.0 && result.motion_score <= 1.0); } #[test] fn test_temporal_smoothing() { let config = MotionDetectorConfig::builder() .smoothing_factor(0.9) .build(); let mut detector = MotionDetector::new(config); // First detection with low confidence let features_low = create_test_features(0.1); let result1 = detector.detect_human(&features_low); // Second detection with high confidence should be smoothed let features_high = create_test_features(0.9); let result2 = detector.detect_human(&features_high); // Due to smoothing, result2.confidence should be between result1 and raw assert!(result2.confidence >= result1.confidence); } #[test] fn test_calibration() { let mut detector = MotionDetector::default_config(); let features = create_test_features(0.5); assert!(!detector.get_statistics().is_calibrated); detector.calibrate(&features); assert!(detector.get_statistics().is_calibrated); detector.clear_calibration(); assert!(!detector.get_statistics().is_calibrated); } #[test] fn test_detection_statistics() { let mut detector = MotionDetector::default_config(); for i in 0..5 { let features = create_test_features((i as f64) / 5.0); let _ = detector.detect_human(&features); } let stats = detector.get_statistics(); assert_eq!(stats.total_detections, 5); assert!(stats.detection_rate >= 0.0 && stats.detection_rate <= 1.0); } #[test] fn test_reset() { let mut detector = MotionDetector::default_config(); let features = create_test_features(0.5); for _ in 0..5 { let _ = detector.detect_human(&features); } detector.reset(); let stats = detector.get_statistics(); assert_eq!(stats.total_detections, 0); assert_eq!(stats.history_size, 0); assert_eq!(detector.previous_confidence(), 0.0); } #[test] fn test_adaptive_threshold() { let config = MotionDetectorConfig::builder() .adaptive_threshold(true) .history_size(20) .build(); let mut detector = MotionDetector::new(config); // Build up history for i in 0..15 { let features = create_test_features((i as f64 % 5.0) / 5.0); let _ = detector.detect_human(&features); } // The adaptive threshold should now be calculated let features = create_test_features(0.5); let result = detector.detect_human(&features); // Threshold should be different from default // (this is a weak assertion, mainly checking it runs) assert!(result.threshold > 0.0); } #[test] fn test_config_builder() { let config = MotionDetectorConfig::builder() .human_detection_threshold(0.7) .motion_threshold(0.4) .smoothing_factor(0.85) .amplitude_threshold(0.15) .phase_threshold(0.08) .history_size(200) .adaptive_threshold(true) .weights(0.35, 0.35, 0.30) .build(); assert_eq!(config.human_detection_threshold, 0.7); assert_eq!(config.motion_threshold, 0.4); assert_eq!(config.smoothing_factor, 0.85); assert_eq!(config.amplitude_threshold, 0.15); assert_eq!(config.phase_threshold, 0.08); assert_eq!(config.history_size, 200); assert!(config.adaptive_threshold); assert_eq!(config.amplitude_weight, 0.35); assert_eq!(config.phase_weight, 0.35); assert_eq!(config.motion_weight, 0.30); } #[test] fn test_low_motion_no_detection() { let config = MotionDetectorConfig::builder() .human_detection_threshold(0.8) .smoothing_factor(0.0) // No smoothing for clear test .build(); let mut detector = MotionDetector::new(config); // Very low motion should not trigger detection let features = create_test_features(0.01); let result = detector.detect_human(&features); // With very low motion, detection should likely be false // (depends on thresholds, but confidence should be low) assert!(result.motion_score < 0.5); } #[test] fn test_motion_history() { let config = MotionDetectorConfig::builder() .history_size(10) .build(); let mut detector = MotionDetector::new(config); for i in 0..15 { let features = create_test_features((i as f64) / 15.0); let _ = detector.detect_human(&features); } let stats = detector.get_statistics(); assert_eq!(stats.history_size, 10); // Should not exceed max } }