use crate::domain::{Coordinates3D, LocationUncertainty, SensorPosition};
#[derive(Debug, Clone)]
pub struct TriangulationConfig {
pub min_sensors: usize,
pub max_uncertainty: f64,
pub path_loss_exponent: f64,
pub reference_distance: f64,
pub reference_rssi: f64,
pub weighted: bool,
}
impl Default for TriangulationConfig {
fn default() -> Self {
Self {
min_sensors: 3,
max_uncertainty: 5.0,
path_loss_exponent: 3.0, reference_distance: 1.0,
reference_rssi: -30.0,
weighted: true,
}
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct DistanceEstimate {
pub sensor_id: String,
pub distance: f64,
pub confidence: f64,
}
pub struct Triangulator {
config: TriangulationConfig,
}
impl Triangulator {
pub fn new(config: TriangulationConfig) -> Self {
Self { config }
}
pub fn with_defaults() -> Self {
Self::new(TriangulationConfig::default())
}
pub fn config(&self) -> &TriangulationConfig {
&self.config
}
pub fn estimate_position(
&self,
sensors: &[SensorPosition],
rssi_values: &[(String, f64)], ) -> Option<Coordinates3D> {
let distances: Vec<(SensorPosition, f64)> = rssi_values
.iter()
.filter_map(|(id, rssi)| {
let sensor = sensors.iter().find(|s| &s.id == id)?;
if !sensor.is_operational {
return None;
}
let distance = self.rssi_to_distance(*rssi);
Some((sensor.clone(), distance))
})
.collect();
if distances.len() < self.config.min_sensors {
return None;
}
self.trilaterate(&distances)
}
pub fn estimate_from_toa(
&self,
sensors: &[SensorPosition],
toa_values: &[(String, f64)], ) -> Option<Coordinates3D> {
const SPEED_OF_LIGHT: f64 = 299_792_458.0;
let distances: Vec<(SensorPosition, f64)> = toa_values
.iter()
.filter_map(|(id, toa)| {
let sensor = sensors.iter().find(|s| &s.id == id)?;
if !sensor.is_operational {
return None;
}
let distance = (*toa * 1e-9) * SPEED_OF_LIGHT / 2.0; Some((sensor.clone(), distance))
})
.collect();
if distances.len() < self.config.min_sensors {
return None;
}
self.trilaterate(&distances)
}
fn rssi_to_distance(&self, rssi: f64) -> f64 {
let exponent =
(self.config.reference_rssi - rssi) / (10.0 * self.config.path_loss_exponent);
self.config.reference_distance * 10.0_f64.powf(exponent)
}
fn trilaterate(&self, distances: &[(SensorPosition, f64)]) -> Option<Coordinates3D> {
if distances.len() < 3 {
return None;
}
let (ref_sensor, ref_dist) = &distances[0];
let x1 = ref_sensor.x;
let y1 = ref_sensor.y;
let r1 = *ref_dist;
let n = distances.len() - 1;
let mut a_matrix = vec![vec![0.0; 2]; n];
let mut b_vector = vec![0.0; n];
for (i, (sensor, dist)) in distances.iter().skip(1).enumerate() {
let xi = sensor.x;
let yi = sensor.y;
let ri = *dist;
a_matrix[i][0] = 2.0 * (xi - x1);
a_matrix[i][1] = 2.0 * (yi - y1);
b_vector[i] = r1 * r1 - ri * ri - x1 * x1 + xi * xi - y1 * y1 + yi * yi;
}
let solution = self.solve_least_squares(&a_matrix, &b_vector)?;
let uncertainty = self.calculate_uncertainty(&solution, distances);
if uncertainty.horizontal_error > self.config.max_uncertainty {
return None;
}
Some(Coordinates3D::new(
solution[0],
solution[1],
0.0, uncertainty,
))
}
#[allow(clippy::needless_range_loop)]
fn solve_least_squares(&self, a: &[Vec<f64>], b: &[f64]) -> Option<Vec<f64>> {
let n = a.len();
if n < 2 || a[0].len() != 2 {
return None;
}
let mut ata = vec![vec![0.0; 2]; 2];
for i in 0..2 {
for j in 0..2 {
for k in 0..n {
ata[i][j] += a[k][i] * a[k][j];
}
}
}
let mut atb = [0.0; 2];
for i in 0..2 {
for k in 0..n {
atb[i] += a[k][i] * b[k];
}
}
let det = ata[0][0] * ata[1][1] - ata[0][1] * ata[1][0];
if det.abs() < 1e-10 {
return None;
}
let x = (atb[0] * ata[1][1] - atb[1] * ata[0][1]) / det;
let y = (ata[0][0] * atb[1] - ata[1][0] * atb[0]) / det;
Some(vec![x, y])
}
fn calculate_uncertainty(
&self,
position: &[f64],
distances: &[(SensorPosition, f64)],
) -> LocationUncertainty {
let mut sum_sq_error = 0.0;
for (sensor, measured_dist) in distances {
let dx = position[0] - sensor.x;
let dy = position[1] - sensor.y;
let estimated_dist = (dx * dx + dy * dy).sqrt();
let error = measured_dist - estimated_dist;
sum_sq_error += error * error;
}
let rmse = (sum_sq_error / distances.len() as f64).sqrt();
let gdop = self
.compute_gdop(position, distances)
.unwrap_or(1.0)
.max(1.0);
LocationUncertainty {
horizontal_error: rmse * gdop,
vertical_error: rmse * gdop * 1.5, confidence: 0.95,
}
}
fn compute_gdop(&self, position: &[f64], distances: &[(SensorPosition, f64)]) -> Option<f64> {
let (tx, ty) = (position[0], position[1]);
let (mut hxx, mut hxy, mut hyy) = (0.0_f64, 0.0_f64, 0.0_f64);
let mut rows = 0usize;
for (sensor, _dist) in distances {
let dx = sensor.x - tx;
let dy = sensor.y - ty;
let d = (dx * dx + dy * dy).sqrt();
if d <= f64::EPSILON {
continue; }
let ux = dx / d;
let uy = dy / d;
hxx += ux * ux;
hxy += ux * uy;
hyy += uy * uy;
rows += 1;
}
if rows < 2 {
return None;
}
let det = hxx * hyy - hxy * hxy;
if det.abs() < 1e-12 {
return None; }
let trace_inv = (hxx + hyy) / det;
if trace_inv <= 0.0 {
return None;
}
Some(trace_inv.sqrt())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::SensorType;
fn create_test_sensors() -> Vec<SensorPosition> {
vec![
SensorPosition {
id: "s1".to_string(),
x: 0.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
last_rssi: None,
},
SensorPosition {
id: "s2".to_string(),
x: 10.0,
y: 0.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
last_rssi: None,
},
SensorPosition {
id: "s3".to_string(),
x: 5.0,
y: 10.0,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
last_rssi: None,
},
]
}
#[test]
fn test_rssi_to_distance() {
let triangulator = Triangulator::with_defaults();
let distance = triangulator.rssi_to_distance(-30.0);
assert!((distance - 1.0).abs() < 0.1);
let distance2 = triangulator.rssi_to_distance(-60.0);
assert!(distance2 > distance);
}
#[test]
fn test_trilateration() {
let triangulator = Triangulator::with_defaults();
let sensors = create_test_sensors();
let target: (f64, f64) = (5.0, 4.0);
let distances: Vec<(&str, f64)> = vec![
(
"s1",
((target.0 - 0.0_f64).powi(2) + (target.1 - 0.0_f64).powi(2)).sqrt(),
),
(
"s2",
((target.0 - 10.0_f64).powi(2) + (target.1 - 0.0_f64).powi(2)).sqrt(),
),
(
"s3",
((target.0 - 5.0_f64).powi(2) + (target.1 - 10.0_f64).powi(2)).sqrt(),
),
];
let dist_vec: Vec<(SensorPosition, f64)> = distances
.iter()
.filter_map(|(id, d)| {
let sensor = sensors.iter().find(|s| s.id == *id)?;
Some((sensor.clone(), *d))
})
.collect();
let result = triangulator.trilaterate(&dist_vec);
assert!(result.is_some());
let pos = result.unwrap();
assert!((pos.x - target.0).abs() < 0.5);
assert!((pos.y - target.1).abs() < 0.5);
}
#[test]
fn test_insufficient_sensors() {
let triangulator = Triangulator::with_defaults();
let sensors = create_test_sensors();
let rssi_values = vec![("s1".to_string(), -40.0), ("s2".to_string(), -45.0)];
let result = triangulator.estimate_position(&sensors, &rssi_values);
assert!(result.is_none());
}
fn sensor_at(id: &str, x: f64, y: f64) -> SensorPosition {
SensorPosition {
id: id.to_string(),
x,
y,
z: 1.5,
sensor_type: SensorType::Transceiver,
is_operational: true,
last_rssi: None,
}
}
#[test]
fn test_gdop_is_real_dilution() {
let t = Triangulator::with_defaults();
let target = [5.0_f64, 5.0_f64];
let good = vec![
(sensor_at("a", 5.0, 15.0), 10.0),
(sensor_at("b", -3.66, 0.0), 10.0),
(sensor_at("c", 13.66, 0.0), 10.0),
];
let gdop_good = t.compute_gdop(&target, &good).expect("good geometry");
let bad = vec![
(sensor_at("a", 5.3, 15.0), 10.0),
(sensor_at("b", 4.7, 15.0), 10.0),
(sensor_at("c", 5.1, -5.0), 10.0),
];
let gdop_bad = t.compute_gdop(&target, &bad).expect("bad geometry");
assert!(gdop_good >= 1.0, "GDOP must be >= 1 (dilution, dimensionless)");
assert!(
gdop_good < gdop_bad,
"well-spread GDOP {gdop_good} must be < near-collinear GDOP {gdop_bad}"
);
let (mut hxx, mut hxy, mut hyy) = (0.0, 0.0, 0.0);
for (s, _d) in &good {
let dx = s.x - target[0];
let dy = s.y - target[1];
let d = (dx * dx + dy * dy).sqrt();
let (ux, uy) = (dx / d, dy / d);
hxx += ux * ux;
hxy += ux * uy;
hyy += uy * uy;
}
let det = hxx * hyy - hxy * hxy;
let expected = ((hxx + hyy) / det).sqrt();
assert!((gdop_good - expected).abs() < 1e-9);
}
#[test]
fn test_gdop_singular_collinear_is_none() {
let t = Triangulator::with_defaults();
let target = [0.0_f64, 0.0_f64];
let collinear = vec![
(sensor_at("a", 1.0, 0.0), 1.0),
(sensor_at("b", 2.0, 0.0), 2.0),
(sensor_at("c", 3.0, 0.0), 3.0),
];
assert!(t.compute_gdop(&target, &collinear).is_none());
}
}
#[cfg(feature = "ruvector")]
use ruvector_solver::neumann::NeumannSolver;
#[cfg(feature = "ruvector")]
use ruvector_solver::types::CsrMatrix;
#[cfg(feature = "ruvector")]
pub fn solve_tdoa_triangulation(
tdoa_measurements: &[(usize, usize, f32)],
ap_positions: &[(f32, f32)],
) -> Option<(f32, f32)> {
let n_meas = tdoa_measurements.len();
if n_meas < 3 || ap_positions.len() < 2 {
return None;
}
const C: f32 = 3e8_f32; let (x_ref, y_ref) = ap_positions[0];
let mut ata = [[0.0_f32; 2]; 2];
let mut atb = [0.0_f32; 2];
for &(i, j, tdoa) in tdoa_measurements {
let (xi, yi) = ap_positions.get(i).copied().unwrap_or((x_ref, y_ref));
let (xj, yj) = ap_positions.get(j).copied().unwrap_or((x_ref, y_ref));
let ai0 = xi - xj;
let ai1 = yi - yj;
let bi = C * tdoa / 2.0 + ((xi * xi - xj * xj) + (yi * yi - yj * yj)) / 2.0
- x_ref * ai0
- y_ref * ai1;
ata[0][0] += ai0 * ai0;
ata[0][1] += ai0 * ai1;
ata[1][0] += ai1 * ai0;
ata[1][1] += ai1 * ai1;
atb[0] += ai0 * bi;
atb[1] += ai1 * bi;
}
let lambda = 0.01_f32;
ata[0][0] += lambda;
ata[1][1] += lambda;
let csr = CsrMatrix::<f32>::from_coo(
2,
2,
vec![
(0, 0, ata[0][0]),
(0, 1, ata[0][1]),
(1, 0, ata[1][0]),
(1, 1, ata[1][1]),
],
);
if let Ok(r) = NeumannSolver::new(1e-5, 500).solve(&csr, &atb) {
return Some((r.solution[0] + x_ref, r.solution[1] + y_ref));
}
let det = ata[0][0] * ata[1][1] - ata[0][1] * ata[1][0];
if det.abs() < 1e-10 {
return None;
}
let x_sol = (atb[0] * ata[1][1] - atb[1] * ata[0][1]) / det;
let y_sol = (ata[0][0] * atb[1] - ata[1][0] * atb[0]) / det;
Some((x_sol + x_ref, y_sol + y_ref))
}
#[cfg(all(test, feature = "ruvector"))]
mod triangulation_tests {
use super::*;
#[test]
fn tdoa_triangulation_insufficient_data() {
let result = solve_tdoa_triangulation(&[(0, 1, 1e-9)], &[(0.0, 0.0), (5.0, 0.0)]);
assert!(result.is_none());
}
#[test]
fn tdoa_triangulation_symmetric_case() {
let aps = vec![(0.0_f32, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0)];
let measurements = vec![
(0_usize, 1_usize, 0.0_f32),
(1, 2, 0.0),
(2, 3, 0.0),
(0, 3, 0.0),
];
let result = solve_tdoa_triangulation(&measurements, &aps);
assert!(result.is_some(), "should solve symmetric case");
let (x, y) = result.unwrap();
assert!(x.is_finite() && y.is_finite());
}
}