Skip to main content

wifi_densepose_mat/localization/
fusion.rs

1//! Position fusion combining multiple localization techniques.
2
3use crate::domain::{
4    Coordinates3D, LocationUncertainty, ScanZone, VitalSignsReading,
5    DepthEstimate, DebrisProfile,
6};
7use super::{Triangulator, TriangulationConfig, DepthEstimator, DepthEstimatorConfig};
8
9/// Service for survivor localization
10pub struct LocalizationService {
11    triangulator: Triangulator,
12    depth_estimator: DepthEstimator,
13    position_fuser: PositionFuser,
14}
15
16impl LocalizationService {
17    /// Create a new localization service
18    pub fn new() -> Self {
19        Self {
20            triangulator: Triangulator::with_defaults(),
21            depth_estimator: DepthEstimator::with_defaults(),
22            position_fuser: PositionFuser::new(),
23        }
24    }
25
26    /// Create with custom configurations
27    pub fn with_config(
28        triangulation_config: TriangulationConfig,
29        depth_config: DepthEstimatorConfig,
30    ) -> Self {
31        Self {
32            triangulator: Triangulator::new(triangulation_config),
33            depth_estimator: DepthEstimator::new(depth_config),
34            position_fuser: PositionFuser::new(),
35        }
36    }
37
38    /// Estimate survivor position
39    pub fn estimate_position(
40        &self,
41        vitals: &VitalSignsReading,
42        zone: &ScanZone,
43    ) -> Option<Coordinates3D> {
44        // Get sensor positions
45        let sensors = zone.sensor_positions();
46
47        if sensors.len() < 3 {
48            return None;
49        }
50
51        // Estimate 2D position from triangulation
52        // In real implementation, RSSI values would come from actual measurements
53        let rssi_values = self.simulate_rssi_measurements(sensors, vitals);
54        let position_2d = self.triangulator.estimate_position(sensors, &rssi_values)?;
55
56        // Estimate depth
57        let debris_profile = self.estimate_debris_profile(zone);
58        let signal_attenuation = self.calculate_signal_attenuation(&rssi_values);
59        let depth_estimate = self.depth_estimator.estimate_depth(
60            signal_attenuation,
61            0.0,
62            &debris_profile,
63        )?;
64
65        // Combine into 3D position
66        let position_3d = Coordinates3D::new(
67            position_2d.x,
68            position_2d.y,
69            -depth_estimate.depth, // Negative = below surface
70            self.combine_uncertainties(&position_2d.uncertainty, &depth_estimate),
71        );
72
73        Some(position_3d)
74    }
75
76    /// Read RSSI measurements from sensors.
77    ///
78    /// Returns empty when no real sensor hardware is connected.
79    /// Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013).
80    /// Caller handles empty readings by returning None/default.
81    fn simulate_rssi_measurements(
82        &self,
83        _sensors: &[crate::domain::SensorPosition],
84        _vitals: &VitalSignsReading,
85    ) -> Vec<(String, f64)> {
86        // No real sensor hardware connected - return empty.
87        // Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013).
88        // Caller handles empty readings by returning None from estimate_position.
89        tracing::warn!("No sensor hardware connected. Real RSSI readings require ESP32 mesh (ADR-012) or Linux WiFi interface (ADR-013).");
90        vec![]
91    }
92
93    /// Estimate debris profile for the zone
94    fn estimate_debris_profile(&self, _zone: &ScanZone) -> DebrisProfile {
95        // Would use zone metadata and signal analysis
96        DebrisProfile::default()
97    }
98
99    /// Calculate average signal attenuation
100    fn calculate_signal_attenuation(&self, rssi_values: &[(String, f64)]) -> f64 {
101        if rssi_values.is_empty() {
102            return 0.0;
103        }
104
105        // Reference RSSI at surface (typical open-air value)
106        const REFERENCE_RSSI: f64 = -30.0;
107
108        let avg_rssi: f64 = rssi_values.iter().map(|(_, r)| r).sum::<f64>()
109            / rssi_values.len() as f64;
110
111        (REFERENCE_RSSI - avg_rssi).max(0.0)
112    }
113
114    /// Combine horizontal and depth uncertainties
115    fn combine_uncertainties(
116        &self,
117        horizontal: &LocationUncertainty,
118        depth: &DepthEstimate,
119    ) -> LocationUncertainty {
120        LocationUncertainty {
121            horizontal_error: horizontal.horizontal_error,
122            vertical_error: depth.uncertainty,
123            confidence: (horizontal.confidence * depth.confidence).sqrt(),
124        }
125    }
126}
127
128impl Default for LocalizationService {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134/// Fuses multiple position estimates
135pub struct PositionFuser {
136    /// History of position estimates for smoothing
137    history: parking_lot::RwLock<Vec<PositionEstimate>>,
138    /// Maximum history size
139    max_history: usize,
140}
141
142/// A position estimate with metadata
143#[derive(Debug, Clone)]
144pub struct PositionEstimate {
145    /// The position
146    pub position: Coordinates3D,
147    /// Timestamp
148    pub timestamp: chrono::DateTime<chrono::Utc>,
149    /// Source of estimate
150    pub source: EstimateSource,
151    /// Weight for fusion
152    pub weight: f64,
153}
154
155/// Source of a position estimate
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum EstimateSource {
158    /// From RSSI-based triangulation
159    RssiTriangulation,
160    /// From time-of-arrival
161    TimeOfArrival,
162    /// From CSI fingerprinting
163    CsiFingerprint,
164    /// From angle of arrival
165    AngleOfArrival,
166    /// From depth estimation
167    DepthEstimation,
168    /// Fused from multiple sources
169    Fused,
170}
171
172impl PositionFuser {
173    /// Create a new position fuser
174    pub fn new() -> Self {
175        Self {
176            history: parking_lot::RwLock::new(Vec::new()),
177            max_history: 20,
178        }
179    }
180
181    /// Add a position estimate
182    pub fn add_estimate(&self, estimate: PositionEstimate) {
183        let mut history = self.history.write();
184        history.push(estimate);
185
186        // Keep only recent history
187        if history.len() > self.max_history {
188            history.remove(0);
189        }
190    }
191
192    /// Fuse multiple position estimates into one
193    pub fn fuse(&self, estimates: &[PositionEstimate]) -> Option<Coordinates3D> {
194        if estimates.is_empty() {
195            return None;
196        }
197
198        if estimates.len() == 1 {
199            return Some(estimates[0].position.clone());
200        }
201
202        // Weighted average based on uncertainty and source confidence
203        let mut total_weight = 0.0;
204        let mut sum_x = 0.0;
205        let mut sum_y = 0.0;
206        let mut sum_z = 0.0;
207
208        for estimate in estimates {
209            let weight = self.calculate_weight(estimate);
210            total_weight += weight;
211            sum_x += estimate.position.x * weight;
212            sum_y += estimate.position.y * weight;
213            sum_z += estimate.position.z * weight;
214        }
215
216        if total_weight == 0.0 {
217            return None;
218        }
219
220        let fused_x = sum_x / total_weight;
221        let fused_y = sum_y / total_weight;
222        let fused_z = sum_z / total_weight;
223
224        // Calculate fused uncertainty (reduced due to multiple estimates)
225        let fused_uncertainty = self.calculate_fused_uncertainty(estimates);
226
227        Some(Coordinates3D::new(
228            fused_x,
229            fused_y,
230            fused_z,
231            fused_uncertainty,
232        ))
233    }
234
235    /// Fuse with temporal smoothing
236    pub fn fuse_with_history(&self, current: &PositionEstimate) -> Option<Coordinates3D> {
237        // Add current to history
238        self.add_estimate(current.clone());
239
240        let history = self.history.read();
241
242        // Use exponentially weighted moving average
243        let alpha: f64 = 0.3; // Smoothing factor
244        let mut smoothed = current.position.clone();
245
246        for (i, estimate) in history.iter().rev().enumerate().skip(1) {
247            let weight = alpha * (1.0_f64 - alpha).powi(i as i32);
248            smoothed.x = smoothed.x * (1.0 - weight) + estimate.position.x * weight;
249            smoothed.y = smoothed.y * (1.0 - weight) + estimate.position.y * weight;
250            smoothed.z = smoothed.z * (1.0 - weight) + estimate.position.z * weight;
251        }
252
253        Some(smoothed)
254    }
255
256    /// Calculate weight for an estimate
257    fn calculate_weight(&self, estimate: &PositionEstimate) -> f64 {
258        // Base weight from source reliability
259        let source_weight = match estimate.source {
260            EstimateSource::TimeOfArrival => 1.0,
261            EstimateSource::AngleOfArrival => 0.9,
262            EstimateSource::CsiFingerprint => 0.8,
263            EstimateSource::RssiTriangulation => 0.7,
264            EstimateSource::DepthEstimation => 0.6,
265            EstimateSource::Fused => 1.0,
266        };
267
268        // Adjust by uncertainty (lower uncertainty = higher weight)
269        let uncertainty_factor = 1.0 / (1.0 + estimate.position.uncertainty.horizontal_error);
270
271        // User-provided weight
272        let user_weight = estimate.weight;
273
274        source_weight * uncertainty_factor * user_weight
275    }
276
277    /// Calculate uncertainty after fusing multiple estimates
278    fn calculate_fused_uncertainty(&self, estimates: &[PositionEstimate]) -> LocationUncertainty {
279        if estimates.is_empty() {
280            return LocationUncertainty::default();
281        }
282
283        // Combined uncertainty is reduced with multiple estimates
284        let n = estimates.len() as f64;
285
286        let avg_h_error: f64 = estimates.iter()
287            .map(|e| e.position.uncertainty.horizontal_error)
288            .sum::<f64>() / n;
289
290        let avg_v_error: f64 = estimates.iter()
291            .map(|e| e.position.uncertainty.vertical_error)
292            .sum::<f64>() / n;
293
294        // Uncertainty reduction factor (more estimates = more confidence)
295        let reduction = (1.0 / n.sqrt()).max(0.5);
296
297        LocationUncertainty {
298            horizontal_error: avg_h_error * reduction,
299            vertical_error: avg_v_error * reduction,
300            confidence: (0.95 * (1.0 + (n - 1.0) * 0.02)).min(0.99),
301        }
302    }
303
304    /// Clear history
305    pub fn clear_history(&self) {
306        self.history.write().clear();
307    }
308}
309
310impl Default for PositionFuser {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320    use chrono::Utc;
321
322    fn create_test_estimate(x: f64, y: f64, z: f64) -> PositionEstimate {
323        PositionEstimate {
324            position: Coordinates3D::with_default_uncertainty(x, y, z),
325            timestamp: Utc::now(),
326            source: EstimateSource::RssiTriangulation,
327            weight: 1.0,
328        }
329    }
330
331    #[test]
332    fn test_single_estimate_fusion() {
333        let fuser = PositionFuser::new();
334        let estimate = create_test_estimate(5.0, 10.0, -2.0);
335
336        let result = fuser.fuse(&[estimate]);
337        assert!(result.is_some());
338
339        let pos = result.unwrap();
340        assert!((pos.x - 5.0).abs() < 0.001);
341    }
342
343    #[test]
344    fn test_multiple_estimate_fusion() {
345        let fuser = PositionFuser::new();
346
347        let estimates = vec![
348            create_test_estimate(4.0, 9.0, -1.5),
349            create_test_estimate(6.0, 11.0, -2.5),
350        ];
351
352        let result = fuser.fuse(&estimates);
353        assert!(result.is_some());
354
355        let pos = result.unwrap();
356        // Should be roughly in between
357        assert!(pos.x > 4.0 && pos.x < 6.0);
358        assert!(pos.y > 9.0 && pos.y < 11.0);
359    }
360
361    #[test]
362    fn test_fused_uncertainty_reduction() {
363        let fuser = PositionFuser::new();
364
365        let estimates = vec![
366            create_test_estimate(5.0, 10.0, -2.0),
367            create_test_estimate(5.1, 10.1, -2.1),
368            create_test_estimate(4.9, 9.9, -1.9),
369        ];
370
371        let single_uncertainty = estimates[0].position.uncertainty.horizontal_error;
372        let fused_uncertainty = fuser.calculate_fused_uncertainty(&estimates);
373
374        // Fused should have lower uncertainty
375        assert!(fused_uncertainty.horizontal_error < single_uncertainty);
376    }
377
378    #[test]
379    fn test_localization_service_creation() {
380        let service = LocalizationService::new();
381        // Just verify it creates without panic
382        assert!(true);
383        drop(service);
384    }
385}