use crate::orbit::{enu_basis, invert4, los_unit};
pub fn geometry_from_los(user: [f64; 3], sats: &[[f64; 3]]) -> Option<Vec<[f64; 4]>> {
if sats.len() < 5 {
return None;
}
let mut g: Vec<[f64; 4]> = Vec::with_capacity(sats.len());
for &s in sats {
let e = los_unit(user, s)?;
g.push([-e[0], -e[1], -e[2], 1.0]);
}
Some(g)
}
#[derive(Clone, Debug)]
pub struct CommonModeSplit {
pub blind_dx: [f64; 4],
pub detectable_residual: Vec<f64>,
pub blind_norm: f64,
pub detectable_norm: f64,
pub blind_fraction: f64,
}
pub fn state_map(geometry: &[[f64; 4]]) -> Option<Vec<[f64; 4]>> {
let n = geometry.len();
if n < 5 {
return None;
}
let mut gtg = [[0.0_f64; 4]; 4];
for row in geometry {
for i in 0..4 {
for j in 0..4 {
gtg[i][j] += row[i] * row[j];
}
}
}
let a0 = invert4(gtg)?;
let s: Vec<[f64; 4]> = (0..n)
.map(|c| {
let mut col = [0.0_f64; 4];
for (i, ci) in col.iter_mut().enumerate() {
*ci = (0..4).map(|k| a0[i][k] * geometry[c][k]).sum();
}
col
})
.collect();
Some(s)
}
pub fn common_mode_split(geometry: &[[f64; 4]], delta_y: &[f64]) -> Option<CommonModeSplit> {
let n = geometry.len();
if n != delta_y.len() || n < 5 {
return None;
}
let dy_norm = norm(delta_y);
if dy_norm == 0.0 {
return None;
}
let s = state_map(geometry)?;
let mut blind_dx = [0.0_f64; 4];
for (c, &dy) in delta_y.iter().enumerate() {
for i in 0..4 {
blind_dx[i] += s[c][i] * dy;
}
}
let mut detectable_residual = Vec::with_capacity(n);
let mut blind_sq = 0.0_f64;
for (c, &dy) in delta_y.iter().enumerate() {
let pred: f64 = (0..4).map(|k| geometry[c][k] * blind_dx[k]).sum();
blind_sq += pred * pred;
detectable_residual.push(dy - pred);
}
let blind_norm = blind_sq.sqrt();
let detectable_norm = norm(&detectable_residual);
let blind_fraction = blind_norm / dy_norm;
Some(CommonModeSplit {
blind_dx,
detectable_residual,
blind_norm,
detectable_norm,
blind_fraction,
})
}
pub fn blind_position_covariance(
geometry: &[[f64; 4]],
cm_cov: &[Vec<f64>],
) -> Option<[[f64; 3]; 3]> {
let n = geometry.len();
if cm_cov.len() != n || cm_cov.iter().any(|row| row.len() != n) {
return None;
}
let s = state_map(geometry)?;
let mut m = vec![[0.0_f64; 3]; n];
for a in 0..n {
for i in 0..3 {
let mut acc = 0.0_f64;
for b in 0..n {
acc += cm_cov[a][b] * s[b][i];
}
m[a][i] = acc;
}
}
let mut c = [[0.0_f64; 3]; 3];
for i in 0..3 {
for j in 0..3 {
let mut acc = 0.0_f64;
for a in 0..n {
acc += s[a][i] * m[a][j];
}
c[i][j] = acc;
}
}
Some(c)
}
pub fn cmpl_horizontal(
geometry: &[[f64; 4]],
user: [f64; 3],
cm_cov: &[Vec<f64>],
k: f64,
) -> Option<f64> {
if !k.is_finite() {
return None;
}
let c = blind_position_covariance(geometry, cm_cov)?;
let (east, north, _up) = enu_basis(user)?;
let basis = [east, north];
let mut h = [[0.0_f64; 2]; 2];
for a in 0..2 {
for b in 0..2 {
let mut acc = 0.0_f64;
for i in 0..3 {
for j in 0..3 {
acc += basis[a][i] * c[i][j] * basis[b][j];
}
}
h[a][b] = acc;
}
}
let half_tr = 0.5 * (h[0][0] + h[1][1]);
let det = h[0][0] * h[1][1] - h[0][1] * h[1][0];
let disc = half_tr * half_tr - det;
if disc < 0.0 {
return None;
}
let lambda_max = half_tr + disc.sqrt();
if !lambda_max.is_finite() || lambda_max < 0.0 {
return None;
}
let cmpl = k * lambda_max.sqrt();
cmpl.is_finite().then_some(cmpl)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IntegrityEnvelope {
pub hpl_araim: f64,
pub cmpl: f64,
pub hpl_total: f64,
}
pub fn integrity_envelope(hpl_araim: f64, cmpl: f64) -> IntegrityEnvelope {
IntegrityEnvelope {
hpl_araim,
cmpl,
hpl_total: hpl_araim + cmpl,
}
}
fn norm(v: &[f64]) -> f64 {
v.iter().map(|x| x * x).sum::<f64>().sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
fn test_geometry() -> Vec<[f64; 4]> {
let user = [7.0e6, 0.0, 0.0];
let sats = [
[user[0] + 2.0e7, user[1], user[2]],
[user[0], user[1] + 2.0e7, user[2]],
[user[0], user[1], user[2] + 2.0e7],
[user[0] - 1.5e7, user[1] + 1.0e7, user[2] + 0.5e7],
[user[0] + 1.0e7, user[1] - 1.5e7, user[2] + 1.0e7],
[user[0] - 1.0e7, user[1] - 1.0e7, user[2] - 1.5e7],
];
geometry_from_los(user, &sats).expect("6 spread satellites give a valid geometry")
}
fn g_times(g: &[[f64; 4]], delta: [f64; 4]) -> Vec<f64> {
g.iter()
.map(|row| (0..4).map(|k| row[k] * delta[k]).sum())
.collect()
}
fn parity_vector(g: &[[f64; 4]], v: &[f64]) -> Vec<f64> {
common_mode_split(g, v)
.expect("split of a nonzero vector exists")
.detectable_residual
}
#[test]
fn pure_common_mode_error_is_perfectly_blind() {
let g = test_geometry();
let delta = [1.0, -2.0, 0.5, 3.0];
let delta_y = g_times(&g, delta);
let split = common_mode_split(&g, &delta_y).expect("valid split");
assert!(
(split.blind_fraction - 1.0).abs() < 1e-9,
"blind_fraction = {}",
split.blind_fraction
);
assert!(
split.detectable_norm < 1e-9,
"detectable_norm = {}",
split.detectable_norm
);
for (k, (&got, &want)) in split.blind_dx.iter().zip(&delta).enumerate() {
assert!(
(got - want).abs() < 1e-9,
"blind_dx[{k}] = {got} vs delta {want}"
);
}
}
#[test]
fn pure_parity_error_is_fully_detectable() {
let g = test_geometry();
let v = [0.3, 1.7, -0.4, 2.1, -1.1, 0.9];
let w = parity_vector(&g, &v);
let w_norm = norm(&w);
assert!(w_norm > 1e-6, "parity component must be non-trivial");
let split = common_mode_split(&g, &w).expect("valid split");
assert!(
split.blind_fraction.abs() < 1e-9,
"blind_fraction = {}",
split.blind_fraction
);
assert!(
(split.detectable_norm - w_norm).abs() < 1e-9,
"detectable_norm = {} vs ‖w‖ = {}",
split.detectable_norm,
w_norm
);
}
#[test]
fn split_is_additive() {
let g = test_geometry();
let delta = [1.0, -2.0, 0.5, 3.0];
let g_delta = g_times(&g, delta);
let v = [0.3, 1.7, -0.4, 2.1, -1.1, 0.9];
let w = parity_vector(&g, &v);
let w_norm = norm(&w);
let a = 2.5;
let b = -1.3;
let combined: Vec<f64> = g_delta
.iter()
.zip(&w)
.map(|(&gd, &wi)| a * gd + b * wi)
.collect();
let split = common_mode_split(&g, &combined).expect("valid split");
for (k, (&got, &d)) in split.blind_dx.iter().zip(&delta).enumerate() {
let want = a * d;
assert!(
(got - want).abs() < 1e-9,
"blind_dx[{k}] = {got} vs a·delta {want}"
);
}
assert!(
(split.detectable_norm - b.abs() * w_norm).abs() < 1e-9,
"detectable_norm = {} vs |b|·‖w‖ = {}",
split.detectable_norm,
b.abs() * w_norm
);
}
#[test]
fn too_few_or_singular_returns_none() {
let user = [7.0e6, 0.0, 0.0];
let few = [[8.0e6, 0.0, 0.0], [7.0e6, 1.0e6, 0.0]];
assert!(geometry_from_los(user, &few).is_none());
let g4 = vec![
[-1.0, 0.0, 0.0, 1.0],
[0.0, -1.0, 0.0, 1.0],
[0.0, 0.0, -1.0, 1.0],
[-0.577, -0.577, -0.577, 1.0],
];
assert!(common_mode_split(&g4, &[1.0, 1.0, 1.0, 1.0]).is_none());
let g = test_geometry();
let wrong_len = vec![0.0; g.len() - 1];
assert!(common_mode_split(&g, &wrong_len).is_none());
let zero = vec![0.0; g.len()];
assert!(common_mode_split(&g, &zero).is_none());
}
fn test_user() -> [f64; 3] {
[7.0e6, 0.0, 0.0]
}
fn rank1_cov(u: &[f64], sigma: f64) -> Vec<Vec<f64>> {
let s2 = sigma * sigma;
u.iter()
.map(|&ui| u.iter().map(|&uj| s2 * ui * uj).collect())
.collect()
}
#[test]
fn common_mode_covariance_gives_positive_cmpl() {
let g = test_geometry();
let user = test_user();
let d = [1.0, -2.0, 0.5, 3.0];
let u = g_times(&g, d);
let sigma = 5.0;
let cm_cov = rank1_cov(&u, sigma);
let cmpl = cmpl_horizontal(&g, user, &cm_cov, 5.33).expect("valid CMPL");
assert!(cmpl.is_finite() && cmpl > 0.0, "cmpl = {cmpl}");
let cpos = blind_position_covariance(&g, &cm_cov).expect("valid position covariance");
for (i, row) in cpos.iter().enumerate() {
assert!(row[i] > 0.0, "cpos[{i}][{i}] = {} not positive", row[i]);
}
}
#[test]
fn parity_only_covariance_gives_near_zero_cmpl() {
let g = test_geometry();
let user = test_user();
let v = [0.3, 1.7, -0.4, 2.1, -1.1, 0.9];
let w = parity_vector(&g, &v);
let sigma = 5.0;
let cm_cov = rank1_cov(&w, sigma);
let cmpl = cmpl_horizontal(&g, user, &cm_cov, 5.33).expect("valid CMPL");
assert!(
cmpl.abs() < 1e-6 * sigma,
"parity-space error must contribute ~no CMPL, got {cmpl}"
);
}
#[test]
fn envelope_sums_the_two_classes() {
let env = integrity_envelope(12.0, 5.0);
assert_eq!(env.hpl_araim, 12.0);
assert_eq!(env.cmpl, 5.0);
assert_eq!(env.hpl_total, 17.0);
}
#[test]
fn state_map_matches_split() {
let g = test_geometry();
let s = state_map(&g).expect("valid state map");
let delta_y = [0.3, 1.7, -0.4, 2.1, -1.1, 0.9];
let mut dx = [0.0_f64; 4];
for (c, &dy) in delta_y.iter().enumerate() {
for i in 0..4 {
dx[i] += s[c][i] * dy;
}
}
let split = common_mode_split(&g, &delta_y).expect("valid split");
for (k, (&got, &want)) in dx.iter().zip(&split.blind_dx).enumerate() {
assert!(
(got - want).abs() < 1e-12,
"state_map dx[{k}] = {got} vs split.blind_dx = {want}"
);
}
}
}