use crate::frame::Wgs84Geodetic;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LineOfSight {
pub e_x: f64,
pub e_y: f64,
pub e_z: f64,
}
impl LineOfSight {
pub const fn new(e_x: f64, e_y: f64, e_z: f64) -> Self {
Self { e_x, e_y, e_z }
}
fn design_row(self) -> [f64; 4] {
[-self.e_x, -self.e_y, -self.e_z, 1.0]
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Dop {
pub gdop: f64,
pub pdop: f64,
pub hdop: f64,
pub vdop: f64,
pub tdop: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DopError {
TooFewSatellites,
Singular,
}
impl core::fmt::Display for DopError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DopError::TooFewSatellites => {
write!(
f,
"fewer satellites than parameters: geometry is rank-deficient"
)
}
DopError::Singular => {
write!(f, "singular or ill-conditioned geometry: no finite DOP")
}
}
}
}
impl std::error::Error for DopError {}
#[allow(clippy::needless_range_loop)] fn normal_matrix(rows: &[[f64; 4]], weights: &[f64]) -> [[f64; 4]; 4] {
let mut a = [[0.0_f64; 4]; 4];
for i in 0..4 {
for j in 0..4 {
let mut s = 0.0_f64;
for k in 0..rows.len() {
s += rows[k][i] * weights[k] * rows[k][j];
}
a[i][j] = s;
}
}
a
}
fn det4(a: &[[f64; 4]; 4]) -> f64 {
let m01 = a[2][0] * a[3][1] - a[2][1] * a[3][0];
let m02 = a[2][0] * a[3][2] - a[2][2] * a[3][0];
let m03 = a[2][0] * a[3][3] - a[2][3] * a[3][0];
let m12 = a[2][1] * a[3][2] - a[2][2] * a[3][1];
let m13 = a[2][1] * a[3][3] - a[2][3] * a[3][1];
let m23 = a[2][2] * a[3][3] - a[2][3] * a[3][2];
let c0 = a[1][1] * m23 - a[1][2] * m13 + a[1][3] * m12;
let c1 = a[1][0] * m23 - a[1][2] * m03 + a[1][3] * m02;
let c2 = a[1][0] * m13 - a[1][1] * m03 + a[1][3] * m01;
let c3 = a[1][0] * m12 - a[1][1] * m02 + a[1][2] * m01;
a[0][0] * c0 - a[0][1] * c1 + a[0][2] * c2 - a[0][3] * c3
}
fn minor3(a: &[[f64; 4]; 4], skip_r: usize, skip_c: usize) -> f64 {
let mut rows = [0usize; 3];
let mut ri = 0;
for r in 0..4 {
if r != skip_r {
rows[ri] = r;
ri += 1;
}
}
let mut cols = [0usize; 3];
let mut ci = 0;
for c in 0..4 {
if c != skip_c {
cols[ci] = c;
ci += 1;
}
}
let b = [
[
a[rows[0]][cols[0]],
a[rows[0]][cols[1]],
a[rows[0]][cols[2]],
],
[
a[rows[1]][cols[0]],
a[rows[1]][cols[1]],
a[rows[1]][cols[2]],
],
[
a[rows[2]][cols[0]],
a[rows[2]][cols[1]],
a[rows[2]][cols[2]],
],
];
b[0][0] * (b[1][1] * b[2][2] - b[1][2] * b[2][1])
- b[0][1] * (b[1][0] * b[2][2] - b[1][2] * b[2][0])
+ b[0][2] * (b[1][0] * b[2][1] - b[1][1] * b[2][0])
}
#[allow(clippy::needless_range_loop)] fn inv4(a: &[[f64; 4]; 4]) -> Option<[[f64; 4]; 4]> {
let det = det4(a);
if det == 0.0 {
return None;
}
let mut inv = [[0.0_f64; 4]; 4];
for i in 0..4 {
for j in 0..4 {
let minor = minor3(a, i, j);
let sign = if (i + j) % 2 == 0 { 1.0 } else { -1.0 };
let cof = sign * minor;
inv[j][i] = cof / det;
}
}
Some(inv)
}
fn ecef_to_enu_rotation(lat_rad: f64, lon_rad: f64) -> [[f64; 3]; 3] {
let sphi = lat_rad.sin();
let cphi = lat_rad.cos();
let slam = lon_rad.sin();
let clam = lon_rad.cos();
[
[-slam, clam, 0.0],
[-sphi * clam, -sphi * slam, cphi],
[cphi * clam, cphi * slam, sphi],
]
}
#[allow(clippy::needless_range_loop)] fn rotate_pos_block(q: &[[f64; 4]; 4], r: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut rq = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = 0.0_f64;
for k in 0..3 {
s += r[i][k] * q[k][j];
}
rq[i][j] = s;
}
}
let mut enu = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = 0.0_f64;
for k in 0..3 {
s += rq[i][k] * r[j][k];
}
enu[i][j] = s;
}
}
enu
}
pub fn dop(los: &[LineOfSight], weights: &[f64], receiver: Wgs84Geodetic) -> Result<Dop, DopError> {
assert_eq!(
los.len(),
weights.len(),
"line-of-sight and weight counts differ"
);
if los.len() < 4 {
return Err(DopError::TooFewSatellites);
}
let rows: Vec<[f64; 4]> = los.iter().map(|l| l.design_row()).collect();
let a = normal_matrix(&rows, weights);
let q = inv4(&a).ok_or(DopError::Singular)?;
let r = ecef_to_enu_rotation(receiver.lat_rad, receiver.lon_rad);
let enu = rotate_pos_block(&q, &r);
let qe = enu[0][0];
let qn = enu[1][1];
let qu = enu[2][2];
let qt = q[3][3];
let gdop_arg = q[0][0] + q[1][1] + q[2][2] + q[3][3];
let pdop_arg = qe + qn + qu;
let hdop_arg = qe + qn;
let vdop_arg = qu;
let tdop_arg = qt;
for arg in [gdop_arg, pdop_arg, hdop_arg, vdop_arg, tdop_arg] {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
let nonpositive_or_nan = !(arg >= 0.0);
if nonpositive_or_nan || !arg.is_finite() {
return Err(DopError::Singular);
}
}
Ok(Dop {
gdop: gdop_arg.sqrt(),
pdop: pdop_arg.sqrt(),
hdop: hdop_arg.sqrt(),
vdop: vdop_arg.sqrt(),
tdop: tdop_arg.sqrt(),
})
}
#[allow(clippy::needless_range_loop)]
fn rotate3(q: &[[f64; 3]; 3], r: &[[f64; 3]; 3]) -> [[f64; 3]; 3] {
let mut rq = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = 0.0_f64;
for k in 0..3 {
s += r[i][k] * q[k][j];
}
rq[i][j] = s;
}
}
let mut enu = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut s = 0.0_f64;
for k in 0..3 {
s += rq[i][k] * r[j][k];
}
enu[i][j] = s;
}
}
enu
}
#[allow(clippy::needless_range_loop)]
fn invert_symmetric_pd(n: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
let p = n.len();
let mut l = vec![vec![0.0_f64; p]; p];
for i in 0..p {
for j in 0..=i {
let mut s = n[i][j];
for k in 0..j {
s -= l[i][k] * l[j][k];
}
if i == j {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
let nonpositive_or_nan = !(s > 0.0);
if nonpositive_or_nan || !s.is_finite() {
return None;
}
l[i][j] = s.sqrt();
} else {
l[i][j] = s / l[j][j];
}
}
}
let mut li = vec![vec![0.0_f64; p]; p];
for i in 0..p {
li[i][i] = 1.0 / l[i][i];
for j in 0..i {
let mut s = 0.0_f64;
for k in j..i {
s -= l[i][k] * li[k][j];
}
li[i][j] = s / l[i][i];
}
}
let mut inv = vec![vec![0.0_f64; p]; p];
for i in 0..p {
for j in 0..p {
let mut s = 0.0_f64;
for k in 0..p {
s += li[k][i] * li[k][j];
}
inv[i][j] = s;
}
}
Some(inv)
}
pub(crate) fn dop_multi(
los: &[LineOfSight],
clock_index: &[usize],
n_clocks: usize,
weights: &[f64],
receiver: Wgs84Geodetic,
) -> Result<Dop, DopError> {
assert_eq!(
los.len(),
weights.len(),
"line-of-sight and weight counts differ"
);
assert_eq!(
los.len(),
clock_index.len(),
"line-of-sight and clock-index counts differ"
);
assert!(n_clocks >= 1, "at least one clock");
debug_assert!(
clock_index.iter().all(|&c| c < n_clocks),
"clock index out of range 0..n_clocks"
);
let p = 3 + n_clocks;
if los.len() < p {
return Err(DopError::TooFewSatellites);
}
let mut a = vec![vec![0.0_f64; p]; p];
for k in 0..los.len() {
let mut row = vec![0.0_f64; p];
row[0] = -los[k].e_x;
row[1] = -los[k].e_y;
row[2] = -los[k].e_z;
row[3 + clock_index[k]] = 1.0;
let w = weights[k];
#[allow(clippy::needless_range_loop)]
for i in 0..p {
for j in 0..p {
a[i][j] += row[i] * w * row[j];
}
}
}
let q = invert_symmetric_pd(&a).ok_or(DopError::Singular)?;
let r = ecef_to_enu_rotation(receiver.lat_rad, receiver.lon_rad);
let qpos = [
[q[0][0], q[0][1], q[0][2]],
[q[1][0], q[1][1], q[1][2]],
[q[2][0], q[2][1], q[2][2]],
];
let enu = rotate3(&qpos, &r);
let qe = enu[0][0];
let qn = enu[1][1];
let qu = enu[2][2];
let qt = q[3][3];
let trace: f64 = (0..p).map(|i| q[i][i]).sum();
let gdop_arg = trace;
let pdop_arg = qe + qn + qu;
let hdop_arg = qe + qn;
let vdop_arg = qu;
let tdop_arg = qt;
for arg in [gdop_arg, pdop_arg, hdop_arg, vdop_arg, tdop_arg] {
#[allow(clippy::neg_cmp_op_on_partial_ord)]
let nonpositive_or_nan = !(arg >= 0.0);
if nonpositive_or_nan || !arg.is_finite() {
return Err(DopError::Singular);
}
}
Ok(Dop {
gdop: gdop_arg.sqrt(),
pdop: pdop_arg.sqrt(),
hdop: hdop_arg.sqrt(),
vdop: vdop_arg.sqrt(),
tdop: tdop_arg.sqrt(),
})
}
#[cfg(test)]
pub(crate) mod test_support {
use super::*;
pub(crate) fn normal_matrix_for(los: &[LineOfSight], weights: &[f64]) -> [[f64; 4]; 4] {
let rows: Vec<[f64; 4]> = los.iter().map(|l| l.design_row()).collect();
normal_matrix(&rows, weights)
}
pub(crate) fn det4_for(a: &[[f64; 4]; 4]) -> f64 {
det4(a)
}
pub(crate) fn inv4_for(a: &[[f64; 4]; 4]) -> Option<[[f64; 4]; 4]> {
inv4(a)
}
pub(crate) fn enu_block_for(q: &[[f64; 4]; 4], lat_rad: f64, lon_rad: f64) -> [[f64; 3]; 3] {
let r = ecef_to_enu_rotation(lat_rad, lon_rad);
rotate_pos_block(q, &r)
}
}
#[cfg(test)]
mod tests;