use crate::domain::{DebrisMaterial, DebrisProfile, DepthEstimate, MoistureLevel};
#[derive(Debug, Clone)]
pub struct DepthEstimatorConfig {
pub max_depth: f64,
pub min_attenuation: f64,
pub frequency_ghz: f64,
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, free_space_loss_1m: 47.0, }
}
}
pub struct DepthEstimator {
config: DepthEstimatorConfig,
}
impl DepthEstimator {
pub fn new(config: DepthEstimatorConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(DepthEstimatorConfig::default())
}
pub fn estimate_depth(
&self,
signal_attenuation: f64, distance_2d: f64, debris_profile: &DebrisProfile,
) -> Option<DepthEstimate> {
if signal_attenuation < self.config.min_attenuation {
return Some(DepthEstimate {
depth: 0.0,
uncertainty: 0.5,
debris_profile: debris_profile.clone(),
confidence: 0.9,
});
}
let fspl = self.free_space_path_loss(distance_2d);
let debris_attenuation = (signal_attenuation - fspl).max(0.0);
let attenuation_per_meter = debris_profile.attenuation_factor();
if attenuation_per_meter < 0.1 {
return None;
}
let depth = debris_attenuation / attenuation_per_meter;
if depth > self.config.max_depth {
return None;
}
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;
let confidence = (1.0 - depth / self.config.max_depth).max(0.3);
Some(DepthEstimate {
depth,
uncertainty,
debris_profile: debris_profile.clone(),
confidence,
})
}
pub fn estimate_debris_profile(
&self,
signal_variance: f64,
signal_multipath: f64,
moisture_indicator: f64,
) -> DebrisProfile {
let primary_material = if signal_variance > 0.5 {
DebrisMaterial::Mixed
} else if signal_multipath > 0.7 {
DebrisMaterial::HeavyConcrete
} else if signal_multipath < 0.3 {
DebrisMaterial::Soil
} else {
DebrisMaterial::LightConcrete
};
let void_fraction = signal_multipath.clamp(0.1, 0.5);
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,
}
}
fn free_space_path_loss(&self, distance: f64) -> f64 {
if distance <= 0.0 {
return 0.0;
}
self.config.free_space_loss_1m + 20.0 * distance.log10()
}
fn material_uncertainty(&self, profile: &DebrisProfile) -> f64 {
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, };
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
}
pub fn estimate_from_multipath(
&self,
direct_path_attenuation: f64,
reflected_paths: &[(f64, f64)], debris_profile: &DebrisProfile,
) -> Option<DepthEstimate> {
if reflected_paths.is_empty() {
return self.estimate_depth(direct_path_attenuation, 0.0, debris_profile);
}
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) .sum::<f64>()
/ reflected_paths.len() as f64;
let estimated_depth = avg_extra_path / 4.0;
let attenuation_per_meter = debris_profile.attenuation_factor();
let attenuation_based_depth = direct_path_attenuation / attenuation_per_meter;
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();
let profile = estimator.estimate_debris_profile(0.7, 0.5, 0.3);
assert!(matches!(profile.primary_material, DebrisMaterial::Mixed));
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();
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);
assert!((fspl_10m - fspl_1m - 20.0).abs() < 1.0);
}
}