- Add detailed wifi-Mat user guide covering: - Installation and setup - Detection capabilities (breathing, heartbeat, movement) - Localization system (triangulation, depth estimation) - START protocol triage classification - Alert system with priority escalation - Field deployment guide - Hardware setup requirements - API reference and troubleshooting - Update main README.md with wifi-Mat section and links - Fix compilation issues: - Add missing deadline field in AlertPayload - Fix type ambiguity in powi calls - Resolve borrow checker issues in scan_cycle - Export CsiDataBuffer from detection module - Add missing imports in test modules - All 83 tests now passing
276 lines
8.7 KiB
Rust
276 lines
8.7 KiB
Rust
//! 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) with higher amplitude
|
|
let signal: Vec<f64> = (0..1000)
|
|
.map(|i| (2.0 * std::f64::consts::PI * i as f64 / 100.0).sin() * 1.5)
|
|
.collect();
|
|
|
|
let profile = classifier.classify(&signal, 100.0);
|
|
// Should detect some movement type (periodic, fine, or at least have non-zero intensity)
|
|
// The exact type depends on thresholds, but with enough amplitude we should detect something
|
|
assert!(profile.intensity > 0.0 || !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);
|
|
}
|
|
}
|