//! Fresnel Zone Breathing Model //! //! Models WiFi signal variation as a function of human chest displacement //! crossing Fresnel zone boundaries. At 5 GHz (λ=60mm), chest displacement //! of 5-10mm during breathing is a significant fraction of the Fresnel zone //! width, producing measurable phase and amplitude changes. //! //! # References //! - FarSense: Pushing the Range Limit (MobiCom 2019) //! - Wi-Sleep: Contactless Sleep Staging (UbiComp 2021) use std::f64::consts::PI; /// Physical constants and defaults for WiFi sensing. pub const SPEED_OF_LIGHT: f64 = 2.998e8; // m/s /// Fresnel zone geometry for a TX-RX-body configuration. #[derive(Debug, Clone)] pub struct FresnelGeometry { /// Distance from TX to body reflection point (meters) pub d_tx_body: f64, /// Distance from body reflection point to RX (meters) pub d_body_rx: f64, /// Carrier frequency in Hz (e.g., 5.8e9 for 5.8 GHz) pub frequency: f64, } impl FresnelGeometry { /// Create geometry for a given TX-body-RX configuration. pub fn new(d_tx_body: f64, d_body_rx: f64, frequency: f64) -> Result { if d_tx_body <= 0.0 || d_body_rx <= 0.0 { return Err(FresnelError::InvalidDistance); } if frequency <= 0.0 { return Err(FresnelError::InvalidFrequency); } Ok(Self { d_tx_body, d_body_rx, frequency, }) } /// Wavelength in meters. pub fn wavelength(&self) -> f64 { SPEED_OF_LIGHT / self.frequency } /// Radius of the nth Fresnel zone at the body point. /// /// F_n = sqrt(n * λ * d1 * d2 / (d1 + d2)) pub fn fresnel_radius(&self, n: u32) -> f64 { let lambda = self.wavelength(); let d1 = self.d_tx_body; let d2 = self.d_body_rx; (n as f64 * lambda * d1 * d2 / (d1 + d2)).sqrt() } /// Phase change caused by a small body displacement Δd (meters). /// /// The reflected path changes by 2*Δd (there and back), producing /// phase change: ΔΦ = 2π * 2Δd / λ pub fn phase_change(&self, displacement_m: f64) -> f64 { 2.0 * PI * 2.0 * displacement_m / self.wavelength() } /// Expected amplitude variation from chest displacement. /// /// The signal amplitude varies as |sin(ΔΦ/2)| when the reflection /// point crosses Fresnel zone boundaries. pub fn expected_amplitude_variation(&self, displacement_m: f64) -> f64 { let delta_phi = self.phase_change(displacement_m); (delta_phi / 2.0).sin().abs() } } /// Breathing rate estimation using Fresnel zone model. #[derive(Debug, Clone)] pub struct FresnelBreathingEstimator { geometry: FresnelGeometry, /// Expected chest displacement range (meters) for breathing min_displacement: f64, max_displacement: f64, } impl FresnelBreathingEstimator { /// Create estimator with geometry and chest displacement bounds. /// /// Typical adult chest displacement: 4-12mm (0.004-0.012 m) pub fn new(geometry: FresnelGeometry) -> Self { Self { geometry, min_displacement: 0.003, max_displacement: 0.015, } } /// Check if observed amplitude variation is consistent with breathing. /// /// Returns confidence (0.0-1.0) based on whether the observed signal /// variation matches the expected Fresnel model prediction for chest /// displacements in the breathing range. pub fn breathing_confidence(&self, observed_amplitude_variation: f64) -> f64 { let min_expected = self.geometry.expected_amplitude_variation(self.min_displacement); let max_expected = self.geometry.expected_amplitude_variation(self.max_displacement); let (low, high) = if min_expected < max_expected { (min_expected, max_expected) } else { (max_expected, min_expected) }; if observed_amplitude_variation >= low && observed_amplitude_variation <= high { // Within expected range: high confidence 1.0 } else if observed_amplitude_variation < low { // Below range: scale linearly (observed_amplitude_variation / low).clamp(0.0, 1.0) } else { // Above range: could be larger motion (walking), lower confidence for breathing (high / observed_amplitude_variation).clamp(0.0, 1.0) } } /// Estimate breathing rate from temporal amplitude signal using the Fresnel model. /// /// Uses autocorrelation to find periodicity, then validates against /// expected Fresnel amplitude range. Returns (rate_bpm, confidence). pub fn estimate_breathing_rate( &self, amplitude_signal: &[f64], sample_rate: f64, ) -> Result { if amplitude_signal.len() < 10 { return Err(FresnelError::InsufficientData { needed: 10, got: amplitude_signal.len(), }); } if sample_rate <= 0.0 { return Err(FresnelError::InvalidFrequency); } // Remove DC (mean) let mean: f64 = amplitude_signal.iter().sum::() / amplitude_signal.len() as f64; let centered: Vec = amplitude_signal.iter().map(|x| x - mean).collect(); // Autocorrelation to find periodicity let n = centered.len(); let max_lag = (sample_rate * 10.0) as usize; // Up to 10 seconds (6 BPM) let min_lag = (sample_rate * 1.5) as usize; // At least 1.5 seconds (40 BPM) let max_lag = max_lag.min(n / 2); if min_lag >= max_lag { return Err(FresnelError::InsufficientData { needed: (min_lag * 2 + 1), got: n, }); } // Compute autocorrelation for breathing-range lags let mut best_lag = min_lag; let mut best_corr = f64::NEG_INFINITY; let norm: f64 = centered.iter().map(|x| x * x).sum(); if norm < 1e-15 { return Err(FresnelError::NoSignal); } for lag in min_lag..max_lag { let mut corr = 0.0; for i in 0..(n - lag) { corr += centered[i] * centered[i + lag]; } corr /= norm; if corr > best_corr { best_corr = corr; best_lag = lag; } } let period_seconds = best_lag as f64 / sample_rate; let rate_bpm = 60.0 / period_seconds; // Compute amplitude variation for Fresnel confidence let amp_var = amplitude_variation(¢ered); let fresnel_conf = self.breathing_confidence(amp_var); // Autocorrelation quality (>0.3 is good periodicity) let autocorr_conf = best_corr.max(0.0).min(1.0); let confidence = fresnel_conf * 0.4 + autocorr_conf * 0.6; Ok(BreathingEstimate { rate_bpm, confidence, period_seconds, autocorrelation_peak: best_corr, fresnel_confidence: fresnel_conf, amplitude_variation: amp_var, }) } } /// Result of breathing rate estimation. #[derive(Debug, Clone)] pub struct BreathingEstimate { /// Estimated breathing rate in breaths per minute pub rate_bpm: f64, /// Combined confidence (0.0-1.0) pub confidence: f64, /// Estimated breathing period in seconds pub period_seconds: f64, /// Peak autocorrelation value at detected period pub autocorrelation_peak: f64, /// Confidence from Fresnel model match pub fresnel_confidence: f64, /// Observed amplitude variation pub amplitude_variation: f64, } /// Compute peak-to-peak amplitude variation (normalized). fn amplitude_variation(signal: &[f64]) -> f64 { if signal.is_empty() { return 0.0; } let max = signal.iter().cloned().fold(f64::NEG_INFINITY, f64::max); let min = signal.iter().cloned().fold(f64::INFINITY, f64::min); max - min } /// Errors from Fresnel computations. #[derive(Debug, thiserror::Error)] pub enum FresnelError { #[error("Distance must be positive")] InvalidDistance, #[error("Frequency must be positive")] InvalidFrequency, #[error("Insufficient data: need {needed}, got {got}")] InsufficientData { needed: usize, got: usize }, #[error("No signal detected (zero variance)")] NoSignal, } #[cfg(test)] mod tests { use super::*; fn test_geometry() -> FresnelGeometry { // TX 3m from body, body 2m from RX, 5 GHz WiFi FresnelGeometry::new(3.0, 2.0, 5.0e9).unwrap() } #[test] fn test_wavelength() { let g = test_geometry(); let lambda = g.wavelength(); assert!((lambda - 0.06).abs() < 0.001); // 5 GHz → 60mm } #[test] fn test_fresnel_radius() { let g = test_geometry(); let f1 = g.fresnel_radius(1); // F1 = sqrt(λ * d1 * d2 / (d1 + d2)) let lambda = g.wavelength(); // actual: 2.998e8 / 5e9 = 0.05996 let expected = (lambda * 3.0 * 2.0 / 5.0_f64).sqrt(); assert!((f1 - expected).abs() < 1e-6); assert!(f1 > 0.1 && f1 < 0.5); // Reasonable range } #[test] fn test_phase_change_from_displacement() { let g = test_geometry(); // 5mm chest displacement at 5 GHz let delta_phi = g.phase_change(0.005); // ΔΦ = 2π * 2 * 0.005 / λ let lambda = g.wavelength(); let expected = 2.0 * PI * 2.0 * 0.005 / lambda; assert!((delta_phi - expected).abs() < 1e-6); } #[test] fn test_amplitude_variation_breathing_range() { let g = test_geometry(); // 5mm displacement should produce detectable variation let var_5mm = g.expected_amplitude_variation(0.005); assert!(var_5mm > 0.01, "5mm should produce measurable variation"); // 10mm should produce more variation let var_10mm = g.expected_amplitude_variation(0.010); assert!(var_10mm > var_5mm || (var_10mm - var_5mm).abs() < 0.1); } #[test] fn test_breathing_confidence() { let g = test_geometry(); let estimator = FresnelBreathingEstimator::new(g.clone()); // Signal matching expected breathing range → high confidence let expected_var = g.expected_amplitude_variation(0.007); let conf = estimator.breathing_confidence(expected_var); assert!(conf > 0.5, "Expected breathing variation should give high confidence"); // Zero variation → low confidence let conf_zero = estimator.breathing_confidence(0.0); assert!(conf_zero < 0.5); } #[test] fn test_breathing_rate_estimation() { let g = test_geometry(); let estimator = FresnelBreathingEstimator::new(g); // Generate 30 seconds of breathing signal at 16 BPM (0.267 Hz) let sample_rate = 100.0; // Hz let duration = 30.0; let n = (sample_rate * duration) as usize; let breathing_freq = 0.267; // 16 BPM let signal: Vec = (0..n) .map(|i| { let t = i as f64 / sample_rate; 0.5 + 0.1 * (2.0 * PI * breathing_freq * t).sin() }) .collect(); let result = estimator .estimate_breathing_rate(&signal, sample_rate) .unwrap(); // Should detect ~16 BPM (within 2 BPM tolerance) assert!( (result.rate_bpm - 16.0).abs() < 2.0, "Expected ~16 BPM, got {:.1}", result.rate_bpm ); assert!(result.confidence > 0.3); assert!(result.autocorrelation_peak > 0.5); } #[test] fn test_invalid_geometry() { assert!(FresnelGeometry::new(-1.0, 2.0, 5e9).is_err()); assert!(FresnelGeometry::new(1.0, 0.0, 5e9).is_err()); assert!(FresnelGeometry::new(1.0, 2.0, 0.0).is_err()); } #[test] fn test_insufficient_data() { let g = test_geometry(); let estimator = FresnelBreathingEstimator::new(g); let short_signal = vec![1.0; 5]; assert!(matches!( estimator.estimate_breathing_rate(&short_signal, 100.0), Err(FresnelError::InsufficientData { .. }) )); } }