use crate::araim::ism::Ism;
use crate::araim::protection::{design_row, gain_matrix_enu, ProtectionModel};
use crate::astro::math::linear::{invert_symmetric_pd, normal_equations_weighted};
use crate::astro::math::special::normal_q_inv;
use crate::quality::{QualityError, DEFAULT_P_FA};
use crate::validate;
use super::{AraimError, AraimGeometry};
const DEFAULT_BETA: f64 = 0.20;
const DEFAULT_MIN_REDUNDANCY: f64 = 1.0e-9;
const REDUNDANCY_TOL: f64 = 1.0e-10;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RangeReliabilityRow {
pub id: String,
pub design_row: Vec<f64>,
pub sigma_m: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ReliabilityOptions {
pub alpha: f64,
pub beta: f64,
pub lambda0_override: Option<f64>,
pub min_redundancy: f64,
}
impl Default for ReliabilityOptions {
fn default() -> Self {
Self {
alpha: DEFAULT_P_FA,
beta: DEFAULT_BETA,
lambda0_override: None,
min_redundancy: DEFAULT_MIN_REDUNDANCY,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ObservationReliability {
pub id: String,
pub redundancy: f64,
pub mdb_m: Option<f64>,
pub external_enu_m: Option<[f64; 3]>,
pub bias_to_noise: Option<f64>,
pub uncheckable: bool,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ReliabilitySummary {
pub n_obs: usize,
pub n_params: usize,
pub dof: usize,
pub sum_redundancy: f64,
pub lambda0: f64,
pub max_mdb_m: Option<(String, f64)>,
pub min_redundancy: (String, f64),
pub n_uncheckable: usize,
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct ReliabilityReport {
pub per_observation: Vec<ObservationReliability>,
pub summary: ReliabilitySummary,
}
pub fn wtest_noncentrality(alpha: f64, beta: f64) -> Result<f64, QualityError> {
Ok(wtest_noncentrality_components(alpha, beta)?.lambda0)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct WtestNoncentralityComponents {
pub delta0: f64,
pub lambda0: f64,
}
pub fn wtest_noncentrality_components(
alpha: f64,
beta: f64,
) -> Result<WtestNoncentralityComponents, QualityError> {
validate_probability(alpha)?;
if !(0.0..1.0).contains(&beta) || !beta.is_finite() {
return Err(QualityError::InvalidReliabilityParameter);
}
let k_alpha = normal_q_inv(0.5 * alpha).ok_or(QualityError::InvalidProbability)?;
let k_beta = normal_q_inv(beta).ok_or(QualityError::InvalidReliabilityParameter)?;
let delta0 = k_alpha + k_beta;
let lambda0 = delta0 * delta0;
if lambda0.is_finite() && lambda0 > 0.0 {
Ok(WtestNoncentralityComponents { delta0, lambda0 })
} else {
Err(QualityError::InvalidReliabilityParameter)
}
}
pub fn reliability_design(
rows: &[RangeReliabilityRow],
options: &ReliabilityOptions,
) -> Result<ReliabilityReport, QualityError> {
let n_params = validate_reliability_rows(rows)?;
let lambda0 = reliability_lambda0(options)?;
let q_xx = design_covariance(rows, n_params)?;
finish_reliability_report(
rows,
n_params,
lambda0,
options.min_redundancy,
|idx, mdb_m| state_external(rows, &q_xx, idx, mdb_m),
)
}
pub fn reliability_araim(
geometry: &AraimGeometry,
model: &dyn ProtectionModel,
options: &ReliabilityOptions,
) -> Result<ReliabilityReport, AraimError> {
let rows = araim_rows(geometry, model)?;
let lambda0 = reliability_lambda0(options).map_err(map_quality_error)?;
let weights = rows
.iter()
.map(|row| 1.0 / (row.sigma_m * row.sigma_m))
.collect::<Vec<_>>();
let gain = gain_matrix_enu(geometry, &weights)?;
let n_params = geometry_state_len(geometry)?;
finish_reliability_report(
&rows,
n_params,
lambda0,
options.min_redundancy,
|idx, mdb_m| {
Some([
gain.enu_rows[0][idx] * mdb_m,
gain.enu_rows[1][idx] * mdb_m,
gain.enu_rows[2][idx] * mdb_m,
])
},
)
.map_err(map_quality_error)
}
impl ProtectionModel for Ism {
fn sigma_int_m(&self, row: &super::AraimRow) -> Result<f64, AraimError> {
Ok(self.effective_for(row)?.sigma_int_m)
}
fn sigma_acc_m(&self, row: &super::AraimRow) -> Result<f64, AraimError> {
Ok(self.effective_for(row)?.sigma_acc_m)
}
}
fn araim_rows(
geometry: &AraimGeometry,
model: &dyn ProtectionModel,
) -> Result<Vec<RangeReliabilityRow>, AraimError> {
let n_params = geometry_state_len(geometry)?;
if geometry.rows.len() < n_params {
return Err(AraimError::InsufficientGeometry);
}
let mut rows = Vec::with_capacity(geometry.rows.len());
for row in &geometry.rows {
let sigma_int_m = model.sigma_int_m(row)?;
let sigma_acc_m = model.sigma_acc_m(row)?;
if !valid_positive_finite(sigma_int_m) || !valid_positive_finite(sigma_acc_m) {
return Err(AraimError::InvalidIsm);
}
rows.push(RangeReliabilityRow {
id: row.id.to_string(),
design_row: design_row(&geometry.clock_systems, row)?,
sigma_m: sigma_int_m,
});
}
Ok(rows)
}
fn reliability_lambda0(options: &ReliabilityOptions) -> Result<f64, QualityError> {
validate_reliability_options(options)?;
match options.lambda0_override {
Some(lambda0) => Ok(lambda0),
None => wtest_noncentrality(options.alpha, options.beta),
}
}
fn validate_reliability_options(options: &ReliabilityOptions) -> Result<(), QualityError> {
validate_probability(options.alpha)?;
if !(0.0..1.0).contains(&options.beta) || !options.beta.is_finite() {
return Err(QualityError::InvalidReliabilityParameter);
}
if let Some(lambda0) = options.lambda0_override {
if !lambda0.is_finite() || lambda0 <= 0.0 {
return Err(QualityError::InvalidReliabilityParameter);
}
}
if !options.min_redundancy.is_finite() || options.min_redundancy <= 0.0 {
return Err(QualityError::InvalidReliabilityParameter);
}
Ok(())
}
fn validate_reliability_rows(rows: &[RangeReliabilityRow]) -> Result<usize, QualityError> {
let first = rows.first().ok_or(QualityError::InvalidDesign)?;
let n_params = first.design_row.len();
if n_params == 0 || rows.len() < n_params {
return Err(QualityError::InvalidDesign);
}
for row in rows {
if row.design_row.len() != n_params {
return Err(QualityError::InvalidDesign);
}
validate::finite_slice(&row.design_row, "reliability design row")
.map_err(|_| QualityError::InvalidDesign)?;
validate::finite_positive(row.sigma_m, "reliability sigma")
.map_err(|_| QualityError::InvalidWeight)?;
let weight = 1.0 / (row.sigma_m * row.sigma_m);
if !weight.is_finite() || weight <= 0.0 {
return Err(QualityError::InvalidWeight);
}
}
Ok(n_params)
}
fn design_covariance(
rows: &[RangeReliabilityRow],
n_params: usize,
) -> Result<Vec<Vec<f64>>, QualityError> {
let (normal, _) = normal_equations_weighted(
rows.iter()
.map(|row| (row.design_row.as_slice(), 0.0, 1.0 / row.sigma_m)),
n_params,
)
.ok_or(QualityError::InvalidDesign)?;
invert_symmetric_pd(&normal).ok_or(QualityError::SingularGeometry)
}
fn finish_reliability_report<F>(
rows: &[RangeReliabilityRow],
n_params: usize,
lambda0: f64,
min_redundancy: f64,
mut external: F,
) -> Result<ReliabilityReport, QualityError>
where
F: FnMut(usize, f64) -> Option<[f64; 3]>,
{
let q_xx = design_covariance(rows, n_params)?;
let mut per_observation = Vec::with_capacity(rows.len());
let mut sum_redundancy = 0.0;
let mut max_mdb_m: Option<(String, f64)> = None;
let mut min_pair: Option<(String, f64)> = None;
let mut n_uncheckable = 0usize;
for (idx, row) in rows.iter().enumerate() {
let redundancy = redundancy_number(row, &q_xx)?;
sum_redundancy += redundancy;
update_min_redundancy(&mut min_pair, &row.id, redundancy);
let uncheckable = redundancy < min_redundancy;
let (mdb_m, external_enu_m, bias_to_noise) = if uncheckable {
n_uncheckable += 1;
(None, None, None)
} else {
let mdb = row.sigma_m * (lambda0 / redundancy).sqrt();
let bnr = (lambda0 * (1.0 - redundancy) / redundancy).sqrt();
update_max_mdb(&mut max_mdb_m, &row.id, mdb);
(Some(mdb), external(idx, mdb), Some(bnr))
};
per_observation.push(ObservationReliability {
id: row.id.clone(),
redundancy,
mdb_m,
external_enu_m,
bias_to_noise,
uncheckable,
});
}
Ok(ReliabilityReport {
per_observation,
summary: ReliabilitySummary {
n_obs: rows.len(),
n_params,
dof: rows.len().saturating_sub(n_params),
sum_redundancy,
lambda0,
max_mdb_m,
min_redundancy: min_pair.unwrap_or_else(|| (String::new(), 0.0)),
n_uncheckable,
},
})
}
fn redundancy_number(row: &RangeReliabilityRow, q_xx: &[Vec<f64>]) -> Result<f64, QualityError> {
let qh = mat_vec(q_xx, &row.design_row);
let leverage = dot(&row.design_row, &qh) / (row.sigma_m * row.sigma_m);
if !leverage.is_finite() {
return Err(QualityError::SingularGeometry);
}
let raw = 1.0 - leverage;
if raw >= 0.0 {
Ok(raw.min(1.0))
} else if raw.abs() <= REDUNDANCY_TOL {
Ok(0.0)
} else {
Err(QualityError::SingularGeometry)
}
}
fn state_external(
rows: &[RangeReliabilityRow],
q_xx: &[Vec<f64>],
idx: usize,
mdb_m: f64,
) -> Option<[f64; 3]> {
if q_xx.len() < 3 {
return None;
}
let row = &rows[idx];
let scale = mdb_m / (row.sigma_m * row.sigma_m);
let qh = mat_vec(q_xx, &row.design_row);
Some([qh[0] * scale, qh[1] * scale, qh[2] * scale])
}
fn update_min_redundancy(pair: &mut Option<(String, f64)>, id: &str, value: f64) {
if match pair.as_ref() {
Some((_, current)) => value < *current,
None => true,
} {
*pair = Some((id.to_owned(), value));
}
}
fn update_max_mdb(pair: &mut Option<(String, f64)>, id: &str, value: f64) {
if match pair.as_ref() {
Some((_, current)) => value > *current,
None => true,
} {
*pair = Some((id.to_owned(), value));
}
}
fn geometry_state_len(geometry: &AraimGeometry) -> Result<usize, AraimError> {
if geometry.clock_systems.is_empty() {
return Err(AraimError::InsufficientGeometry);
}
Ok(3 + geometry.clock_systems.len())
}
fn map_quality_error(err: QualityError) -> AraimError {
match err {
QualityError::InvalidProbability | QualityError::InvalidReliabilityParameter => {
AraimError::InvalidAllocation
}
QualityError::InvalidWeight => AraimError::InvalidIsm,
QualityError::InvalidDesign | QualityError::SingularGeometry => {
AraimError::InsufficientGeometry
}
_ => AraimError::NumericalFailure,
}
}
fn validate_probability(value: f64) -> Result<(), QualityError> {
if (0.0..1.0).contains(&value) && value.is_finite() {
Ok(())
} else {
Err(QualityError::InvalidProbability)
}
}
fn valid_positive_finite(value: f64) -> bool {
value.is_finite() && value > 0.0
}
fn mat_vec(a: &[Vec<f64>], x: &[f64]) -> Vec<f64> {
a.iter().map(|row| dot(row, x)).collect()
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::araim::protection::gain_matrix_enu;
use crate::araim::{AraimGeometry, AraimRow};
use crate::dop::{ecef_to_enu_rotation, LineOfSight};
use crate::frame::Wgs84Geodetic;
use crate::id::{GnssSatelliteId, GnssSystem};
const LAMBDA_BAARDA_4DP: f64 = 17.075;
#[test]
fn algebraic_identities_hold_for_weighted_design() {
let rows = algebraic_rows();
let options = options_with_lambda(LAMBDA_BAARDA_4DP);
let report = reliability_design(&rows, &options).expect("reliability report");
let n = rows.len();
let u = rows[0].design_row.len();
let q_xx = design_covariance(&rows, u).expect("covariance");
let r = residual_projector(&rows, &q_xx);
let h = design_matrix(&rows);
let w = weight_matrix(&rows);
assert_close(report.summary.sum_redundancy, (n - u) as f64, 2.0e-14);
assert_close(trace(&r), (n - u) as f64, 2.0e-14);
assert!(inf_norm(&mat_sub(&mat_mul(&r, &r), &r)) < 1.0e-9);
assert!(inf_norm(&mat_mul(&r, &h)) < 1.0e-9);
assert!(inf_norm(&mat_mul(&mat_mul(&transpose(&r), &w), &h)) < 1.0e-9);
let q_v = q_v_matrix(&rows, &q_xx);
let q_v_w = mat_mul(&q_v, &w);
let normal = normal_matrix(&rows);
for (idx, obs) in report.per_observation.iter().enumerate() {
assert_close(obs.redundancy, q_v_w[idx][idx], 2.0e-14);
let mdb = obs.mdb_m.expect("checkable MDB");
let dx = full_state_external(&rows[idx], &q_xx, mdb);
let bnr_full = quadratic_form(&normal, &dx).sqrt();
assert_close(bnr_full, obs.bias_to_noise.expect("checkable BNR"), 2.0e-12);
}
}
#[test]
fn baarda_wtest_constant_and_monotonicity_are_pinned() {
let lambda0 = wtest_noncentrality(1.0e-3, 0.20).expect("canonical lambda");
let delta0 = lambda0.sqrt();
assert_close(delta0, 4.132147965064809, 2.0e-15);
assert_close(lambda0, 17.074646805189243, 4.0e-15);
assert_eq!((delta0 * 100.0).round() / 100.0, 4.13);
assert_eq!((lambda0 * 100.0).round() / 100.0, 17.07);
let smaller_alpha = wtest_noncentrality(1.0e-4, 0.20).expect("smaller alpha");
let smaller_beta = wtest_noncentrality(1.0e-3, 0.10).expect("smaller beta");
assert!(smaller_alpha > lambda0);
assert!(smaller_beta > lambda0);
}
#[test]
fn equal_precision_single_unknown_closed_form_is_exact_to_roundoff() {
let lambda0 = wtest_noncentrality(1.0e-3, 0.20).expect("lambda");
let sigma_m = 1.0;
let rows = equal_precision_rows(2, sigma_m);
let report = reliability_design(&rows, &ReliabilityOptions::default()).expect("report");
for obs in &report.per_observation {
assert_close(obs.redundancy, 0.5, 1.0e-15);
assert_close(
obs.mdb_m.expect("MDB"),
sigma_m * (2.0 * lambda0).sqrt(),
1.0e-14,
);
assert_close(obs.bias_to_noise.expect("BNR"), lambda0.sqrt(), 1.0e-14);
}
assert_eq!(
(report.per_observation[0].mdb_m.expect("MDB") * 100.0).round() / 100.0,
5.84
);
assert_eq!(
(report.per_observation[0].bias_to_noise.expect("BNR") * 100.0).round() / 100.0,
4.13
);
assert_close(report.summary.sum_redundancy, 1.0, 1.0e-15);
let report = reliability_design(
&equal_precision_rows(1, sigma_m),
&ReliabilityOptions::default(),
)
.expect("one-row report");
assert_eq!(report.summary.n_uncheckable, 1);
assert!(report.per_observation[0].uncheckable);
assert_eq!(report.per_observation[0].mdb_m, None);
assert_eq!(report.per_observation[0].bias_to_noise, None);
assert_eq!(report.per_observation[0].external_enu_m, None);
}
#[test]
fn teunissen_leveling_connection_matches_published_formula() {
let n = 4usize;
let sigma_h = 2.0;
let sigma_h_cap = 3.0;
let rows = leveling_connection_rows(n, sigma_h, sigma_h_cap);
let report = reliability_design(&rows, &options_with_lambda(LAMBDA_BAARDA_4DP))
.expect("leveling report");
let variance_sum = sigma_h * sigma_h + sigma_h_cap * sigma_h_cap;
let common = 1.0 - 1.0 / n as f64;
let expected_r_h = sigma_h * sigma_h * common / variance_sum;
let expected_r_h_cap = sigma_h_cap * sigma_h_cap * common / variance_sum;
let expected_mdb = (LAMBDA_BAARDA_4DP * variance_sum / common).sqrt();
for obs in &report.per_observation[..n] {
assert_close(obs.redundancy, expected_r_h, 2.0e-14);
assert_close(obs.mdb_m.expect("MDB"), expected_mdb, 2.0e-13);
}
for obs in &report.per_observation[n..] {
assert_close(obs.redundancy, expected_r_h_cap, 2.0e-14);
assert_close(obs.mdb_m.expect("MDB"), expected_mdb, 2.0e-13);
}
assert_close(report.summary.sum_redundancy, n as f64 - 1.0, 2.0e-13);
let one = reliability_design(
&leveling_connection_rows(1, sigma_h, sigma_h_cap),
&options_with_lambda(LAMBDA_BAARDA_4DP),
)
.expect("one station report");
assert_eq!(one.summary.n_uncheckable, 2);
assert!(one.per_observation.iter().all(|obs| obs.mdb_m.is_none()));
}
#[test]
fn araim_external_reliability_reuses_gain_matrix_columns() {
let geometry = simple_araim_geometry();
let model = UnitProtectionModel;
let options = options_with_lambda(LAMBDA_BAARDA_4DP);
let report = reliability_araim(&geometry, &model, &options).expect("ARAIM report");
let weights = vec![1.0; geometry.rows.len()];
let gain = gain_matrix_enu(&geometry, &weights).expect("gain");
for (idx, obs) in report.per_observation.iter().enumerate() {
let mdb = obs.mdb_m.expect("MDB");
let external = obs.external_enu_m.expect("external ENU");
for (coord, value) in external.iter().enumerate() {
assert_close(*value / mdb, gain.enu_rows[coord][idx], 2.0e-12);
}
}
let design_rows = geometry
.rows
.iter()
.map(|row| RangeReliabilityRow {
id: row.id.to_string(),
design_row: design_row(&geometry.clock_systems, row).expect("design row"),
sigma_m: 1.0,
})
.collect::<Vec<_>>();
let design_report = reliability_design(&design_rows, &options).expect("design report");
let enu = ecef_to_enu_rotation(geometry.receiver.lat_rad, geometry.receiver.lon_rad);
for (idx, obs) in design_report.per_observation.iter().enumerate() {
let mdb = obs.mdb_m.expect("MDB");
let state = obs.external_enu_m.expect("state external");
let rotated = mat3_vec(enu, state);
for (coord, value) in rotated.iter().enumerate() {
assert_close(*value / mdb, gain.enu_rows[coord][idx], 2.0e-12);
}
}
}
#[test]
fn invalid_inputs_use_quality_error_domains() {
let mut rows = equal_precision_rows(2, 1.0);
rows[0].sigma_m = 0.0;
assert_eq!(
reliability_design(&rows, &ReliabilityOptions::default()),
Err(QualityError::InvalidWeight)
);
rows = equal_precision_rows(2, 1.0);
let mut options = ReliabilityOptions {
beta: 0.0,
..ReliabilityOptions::default()
};
assert_eq!(
reliability_design(&rows, &options),
Err(QualityError::InvalidReliabilityParameter)
);
options = ReliabilityOptions {
lambda0_override: Some(f64::INFINITY),
..ReliabilityOptions::default()
};
assert_eq!(
reliability_design(&rows, &options),
Err(QualityError::InvalidReliabilityParameter)
);
options = ReliabilityOptions {
min_redundancy: 0.0,
..ReliabilityOptions::default()
};
assert_eq!(
reliability_design(&rows, &options),
Err(QualityError::InvalidReliabilityParameter)
);
options = ReliabilityOptions {
alpha: 1.0,
..ReliabilityOptions::default()
};
assert_eq!(
reliability_design(&rows, &options),
Err(QualityError::InvalidProbability)
);
let singular = vec![
RangeReliabilityRow {
id: "a".to_owned(),
design_row: vec![1.0, 1.0],
sigma_m: 1.0,
},
RangeReliabilityRow {
id: "b".to_owned(),
design_row: vec![2.0, 2.0],
sigma_m: 1.0,
},
];
assert_eq!(
reliability_design(&singular, &ReliabilityOptions::default()),
Err(QualityError::SingularGeometry)
);
}
struct UnitProtectionModel;
impl ProtectionModel for UnitProtectionModel {
fn sigma_int_m(&self, _row: &AraimRow) -> Result<f64, AraimError> {
Ok(1.0)
}
fn sigma_acc_m(&self, _row: &AraimRow) -> Result<f64, AraimError> {
Ok(1.0)
}
}
fn options_with_lambda(lambda0: f64) -> ReliabilityOptions {
ReliabilityOptions {
lambda0_override: Some(lambda0),
..ReliabilityOptions::default()
}
}
fn algebraic_rows() -> Vec<RangeReliabilityRow> {
vec![
row("r1", &[1.0, 0.3, -0.2], 0.9),
row("r2", &[0.2, 1.1, 0.4], 1.1),
row("r3", &[-0.5, 0.7, 1.0], 1.3),
row("r4", &[1.2, -0.4, 0.6], 0.8),
row("r5", &[-0.7, -0.9, 0.3], 1.5),
row("r6", &[0.4, -0.2, -1.1], 1.2),
]
}
fn equal_precision_rows(n: usize, sigma_m: f64) -> Vec<RangeReliabilityRow> {
(0..n)
.map(|idx| RangeReliabilityRow {
id: format!("obs{}", idx + 1),
design_row: vec![1.0],
sigma_m,
})
.collect()
}
fn leveling_connection_rows(
n: usize,
sigma_h: f64,
sigma_h_cap: f64,
) -> Vec<RangeReliabilityRow> {
let mut rows = Vec::with_capacity(2 * n);
for idx in 0..n {
let mut design = vec![0.0; n + 1];
design[idx] = 1.0;
design[n] = 1.0;
rows.push(RangeReliabilityRow {
id: format!("h{}", idx + 1),
design_row: design,
sigma_m: sigma_h,
});
}
for idx in 0..n {
let mut design = vec![0.0; n + 1];
design[idx] = 1.0;
rows.push(RangeReliabilityRow {
id: format!("H{}", idx + 1),
design_row: design,
sigma_m: sigma_h_cap,
});
}
rows
}
fn row(id: &str, design_row: &[f64], sigma_m: f64) -> RangeReliabilityRow {
RangeReliabilityRow {
id: id.to_owned(),
design_row: design_row.to_vec(),
sigma_m,
}
}
fn simple_araim_geometry() -> AraimGeometry {
const INV_SQRT_3: f64 = 0.5773502691896258;
let receiver = Wgs84Geodetic::new(0.5, 0.2, 0.0).expect("valid receiver");
let vectors = [
[INV_SQRT_3, INV_SQRT_3, INV_SQRT_3],
[INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3],
[-INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3],
[-INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3],
[0.2, 0.6, 0.7745966692414834],
];
let rows = vectors
.iter()
.enumerate()
.map(|(idx, los)| AraimRow {
id: GnssSatelliteId::new(GnssSystem::Gps, (idx + 1) as u8)
.expect("valid satellite"),
line_of_sight: LineOfSight::new(los[0], los[1], los[2]),
system: GnssSystem::Gps,
elevation_rad: 0.8,
})
.collect();
AraimGeometry {
rows,
receiver,
clock_systems: vec![GnssSystem::Gps],
}
}
fn design_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
rows.iter().map(|row| row.design_row.clone()).collect()
}
fn weight_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
let mut w = vec![vec![0.0; rows.len()]; rows.len()];
for (idx, row) in rows.iter().enumerate() {
w[idx][idx] = 1.0 / (row.sigma_m * row.sigma_m);
}
w
}
fn residual_projector(rows: &[RangeReliabilityRow], q_xx: &[Vec<f64>]) -> Vec<Vec<f64>> {
let h = design_matrix(rows);
let h_t = transpose(&h);
let w = weight_matrix(rows);
let h_q = mat_mul(&h, q_xx);
let h_q_h_t_w = mat_mul(&mat_mul(&h_q, &h_t), &w);
let mut out = identity(rows.len());
for i in 0..rows.len() {
for j in 0..rows.len() {
out[i][j] -= h_q_h_t_w[i][j];
}
}
out
}
fn q_v_matrix(rows: &[RangeReliabilityRow], q_xx: &[Vec<f64>]) -> Vec<Vec<f64>> {
let h = design_matrix(rows);
let h_t = transpose(&h);
let h_q_h_t = mat_mul(&mat_mul(&h, q_xx), &h_t);
let mut q_v = vec![vec![0.0; rows.len()]; rows.len()];
for i in 0..rows.len() {
q_v[i][i] = rows[i].sigma_m * rows[i].sigma_m;
for j in 0..rows.len() {
q_v[i][j] -= h_q_h_t[i][j];
}
}
q_v
}
fn normal_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
let n_params = rows[0].design_row.len();
normal_equations_weighted(
rows.iter()
.map(|row| (row.design_row.as_slice(), 0.0, 1.0 / row.sigma_m)),
n_params,
)
.expect("normal matrix")
.0
}
fn full_state_external(row: &RangeReliabilityRow, q_xx: &[Vec<f64>], mdb_m: f64) -> Vec<f64> {
let scale = mdb_m / (row.sigma_m * row.sigma_m);
mat_vec(q_xx, &row.design_row)
.iter()
.map(|value| value * scale)
.collect()
}
fn identity(n: usize) -> Vec<Vec<f64>> {
let mut out = vec![vec![0.0; n]; n];
for (idx, row) in out.iter_mut().enumerate() {
row[idx] = 1.0;
}
out
}
fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
let b_t = transpose(b);
a.iter()
.map(|row| b_t.iter().map(|col| dot(row, col)).collect())
.collect()
}
fn mat_sub(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
a.iter()
.zip(b)
.map(|(ra, rb)| ra.iter().zip(rb).map(|(x, y)| x - y).collect())
.collect()
}
fn transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
let cols = a[0].len();
(0..cols)
.map(|col| a.iter().map(|row| row[col]).collect())
.collect()
}
fn trace(a: &[Vec<f64>]) -> f64 {
a.iter().enumerate().map(|(idx, row)| row[idx]).sum()
}
fn inf_norm(a: &[Vec<f64>]) -> f64 {
a.iter()
.map(|row| row.iter().map(|value| value.abs()).sum::<f64>())
.fold(0.0, f64::max)
}
fn quadratic_form(a: &[Vec<f64>], x: &[f64]) -> f64 {
dot(x, &mat_vec(a, x))
}
fn mat3_vec(a: [[f64; 3]; 3], x: [f64; 3]) -> [f64; 3] {
[
a[0][0] * x[0] + a[0][1] * x[1] + a[0][2] * x[2],
a[1][0] * x[0] + a[1][1] * x[1] + a[1][2] * x[2],
a[2][0] * x[0] + a[2][1] * x[1] + a[2][2] * x[2],
]
}
fn assert_close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() <= tol,
"actual={actual:.17e} expected={expected:.17e} tol={tol:.17e}"
);
}
}