pub type Vec3 = [f64; 3];
pub const C: f64 = 299_792_458.0;
fn sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn norm(a: Vec3) -> f64 {
(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]).sqrt()
}
fn dot(a: Vec3, b: Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn los(p: Vec3, r: Vec3) -> Vec3 {
let d = sub(p, r);
let n = norm(d);
[d[0] / n, d[1] / n, d[2] / n]
}
pub fn tdoa_predict(p: Vec3, receivers: &[Vec3]) -> Vec<f64> {
let r0 = norm(sub(p, receivers[0]));
receivers[1..]
.iter()
.map(|&ri| (norm(sub(p, ri)) - r0) / C)
.collect()
}
pub fn fdoa_predict(p: Vec3, v: Vec3, receivers: &[Vec3], recv_vel: &[Vec3]) -> Vec<f64> {
let rr = |i: usize| dot(los(p, receivers[i]), sub(v, recv_vel[i]));
let rr0 = rr(0);
(1..receivers.len()).map(|i| rr(i) - rr0).collect()
}
pub fn solve_tdoa(receivers: &[Vec3], tdoa: &[f64], sigma_s: f64, x0: Vec3) -> Option<Vec3> {
if receivers.len() < 4 || tdoa.len() != receivers.len() - 1 {
return None;
}
let recv = receivers.to_vec();
let model = move |x: &[f64]| tdoa_predict([x[0], x[1], x[2]], &recv);
let w = vec![1.0 / (sigma_s * sigma_s); tdoa.len()];
let res = crate::batch_ls::gauss_newton(model, tdoa, &w, &x0, 100, 1e-9)?;
Some([res.x[0], res.x[1], res.x[2]])
}
#[allow(clippy::too_many_arguments)]
pub fn solve_tdoa_fdoa(
receivers: &[Vec3],
recv_vel: &[Vec3],
tdoa: &[f64],
fdoa: &[f64],
sigma_s: f64,
sigma_rr: f64,
x0: [f64; 6],
) -> Option<[f64; 6]> {
let k = receivers.len();
if k < 4 || recv_vel.len() != k || tdoa.len() != k - 1 || fdoa.len() != k - 1 {
return None;
}
let recv = receivers.to_vec();
let rvel = recv_vel.to_vec();
let model = move |x: &[f64]| {
let p = [x[0], x[1], x[2]];
let v = [x[3], x[4], x[5]];
let mut out = tdoa_predict(p, &recv);
out.extend(fdoa_predict(p, v, &recv, &rvel));
out
};
let mut z = tdoa.to_vec();
z.extend_from_slice(fdoa);
let mut w = vec![1.0 / (sigma_s * sigma_s); tdoa.len()];
w.extend(vec![1.0 / (sigma_rr * sigma_rr); fdoa.len()]);
let p0 = solve_tdoa(receivers, tdoa, sigma_s, [x0[0], x0[1], x0[2]])
.unwrap_or([x0[0], x0[1], x0[2]]);
let seed = [p0[0], p0[1], p0[2], x0[3], x0[4], x0[5]];
let res = crate::batch_ls::gauss_newton(model, &z, &w, &seed, 200, 1e-9)?;
let x = res.x;
Some([x[0], x[1], x[2], x[3], x[4], x[5]])
}
fn inverse3(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
- m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
+ m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]);
if det.abs() < 1e-18 {
return None;
}
let inv_det = 1.0 / det;
let c = |a: usize, b: usize, d: usize, e: usize| m[a][b] * m[d][e];
let mut out = [[0.0f64; 3]; 3];
out[0][0] = (c(1, 1, 2, 2) - c(1, 2, 2, 1)) * inv_det;
out[0][1] = (c(0, 2, 2, 1) - c(0, 1, 2, 2)) * inv_det;
out[0][2] = (c(0, 1, 1, 2) - c(0, 2, 1, 1)) * inv_det;
out[1][0] = (c(1, 2, 2, 0) - c(1, 0, 2, 2)) * inv_det;
out[1][1] = (c(0, 0, 2, 2) - c(0, 2, 2, 0)) * inv_det;
out[1][2] = (c(0, 2, 1, 0) - c(0, 0, 1, 2)) * inv_det;
out[2][0] = (c(1, 0, 2, 1) - c(1, 1, 2, 0)) * inv_det;
out[2][1] = (c(0, 1, 2, 0) - c(0, 0, 2, 1)) * inv_det;
out[2][2] = (c(0, 0, 1, 1) - c(0, 1, 1, 0)) * inv_det;
Some(out)
}
pub fn tdoa_fisher(receivers: &[Vec3], emitter: Vec3, sigma_s: f64) -> [[f64; 3]; 3] {
let sigma_rho = C * sigma_s; let inv_var = 1.0 / (sigma_rho * sigma_rho);
let u0 = los(emitter, receivers[0]);
let mut j = [[0.0f64; 3]; 3];
for &ri in &receivers[1..] {
let ui = los(emitter, ri);
let g = [ui[0] - u0[0], ui[1] - u0[1], ui[2] - u0[2]];
for a in 0..3 {
for b in 0..3 {
j[a][b] += g[a] * g[b] * inv_var;
}
}
}
j
}
pub fn tdoa_crlb(receivers: &[Vec3], emitter: Vec3, sigma_s: f64) -> Option<[[f64; 3]; 3]> {
inverse3(&tdoa_fisher(receivers, emitter, sigma_s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tdoa_forward_then_inverse_recovers_a_known_emitter() {
let receivers = [
[0.0, 0.0, 0.0],
[10_000.0, 0.0, 0.0],
[0.0, 10_000.0, 0.0],
[0.0, 0.0, 10_000.0],
[10_000.0, 10_000.0, 0.0],
];
let emitter = [3_200.0, 4_100.0, 1_500.0];
let tdoa = tdoa_predict(emitter, &receivers);
let got = solve_tdoa(&receivers, &tdoa, 1e-9, [0.0, 0.0, 0.0]).expect("solves");
let err = norm(sub(got, emitter));
assert!(err < 1e-6, "recovered {got:?} vs {emitter:?} (err {err} m)");
}
#[test]
fn crlb_is_the_inverse_of_the_fisher_information() {
let receivers = [
[0.0, 0.0, 0.0],
[12_000.0, 0.0, 500.0],
[0.0, 9_000.0, -300.0],
[-8_000.0, 4_000.0, 200.0],
[5_000.0, -7_000.0, 100.0],
];
let emitter = [2_000.0, 1_500.0, 800.0];
let j = tdoa_fisher(&receivers, emitter, 5e-9);
let cov = tdoa_crlb(&receivers, emitter, 5e-9).expect("non-singular geometry");
#[allow(clippy::needless_range_loop)]
for a in 0..3 {
for b in 0..3 {
let mut v = 0.0;
for k in 0..3 {
v += j[a][k] * cov[k][b];
}
let expected = if a == b { 1.0 } else { 0.0 };
assert!((v - expected).abs() < 1e-6, "J·CRLB[{a}][{b}]={v}");
}
}
assert!((cov[0][1] - cov[1][0]).abs() < 1e-9);
assert!((cov[0][2] - cov[2][0]).abs() < 1e-9);
assert!((cov[1][2] - cov[2][1]).abs() < 1e-9);
}
#[test]
fn adding_a_receiver_does_not_worsen_the_position_bound() {
let base = [
[0.0, 0.0, 0.0],
[12_000.0, 0.0, 500.0],
[0.0, 9_000.0, -300.0],
[-8_000.0, 4_000.0, 200.0],
];
let emitter = [2_000.0, 1_500.0, 800.0];
let trace = |c: [[f64; 3]; 3]| c[0][0] + c[1][1] + c[2][2];
let t4 = trace(tdoa_crlb(&base, emitter, 5e-9).expect("4 rx"));
let mut more = base.to_vec();
more.push([5_000.0, -7_000.0, 100.0]);
let t5 = trace(tdoa_crlb(&more, emitter, 5e-9).expect("5 rx"));
assert!(
t5 <= t4 + 1e-6,
"adding a receiver worsened the bound: {t5} > {t4}"
);
}
#[test]
fn tdoa_fdoa_recovers_a_moving_emitter_position_and_velocity() {
let receivers = [
[0.0, 0.0, 0.0],
[10_000.0, 0.0, 0.0],
[0.0, 10_000.0, 0.0],
[0.0, 0.0, 10_000.0],
[10_000.0, 10_000.0, 5_000.0],
];
let recv_vel = [
[200.0, 0.0, 0.0],
[0.0, 220.0, 0.0],
[-180.0, 0.0, 0.0],
[0.0, -210.0, 0.0],
[150.0, 150.0, 0.0],
];
let emitter = [3_200.0, 4_100.0, 1_500.0];
let vel = [12.0, -7.0, 3.0];
let tdoa = tdoa_predict(emitter, &receivers);
let fdoa = fdoa_predict(emitter, vel, &receivers, &recv_vel);
let x0 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let got =
solve_tdoa_fdoa(&receivers, &recv_vel, &tdoa, &fdoa, 1e-9, 1e-3, x0).expect("solves");
let perr = norm(sub([got[0], got[1], got[2]], emitter));
let verr = norm(sub([got[3], got[4], got[5]], vel));
assert!(
perr < 1e-4,
"position {:?} vs {emitter:?} (err {perr} m)",
&got[0..3]
);
assert!(
verr < 1e-4,
"velocity {:?} vs {vel:?} (err {verr} m/s)",
&got[3..6]
);
}
#[test]
fn too_few_receivers_is_rejected() {
let receivers = [[0.0, 0.0, 0.0], [10_000.0, 0.0, 0.0], [0.0, 10_000.0, 0.0]];
let tdoa = tdoa_predict([1.0, 2.0, 3.0], &receivers);
assert!(solve_tdoa(&receivers, &tdoa, 1e-9, [0.0, 0.0, 0.0]).is_none());
}
}