use crate::ellipsoid::Ellipsoid;
use crate::frame::LocalFrame;
use crate::local::{Aer, Enu};
#[cfg(all(feature = "libm", not(feature = "std"), not(test)))]
use crate::math::Float as _;
use crate::position::Geodetic;
const DEG: f64 = core::f64::consts::PI / 180.0;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Cov3(pub [[f64; 3]; 3]);
impl Cov3 {
pub const fn from_std_devs(s: [f64; 3]) -> Self {
Cov3([
[s[0] * s[0], 0.0, 0.0],
[0.0, s[1] * s[1], 0.0],
[0.0, 0.0, s[2] * s[2]],
])
}
pub fn std_devs(self) -> [f64; 3] {
[self.0[0][0].sqrt(), self.0[1][1].sqrt(), self.0[2][2].sqrt()]
}
pub fn enu_to_ned(self) -> Cov3 {
self.transformed([[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0]])
}
pub fn ned_to_enu(self) -> Cov3 {
self.enu_to_ned() }
fn transformed(self, j: [[f64; 3]; 3]) -> Cov3 {
let s = self.0;
let mut js = [[0.0; 3]; 3];
for (row, jr) in js.iter_mut().zip(&j) {
for (c, out) in row.iter_mut().enumerate() {
*out = jr[0] * s[0][c] + jr[1] * s[1][c] + jr[2] * s[2][c];
}
}
let mut out = [[0.0; 3]; 3];
for r in 0..3 {
for c in r..3 {
out[r][c] = js[r][0] * j[c][0] + js[r][1] * j[c][1] + js[r][2] * j[c][2];
out[c][r] = out[r][c];
}
}
Cov3(out)
}
}
impl LocalFrame {
pub fn enu_cov_to_ecef(&self, cov: Cov3) -> Cov3 {
let (e, n, u) = self.axes();
cov.transformed([[e.x, n.x, u.x], [e.y, n.y, u.y], [e.z, n.z, u.z]])
}
pub fn ecef_cov_to_enu(&self, cov: Cov3) -> Cov3 {
let (e, n, u) = self.axes();
cov.transformed([[e.x, e.y, e.z], [n.x, n.y, n.z], [u.x, u.y, u.z]])
}
}
impl Aer {
pub fn cov_to_enu(self, cov: Cov3) -> Cov3 {
let (saz, caz) = self.azimuth_deg.to_radians().sin_cos();
let (sel, cel) = self.elevation_deg.to_radians().sin_cos();
let r = self.range_m;
cov.transformed([
[r * cel * caz * DEG, -r * sel * saz * DEG, cel * saz],
[-r * cel * saz * DEG, -r * sel * caz * DEG, cel * caz],
[0.0, r * cel * DEG, sel],
])
}
}
impl Enu {
pub fn cov_to_aer(self, cov: Cov3) -> Cov3 {
let (e, n, u) = (self.east, self.north, self.up);
let h2 = e * e + n * n;
let (h, r2) = (h2.sqrt(), h2 + u * u);
let r = r2.sqrt();
cov.transformed([
[n / h2 / DEG, -e / h2 / DEG, 0.0],
[-e * u / (r2 * h) / DEG, -n * u / (r2 * h) / DEG, h / r2 / DEG],
[e / r, n / r, u / r],
])
}
}
impl Ellipsoid {
pub fn geodetic_cov_to_ecef(self, at: Geodetic, cov: Cov3) -> Cov3 {
let (e, n, u) = LocalFrame::on(self, at).axes();
let (dlat, dlon) = self.metres_per_degree(at);
cov.transformed([
[n.x * dlat, e.x * dlon, u.x],
[n.y * dlat, e.y * dlon, u.y],
[n.z * dlat, e.z * dlon, u.z],
])
}
pub fn ecef_cov_to_geodetic(self, at: Geodetic, cov: Cov3) -> Cov3 {
let (e, n, u) = LocalFrame::on(self, at).axes();
let (dlat, dlon) = self.metres_per_degree(at);
cov.transformed([
[n.x / dlat, n.y / dlat, n.z / dlat],
[e.x / dlon, e.y / dlon, e.z / dlon],
[u.x, u.y, u.z],
])
}
fn metres_per_degree(self, at: Geodetic) -> (f64, f64) {
let lat = at.lat_rad();
let h = at.height_m;
(
(self.meridional_radius(lat) + h) * DEG,
(self.prime_vertical_radius(lat) + h) * lat.cos() * DEG,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Ecef, WGS84};
fn outer(v: [f64; 3]) -> Cov3 {
Cov3(core::array::from_fn(|i| core::array::from_fn(|j| v[i] * v[j])))
}
fn check_jacobian(
propagate: impl Fn(Cov3) -> Cov3,
f: impl Fn([f64; 3]) -> [f64; 3],
x: [f64; 3],
steps: [f64; 3],
) {
for k in 0..3 {
let mut v = [0.0; 3];
v[k] = 1.0;
let (mut lo, mut hi) = (x, x);
lo[k] -= steps[k];
hi[k] += steps[k];
let (a, b) = (f(lo), f(hi));
let jv: [f64; 3] = core::array::from_fn(|i| (b[i] - a[i]) / (2.0 * steps[k]));
let got = propagate(outer(v)).0;
let scale = jv.iter().fold(0.0f64, |m, x| m.max(x.abs()));
for i in 0..3 {
for j in 0..3 {
let want = jv[i] * jv[j];
assert!(
(got[i][j] - want).abs() <= 1e-6 * scale * scale,
"at {x:?}, column {k}: [{i}][{j}] = {} vs finite difference {want}",
got[i][j]
);
}
}
}
}
#[test]
fn jacobians_match_finite_differences() {
for &(az, el, r) in &[(40.0, 10.0, 50_000.0), (200.0, -30.0, 1_000.0), (100.0, 80.0, 3.0e7)] {
let aer = Aer::new(az, el, r);
check_jacobian(
|c| aer.cov_to_enu(c),
|p| {
let v = Aer::new(p[0], p[1], p[2]).to_enu();
[v.east, v.north, v.up]
},
[az, el, r],
[1e-6, 1e-6, r * 1e-7],
);
let enu = aer.to_enu();
check_jacobian(
|c| enu.cov_to_aer(c),
|p| {
let a = Enu::new(p[0], p[1], p[2]).to_aer();
[a.azimuth_deg, a.elevation_deg, a.range_m]
},
[enu.east, enu.north, enu.up],
[r * 1e-7; 3],
);
}
for &(lat, lon, h) in &[(-33.87, 151.21, 20.0), (0.0, 10.0, -5_000.0), (89.0, -120.0, 4.0e5)] {
let at = Geodetic::new(lat, lon, h);
check_jacobian(
|c| WGS84.geodetic_cov_to_ecef(at, c),
|p| WGS84.to_ecef(Geodetic::new(p[0], p[1], p[2])).to_array(),
[lat, lon, h],
[1e-6, 1e-6, 1.0],
);
let x = WGS84.to_ecef(at);
check_jacobian(
|c| WGS84.ecef_cov_to_geodetic(at, c),
|p| {
let g = WGS84.to_geodetic(Ecef::new(p[0], p[1], p[2]));
[g.lat_deg, g.lon_deg, g.height_m]
},
x.to_array(),
[x.to_array().iter().map(|c| c * c).sum::<f64>().sqrt() * 1e-7; 3],
);
}
}
struct Normal(u64);
impl Normal {
fn unit(&mut self) -> f64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
((self.0.wrapping_mul(0x2545_f491_4f6c_dd1d) >> 11) as f64 + 0.5) / (1u64 << 53) as f64
}
fn next(&mut self) -> f64 {
(-2.0 * self.unit().ln()).sqrt() * (2.0 * core::f64::consts::PI * self.unit()).cos()
}
}
fn monte_carlo(x: [f64; 3], sigma: [f64; 3], f: impl Fn([f64; 3]) -> [f64; 3], propagated: Cov3) {
let (mut rng, n) = (Normal(0x9e37_79b9_7f4a_7c15), 200_000);
let samples: Vec<[f64; 3]> = (0..n)
.map(|_| f(core::array::from_fn(|i| x[i] + sigma[i] * rng.next())))
.collect();
let mean: [f64; 3] = core::array::from_fn(|i| samples.iter().map(|s| s[i]).sum::<f64>() / n as f64);
let p = propagated.0;
for i in 0..3 {
for j in 0..3 {
let s = samples.iter().map(|v| (v[i] - mean[i]) * (v[j] - mean[j])).sum::<f64>() / (n - 1) as f64;
let d = (s - p[i][j]) / (p[i][i] * p[j][j]).sqrt();
assert!(d.abs() < 0.01, "[{i}][{j}]: sample {s} vs propagated {}", p[i][j]);
}
}
}
#[test]
fn propagated_covariances_match_monte_carlo() {
let (aer, sigma) = (Aer::new(40.0, 10.0, 50_000.0), [0.2, 0.2, 20.0]);
monte_carlo(
[40.0, 10.0, 50_000.0],
sigma,
|p| {
let v = Aer::new(p[0], p[1], p[2]).to_enu();
[v.east, v.north, v.up]
},
aer.cov_to_enu(Cov3::from_std_devs(sigma)),
);
let (at, sigma) = (Geodetic::new(-33.87, 151.21, 20.0), [3e-5, 3e-5, 5.0]);
monte_carlo(
[-33.87, 151.21, 20.0],
sigma,
|p| WGS84.to_ecef(Geodetic::new(p[0], p[1], p[2])).to_array(),
WGS84.geodetic_cov_to_ecef(at, Cov3::from_std_devs(sigma)),
);
}
fn assert_same(b: Cov3, c: Cov3) {
for i in 0..3 {
for j in 0..3 {
let scale = (c.0[i][i] * c.0[j][j]).sqrt();
assert!((b.0[i][j] - c.0[i][j]).abs() < 1e-11 * scale, "{b:?} vs {c:?}");
assert_eq!(b.0[i][j], b.0[j][i]);
}
}
}
#[test]
fn round_trips_and_exact_cases() {
let cov = Cov3([[4.0, 1.0, -0.5], [1.0, 9.0, 2.0], [-0.5, 2.0, 25.0]]);
let frame = LocalFrame::new(Geodetic::new(51.5, -0.12, 30.0));
assert_same(frame.ecef_cov_to_enu(frame.enu_cov_to_ecef(cov)), cov);
let aer = Aer::new(123.0, 45.0, 10_000.0);
assert_same(aer.to_enu().cov_to_aer(aer.cov_to_enu(cov)), cov);
assert_same(cov.enu_to_ned().ned_to_enu(), cov);
let geo = Cov3([[9e-10, 1e-10, 2e-5], [1e-10, 9e-10, -1e-5], [2e-5, -1e-5, 25.0]]);
let at = Geodetic::new(-33.87, 151.21, 20.0);
assert_same(WGS84.ecef_cov_to_geodetic(at, WGS84.geodetic_cov_to_ecef(at, geo)), geo);
let ned = cov.enu_to_ned().0;
assert_eq!((ned[0][0], ned[1][1], ned[2][2]), (9.0, 4.0, 25.0));
assert_eq!((ned[0][1], ned[0][2], ned[1][2]), (1.0, -2.0, 0.5));
let ecef = frame.enu_cov_to_ecef(cov).0;
assert!((ecef[0][0] + ecef[1][1] + ecef[2][2] - 38.0).abs() < 1e-12);
assert_eq!(Cov3::from_std_devs([3.0, 4.0, 5.0]).std_devs(), [3.0, 4.0, 5.0]);
let up = Enu::new(0.0, 0.0, 1_000.0).cov_to_aer(Cov3::from_std_devs([1.0; 3]));
assert!(!up.0[0][0].is_finite());
assert!((up.std_devs()[2] - 1.0).abs() < 1e-12);
}
}