//! Depth estimation through debris layers. use crate::domain::{DebrisMaterial, DebrisProfile, DepthEstimate, MoistureLevel}; /// Configuration for depth estimation #[derive(Debug, Clone)] pub struct DepthEstimatorConfig { /// Maximum depth to estimate (meters) pub max_depth: f64, /// Minimum signal attenuation to consider (dB) pub min_attenuation: f64, /// WiFi frequency in GHz pub frequency_ghz: f64, /// Free space path loss at 1 meter (dB) pub free_space_loss_1m: f64, } impl Default for DepthEstimatorConfig { fn default() -> Self { Self { max_depth: 10.0, min_attenuation: 3.0, frequency_ghz: 5.8, // 5.8 GHz WiFi free_space_loss_1m: 47.0, // FSPL at 1m for 5.8 GHz } } } /// Estimator for survivor depth through debris pub struct DepthEstimator { config: DepthEstimatorConfig, } impl DepthEstimator { /// Create a new depth estimator pub fn new(config: DepthEstimatorConfig) -> Self { Self { config } } /// Create with default configuration pub fn with_defaults() -> Self { Self::new(DepthEstimatorConfig::default()) } /// Estimate depth from signal attenuation pub fn estimate_depth( &self, signal_attenuation: f64, // Total attenuation in dB distance_2d: f64, // Horizontal distance in meters debris_profile: &DebrisProfile, ) -> Option { if signal_attenuation < self.config.min_attenuation { // Very little attenuation - probably not buried return Some(DepthEstimate { depth: 0.0, uncertainty: 0.5, debris_profile: debris_profile.clone(), confidence: 0.9, }); } // Calculate free space path loss for horizontal distance let fspl = self.free_space_path_loss(distance_2d); // Debris attenuation = total - free space loss let debris_attenuation = (signal_attenuation - fspl).max(0.0); // Get attenuation coefficient for debris type let attenuation_per_meter = debris_profile.attenuation_factor(); if attenuation_per_meter < 0.1 { return None; } // Estimate depth let depth = debris_attenuation / attenuation_per_meter; // Clamp to maximum if depth > self.config.max_depth { return None; } // Calculate uncertainty (increases with depth and material variability) let base_uncertainty = 0.3; let depth_uncertainty = depth * 0.15; let material_uncertainty = self.material_uncertainty(debris_profile); let uncertainty = base_uncertainty + depth_uncertainty + material_uncertainty; // Calculate confidence (decreases with depth) let confidence = (1.0 - depth / self.config.max_depth).max(0.3); Some(DepthEstimate { depth, uncertainty, debris_profile: debris_profile.clone(), confidence, }) } /// Estimate debris profile from signal characteristics pub fn estimate_debris_profile( &self, signal_variance: f64, signal_multipath: f64, moisture_indicator: f64, ) -> DebrisProfile { // Estimate material based on signal characteristics let primary_material = if signal_variance > 0.5 { // High variance suggests heterogeneous material DebrisMaterial::Mixed } else if signal_multipath > 0.7 { // High multipath suggests reflective surfaces DebrisMaterial::HeavyConcrete } else if signal_multipath < 0.3 { // Low multipath suggests absorptive material DebrisMaterial::Soil } else { DebrisMaterial::LightConcrete }; // Estimate void fraction from multipath let void_fraction = signal_multipath.clamp(0.1, 0.5); // Estimate moisture from signal characteristics let moisture_content = if moisture_indicator > 0.7 { MoistureLevel::Wet } else if moisture_indicator > 0.4 { MoistureLevel::Damp } else { MoistureLevel::Dry }; DebrisProfile { primary_material, void_fraction, moisture_content, metal_content: crate::domain::MetalContent::Low, } } /// Calculate free space path loss fn free_space_path_loss(&self, distance: f64) -> f64 { // FSPL = 20*log10(d) + 20*log10(f) + 20*log10(4*pi/c) // Simplified: FSPL(d) = FSPL(1m) + 20*log10(d) if distance <= 0.0 { return 0.0; } self.config.free_space_loss_1m + 20.0 * distance.log10() } /// Calculate uncertainty based on material properties fn material_uncertainty(&self, profile: &DebrisProfile) -> f64 { // Mixed materials have higher uncertainty let material_factor = match profile.primary_material { DebrisMaterial::Mixed => 0.4, DebrisMaterial::HeavyConcrete => 0.2, DebrisMaterial::LightConcrete => 0.2, DebrisMaterial::Soil => 0.3, DebrisMaterial::Wood => 0.15, DebrisMaterial::Snow => 0.1, DebrisMaterial::Metal => 0.5, // Very unpredictable }; // Moisture adds uncertainty let moisture_factor = match profile.moisture_content { MoistureLevel::Dry => 0.0, MoistureLevel::Damp => 0.1, MoistureLevel::Wet => 0.2, MoistureLevel::Saturated => 0.3, }; material_factor + moisture_factor } /// Estimate depth from multiple signal paths pub fn estimate_from_multipath( &self, direct_path_attenuation: f64, reflected_paths: &[(f64, f64)], // (attenuation, delay) debris_profile: &DebrisProfile, ) -> Option { // Use path differences to estimate depth if reflected_paths.is_empty() { return self.estimate_depth(direct_path_attenuation, 0.0, debris_profile); } // Average extra path length from reflections const SPEED_OF_LIGHT: f64 = 299_792_458.0; let avg_extra_path: f64 = reflected_paths .iter() .map(|(_, delay)| delay * SPEED_OF_LIGHT / 2.0) // Round trip .sum::() / reflected_paths.len() as f64; // Extra path length is approximately related to depth // (reflections bounce off debris layers) let estimated_depth = avg_extra_path / 4.0; // Empirical factor let attenuation_per_meter = debris_profile.attenuation_factor(); let attenuation_based_depth = direct_path_attenuation / attenuation_per_meter; // Combine estimates let depth = (estimated_depth + attenuation_based_depth) / 2.0; if depth > self.config.max_depth { return None; } let uncertainty = 0.5 + depth * 0.2; let confidence = (1.0 - depth / self.config.max_depth).max(0.3); Some(DepthEstimate { depth, uncertainty, debris_profile: debris_profile.clone(), confidence, }) } } #[cfg(test)] mod tests { use super::*; fn default_debris() -> DebrisProfile { DebrisProfile { primary_material: DebrisMaterial::Mixed, void_fraction: 0.25, moisture_content: MoistureLevel::Dry, metal_content: crate::domain::MetalContent::Low, } } #[test] fn test_low_attenuation_surface() { let estimator = DepthEstimator::with_defaults(); let result = estimator.estimate_depth(1.0, 5.0, &default_debris()); assert!(result.is_some()); let estimate = result.unwrap(); assert!(estimate.depth < 0.1); assert!(estimate.confidence > 0.8); } #[test] fn test_depth_increases_with_attenuation() { let estimator = DepthEstimator::with_defaults(); let debris = default_debris(); let low = estimator.estimate_depth(10.0, 0.0, &debris); let high = estimator.estimate_depth(30.0, 0.0, &debris); assert!(low.is_some() && high.is_some()); assert!(high.unwrap().depth > low.unwrap().depth); } #[test] fn test_confidence_decreases_with_depth() { let estimator = DepthEstimator::with_defaults(); let debris = default_debris(); let shallow = estimator.estimate_depth(5.0, 0.0, &debris); let deep = estimator.estimate_depth(40.0, 0.0, &debris); if let (Some(s), Some(d)) = (shallow, deep) { assert!(s.confidence > d.confidence); } } #[test] fn test_debris_profile_estimation() { let estimator = DepthEstimator::with_defaults(); // High variance = mixed materials let profile = estimator.estimate_debris_profile(0.7, 0.5, 0.3); assert!(matches!(profile.primary_material, DebrisMaterial::Mixed)); // High multipath = concrete let profile2 = estimator.estimate_debris_profile(0.2, 0.8, 0.3); assert!(matches!( profile2.primary_material, DebrisMaterial::HeavyConcrete )); } #[test] fn test_free_space_path_loss() { let estimator = DepthEstimator::with_defaults(); // FSPL increases with distance let fspl_1m = estimator.free_space_path_loss(1.0); let fspl_10m = estimator.free_space_path_loss(10.0); assert!(fspl_10m > fspl_1m); // Should be about 20 dB difference (20*log10(10)) assert!((fspl_10m - fspl_1m - 20.0).abs() < 1.0); } }