#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RangeConstraint {
pub anchor_id: u32,
pub anchor_pos: [f64; 3],
pub measured_range_m: f64,
pub uncertainty_m: f64,
pub signal_quality: f32,
pub at_ns: u64,
}
impl RangeConstraint {
#[must_use]
pub fn predicted_range(&self, p: [f64; 3]) -> f64 {
(0..3).map(|a| (p[a] - self.anchor_pos[a]).powi(2)).sum::<f64>().sqrt()
}
#[must_use]
pub fn residual(&self, p: [f64; 3]) -> f64 {
self.predicted_range(p) - self.measured_range_m
}
#[must_use]
pub fn mahalanobis(&self, p: [f64; 3]) -> f64 {
let u = self.uncertainty_m.max(1e-6);
self.residual(p).abs() / u
}
#[must_use]
pub fn is_consistent(&self, p: [f64; 3], gate_sigma: f64) -> bool {
self.mahalanobis(p) <= gate_sigma
}
}
#[derive(Debug, Clone)]
pub struct RefineResult {
pub position: [f64; 3],
pub rms_residual_sigma: f64,
pub rejected_anchors: Vec<u32>,
pub iterations: usize,
}
#[derive(Debug, Clone)]
pub struct RangeConstraintFusion {
pub gate_sigma: f64,
pub step: f64,
pub max_iters: usize,
pub tol_m: f64,
}
impl Default for RangeConstraintFusion {
fn default() -> Self {
Self { gate_sigma: 3.0, step: 1.0, max_iters: 200, tol_m: 1e-4 }
}
}
impl RangeConstraintFusion {
#[must_use]
pub fn refine(&self, prior: [f64; 3], constraints: &[RangeConstraint]) -> RefineResult {
let mut admitted: Vec<&RangeConstraint> = Vec::new();
let mut rejected_anchors = Vec::new();
for c in constraints {
if c.is_consistent(prior, self.gate_sigma) {
admitted.push(c);
} else {
rejected_anchors.push(c.anchor_id);
}
}
let mut p = prior;
let mut iterations = 0;
if !admitted.is_empty() {
for _ in 0..self.max_iters {
iterations += 1;
let mut grad = [0.0f64; 3];
let mut sum_w = 0.0f64;
for c in &admitted {
let d = c.predicted_range(p).max(1e-9);
let w = 1.0 / (c.uncertainty_m.max(1e-6)).powi(2);
sum_w += w;
let coeff = 2.0 * w * (d - c.measured_range_m) / d;
for a in 0..3 {
grad[a] += coeff * (p[a] - c.anchor_pos[a]);
}
}
let scale = self.step / (2.0 * sum_w.max(1e-12));
let mut upd_norm = 0.0;
for a in 0..3 {
let delta = scale * grad[a];
p[a] -= delta;
upd_norm += delta * delta;
}
if upd_norm.sqrt() < self.tol_m {
break;
}
}
}
let rms_residual_sigma = if admitted.is_empty() {
f64::INFINITY
} else {
let ss: f64 = admitted.iter().map(|c| c.mahalanobis(p).powi(2)).sum();
(ss / admitted.len() as f64).sqrt()
};
RefineResult { position: p, rms_residual_sigma, rejected_anchors, iterations }
}
#[must_use]
pub fn associate(&self, tracks: &[[f64; 3]], c: &RangeConstraint) -> Option<usize> {
tracks
.iter()
.enumerate()
.map(|(i, &t)| (i, c.mahalanobis(t)))
.filter(|(_, m)| *m <= self.gate_sigma)
.min_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
.map(|(i, _)| i)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rc(id: u32, pos: [f64; 3], range: f64) -> RangeConstraint {
RangeConstraint {
anchor_id: id,
anchor_pos: pos,
measured_range_m: range,
uncertainty_m: 0.1,
signal_quality: 0.9,
at_ns: 0,
}
}
#[test]
fn refine_converges_to_true_point() {
let truth: [f64; 3] = [2.0, 2.0, 0.0];
let anchors: [[f64; 3]; 3] = [[0.0, 0.0, 0.0], [4.0, 0.0, 0.0], [0.0, 4.0, 0.0]];
let constraints: Vec<RangeConstraint> = anchors
.iter()
.enumerate()
.map(|(i, &a)| {
let r = ((truth[0] - a[0]).powi(2) + (truth[1] - a[1]).powi(2) + (truth[2] - a[2]).powi(2)).sqrt();
RangeConstraint { uncertainty_m: 0.3, ..rc(i as u32, a, r) }
})
.collect();
let fusion = RangeConstraintFusion::default();
let res = fusion.refine([1.5, 1.5, 0.0], &constraints);
let err = ((res.position[0] - 2.0).powi(2) + (res.position[1] - 2.0).powi(2)).sqrt();
assert!(err < 0.05, "refined within 5 cm of truth, got err={err}");
assert!(res.rejected_anchors.is_empty());
assert!(res.rms_residual_sigma < 1.0);
}
#[test]
fn inconsistent_constraint_is_gated_out() {
let mut constraints = vec![
rc(0, [0.0, 0.0, 0.0], 2.83),
rc(1, [4.0, 0.0, 0.0], 2.83),
];
constraints.push(rc(9, [0.0, 4.0, 0.0], 100.0)); let fusion = RangeConstraintFusion::default();
let res = fusion.refine([2.0, 2.0, 0.0], &constraints);
assert!(res.rejected_anchors.contains(&9), "absurd range gated out");
}
#[test]
fn consistency_gate_and_residual() {
let c = rc(0, [0.0, 0.0, 0.0], 5.0);
assert!(c.residual([5.0, 0.0, 0.0]).abs() < 1e-9);
assert!(c.is_consistent([5.0, 0.0, 0.0], 3.0));
assert!(!c.is_consistent([5.5, 0.0, 0.0], 3.0));
assert!((c.mahalanobis([5.5, 0.0, 0.0]) - 5.0).abs() < 1e-6);
}
#[test]
fn associate_picks_nearest_consistent_track() {
let c = rc(0, [0.0, 0.0, 0.0], 3.0); let fusion = RangeConstraintFusion::default();
let tracks = [[3.0, 0.0, 0.0], [8.0, 0.0, 0.0]];
assert_eq!(fusion.associate(&tracks, &c), Some(0));
let far = [[20.0, 0.0, 0.0], [25.0, 0.0, 0.0]];
assert_eq!(fusion.associate(&far, &c), None);
}
}