use super::fault_modes::enumerate_fault_modes;
use super::ism::{ConstellationIsm, Ism, SatelliteIsm, SatelliteIsmModel};
use super::protection::{gain_matrix_enu, metric_bias, metric_sigma};
use super::{araim, AraimError, AraimGeometry, AraimRow, IntegrityAllocation};
use crate::astro::math::least_squares::Status;
use crate::astro::math::special::normal_q_inv;
use crate::dop::{ecef_to_enu_rotation, line_of_sight_from_az_el_deg, LineOfSight};
use crate::frame::{ItrfPositionM, Wgs84Geodetic};
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::quality::{raim_fde_design, RangeFdeOptions, RangeFdeRow};
use crate::spp::{EphemerisSource, ReceiverSolution, SolutionMetadata};
const INV_SQRT_3: f64 = 0.577_350_269_189_625_8;
const WG_C_SIGMA_URA_M: f64 = 0.75;
const WG_C_SIGMA_URE_M: f64 = 0.5;
const WG_C_B_NOM_M: f64 = 0.75;
const WG_C_EXAMPLE_B_NOM_M: f64 = 0.5;
const WG_C_P_SAT: f64 = 1.0e-5;
const WG_C_P_CONST_GPS: f64 = 0.0;
const WG_C_P_CONST_GAL: f64 = 1.0e-4;
const WG_C_EXAMPLE_P_CONST: f64 = 1.0e-4;
const WG_C_LPV_200_PHMI_TOTAL: f64 = 1.0e-7;
const WG_C_LPV_200_PHMI_VERT: f64 = 9.8e-8;
const WG_C_LPV_200_PHMI_HOR: f64 = 2.0e-9;
const WG_C_LPV_200_PFA_VERT: f64 = 3.9e-6;
const WG_C_LPV_200_PFA_HOR: f64 = 9.0e-8;
const WG_C_LPV_200_PTHRES: f64 = 8.0e-8;
const WG_C_LPV_200_PEMT: f64 = 1.0e-5;
const PL_REFERENCE_TOL_M: f64 = 0.05;
const SIGMA_REFERENCE_TOL_M: f64 = 0.02;
const WG_C_INTERMEDIATE_TOL_M: f64 = 5.0e-4;
const WG_C_ADD_V3_ROWS: [(GnssSystem, u8, [f64; 3], f64, f64); 10] = [
(
GnssSystem::Gps,
1,
[0.0225, 0.9951, -0.0966],
3.8865,
3.5740,
),
(
GnssSystem::Gps,
2,
[0.6750, -0.6900, -0.2612],
1.4377,
1.1252,
),
(
GnssSystem::Gps,
3,
[0.0723, -0.6601, -0.7477],
0.8604,
0.5479,
),
(
GnssSystem::Gps,
4,
[-0.9398, 0.2553, -0.2269],
1.6383,
1.3258,
),
(
GnssSystem::Gps,
5,
[-0.5907, -0.7539, -0.2877],
1.3229,
1.0104,
),
(
GnssSystem::Galileo,
1,
[-0.3236, -0.0354, -0.9455],
0.8434,
0.5309,
),
(
GnssSystem::Galileo,
2,
[-0.6748, 0.4356, -0.5957],
0.8963,
0.5838,
),
(
GnssSystem::Galileo,
3,
[0.0938, -0.7004, -0.7075],
0.8669,
0.5544,
),
(
GnssSystem::Galileo,
4,
[0.5571, 0.3088, -0.7709],
0.8573,
0.5448,
),
(
GnssSystem::Galileo,
5,
[0.6622, 0.6958, -0.2780],
1.3616,
1.0491,
),
];
#[test]
fn fault_free_only_reduces_to_closed_form_pl() {
let geometry = gps_geometry();
let ism = Ism::new(
vec![ConstellationIsm::new(
GnssSystem::Gps,
0.0,
SatelliteIsmModel::new(2.0, 1.0, 0.25, 0.0),
)],
Vec::new(),
);
let allocation = IntegrityAllocation {
phmi_total: 1.0e-7,
phmi_vert: WG_C_LPV_200_PHMI_VERT,
phmi_hor: WG_C_LPV_200_PHMI_HOR,
pfa_vert: WG_C_LPV_200_PFA_VERT,
pfa_hor: WG_C_LPV_200_PFA_HOR,
p_threshold_unmonitored: 0.0,
p_emt: WG_C_LPV_200_PEMT,
max_fault_order: 0,
};
let result = araim(&geometry, &ism, &allocation).expect("fault-free ARAIM");
let fault_free = &result.fault_modes[0];
let k = normal_q_inv(allocation.phmi_vert * 0.5).expect("valid PHMI");
let expected_vpl = k * fault_free.sigma_int_enu_m[2] + fault_free.bias_enu_m[2];
assert_abs_diff(result.vpl_m, expected_vpl, 1.0e-9);
let k_h = normal_q_inv(allocation.phmi_hor * 0.25).expect("valid PHMI");
let expected_e = k_h * fault_free.sigma_int_enu_m[0] + fault_free.bias_enu_m[0];
let expected_n = k_h * fault_free.sigma_int_enu_m[1] + fault_free.bias_enu_m[1];
let expected_hpl = (expected_e * expected_e + expected_n * expected_n).sqrt();
assert_abs_diff(result.hpl_m, expected_hpl, 1.0e-9);
assert_eq!(result.emt_m, 0.0);
assert!(result.availability);
}
#[test]
fn weight_zeroed_gain_matches_row_deleted_wls() {
let geometry = gps_geometry_with_extra_row();
let weights = vec![0.0, 1.0, 1.0, 1.0, 1.0];
let gain_zeroed = gain_matrix_enu(&geometry, &weights).expect("zeroed gain");
let deleted_geometry = AraimGeometry {
rows: geometry.rows[1..].to_vec(),
receiver: geometry.receiver,
clock_systems: geometry.clock_systems.clone(),
};
let deleted_weights = vec![1.0; deleted_geometry.rows.len()];
let gain_deleted = gain_matrix_enu(&deleted_geometry, &deleted_weights).expect("deleted gain");
for coord in 0..3 {
assert_eq!(gain_zeroed.enu_rows[coord][0], 0.0);
for idx in 1..geometry.rows.len() {
assert_abs_diff(
gain_zeroed.enu_rows[coord][idx],
gain_deleted.enu_rows[coord][idx - 1],
2.0e-12,
);
}
}
let residuals = [0.0, 0.4, -0.2, 0.7, -0.1];
let enu_from_gain = [
dot(&gain_zeroed.enu_rows[0], &residuals),
dot(&gain_zeroed.enu_rows[1], &residuals),
dot(&gain_zeroed.enu_rows[2], &residuals),
];
let fde_rows: Vec<RangeFdeRow> = geometry.rows[1..]
.iter()
.zip(residuals[1..].iter())
.map(|(row, &residual_m)| RangeFdeRow {
id: row.id.to_string(),
residual_m,
design_row: vec![
-row.line_of_sight.e_x,
-row.line_of_sight.e_y,
-row.line_of_sight.e_z,
1.0,
],
weight: 1.0,
})
.collect();
let fit = raim_fde_design(
&fde_rows,
&RangeFdeOptions {
max_exclusions: 0,
..RangeFdeOptions::default()
},
)
.expect("row-deleted WLS");
let r = ecef_to_enu_rotation(geometry.receiver.lat_rad, geometry.receiver.lon_rad);
let dx = &fit.state_correction;
let enu_from_wls = [
r[0][0] * dx[0] + r[0][1] * dx[1] + r[0][2] * dx[2],
r[1][0] * dx[0] + r[1][1] * dx[1] + r[1][2] * dx[2],
r[2][0] * dx[0] + r[2][1] * dx[1] + r[2][2] * dx[2],
];
for coord in 0..3 {
assert_abs_diff(enu_from_gain[coord], enu_from_wls[coord], 2.0e-12);
}
}
#[test]
fn fault_modes_are_prior_ordered_and_pinned() {
let geometry = mixed_geometry_for_enumeration();
let ism = Ism::new(
vec![
ConstellationIsm::new(
GnssSystem::Gps,
1.0e-3,
SatelliteIsmModel::new(2.0, 1.0, 0.0, 0.01),
),
ConstellationIsm::new(
GnssSystem::Galileo,
2.0e-3,
SatelliteIsmModel::new(2.0, 1.0, 0.0, 0.03),
),
],
vec![SatelliteIsm::new(
sat(GnssSystem::Gps, 2),
2.0,
1.0,
0.0,
0.02,
)],
);
let allocation = IntegrityAllocation {
phmi_total: 1.0e-7,
phmi_vert: WG_C_LPV_200_PHMI_VERT,
phmi_hor: WG_C_LPV_200_PHMI_HOR,
pfa_vert: WG_C_LPV_200_PFA_VERT,
pfa_hor: WG_C_LPV_200_PFA_HOR,
p_threshold_unmonitored: 0.0,
p_emt: WG_C_LPV_200_PEMT,
max_fault_order: 2,
};
let modes = enumerate_fault_modes(&geometry, &ism, &allocation).expect("fault modes");
assert_eq!(modes.len(), 9);
assert_eq!(modes[0].excluded, Vec::<GnssSatelliteId>::new());
assert_eq!(modes[1].excluded, vec![sat(GnssSystem::Gps, 1)]);
assert_eq!(modes[2].excluded, vec![sat(GnssSystem::Gps, 2)]);
assert_eq!(modes[3].excluded, vec![sat(GnssSystem::Galileo, 1)]);
assert_eq!(modes[4].excluded_constellation, Some(GnssSystem::Gps));
assert_eq!(modes[5].excluded_constellation, Some(GnssSystem::Galileo));
assert_eq!(
modes[6].excluded,
vec![sat(GnssSystem::Gps, 2), sat(GnssSystem::Galileo, 1)]
);
assert_eq!(
modes[7].excluded,
vec![sat(GnssSystem::Gps, 1), sat(GnssSystem::Galileo, 1)]
);
assert_eq!(
modes[8].excluded,
vec![sat(GnssSystem::Gps, 1), sat(GnssSystem::Gps, 2)]
);
assert_abs_diff(modes[6].prior, 6.0e-4, 0.0);
assert_abs_diff(modes[7].prior, 3.0e-4, 0.0);
assert_abs_diff(modes[8].prior, 2.0e-4, 0.0);
}
#[test]
fn constellation_fault_modes_drop_excluded_clock_and_stay_monitorable() {
let geometry = reference_gps_galileo_geometry();
let ism = reference_gps_galileo_ism();
let allocation = IntegrityAllocation::lpv_200();
let result = araim(&geometry, &ism, &allocation).expect("GPS plus Galileo ARAIM");
assert!(result.availability);
assert!(result.hpl_m.is_finite());
assert!(result.vpl_m.is_finite());
assert!(result.p_unmonitored <= allocation.p_threshold_unmonitored);
let galileo_constellation = result
.fault_modes
.iter()
.find(|mode| mode.excluded_constellation == Some(GnssSystem::Galileo))
.expect("Galileo constellation mode");
assert!(galileo_constellation.monitorable);
}
#[test]
fn lpv_200_allocation_matches_wg_c_baseline() {
let allocation = IntegrityAllocation::lpv_200();
assert_abs_diff(allocation.phmi_total, WG_C_LPV_200_PHMI_TOTAL, 0.0);
assert_abs_diff(allocation.phmi_vert, WG_C_LPV_200_PHMI_VERT, 0.0);
assert_abs_diff(allocation.phmi_hor, WG_C_LPV_200_PHMI_HOR, 0.0);
assert_abs_diff(allocation.pfa_vert, WG_C_LPV_200_PFA_VERT, 0.0);
assert_abs_diff(allocation.pfa_hor, WG_C_LPV_200_PFA_HOR, 0.0);
assert_abs_diff(allocation.p_threshold_unmonitored, WG_C_LPV_200_PTHRES, 0.0);
assert_abs_diff(allocation.p_emt, WG_C_LPV_200_PEMT, 0.0);
assert!(phmi_split_fits_total(
allocation.phmi_vert,
allocation.phmi_hor,
allocation.phmi_total
));
}
#[test]
fn allocation_rejects_phmi_split_above_total() {
let mut allocation = IntegrityAllocation::lpv_200();
allocation.phmi_vert = 1.0e-7;
allocation.phmi_hor = 1.0e-7;
let error = araim(&reference_gps_geometry(), &reference_gps_ism(), &allocation)
.expect_err("invalid PHMI split");
assert_eq!(error, AraimError::InvalidAllocation);
}
#[test]
fn wg_c_add_v3_numerical_example_matches_published_pls() {
let allocation = IntegrityAllocation::lpv_200();
let result = araim(
&wg_c_add_v3_numerical_example_geometry(),
&wg_c_add_v3_numerical_example_ism(),
&allocation,
)
.expect("WG-C ADD v3.0 Appendix D ARAIM");
assert!(result.availability);
let n_fault_modes = result.fault_modes.len() - 1;
assert_eq!(n_fault_modes, 12);
let k_fa_3 =
normal_q_inv(allocation.pfa_vert / (2.0 * n_fault_modes as f64)).expect("valid PFA");
assert_abs_diff(k_fa_3, 5.1083, WG_C_INTERMEDIATE_TOL_M);
assert_constellation_intermediates(&result, GnssSystem::Gps, k_fa_3, 2.5760, 1.5307, 2.8935);
assert_constellation_intermediates(
&result,
GnssSystem::Galileo,
k_fa_3,
2.5577,
1.5292,
2.0875,
);
assert_abs_diff(result.vpl_m, 19.2, PL_REFERENCE_TOL_M);
assert_abs_diff(result.hpl_m, 14.5, PL_REFERENCE_TOL_M);
assert_abs_diff(result.emt_m, 7.8, PL_REFERENCE_TOL_M);
assert_abs_diff(result.sigma_acc_v_m, 1.47, SIGMA_REFERENCE_TOL_M);
}
#[test]
fn synthetic_lpv_200_modes_are_regression_pinned() {
let allocation = IntegrityAllocation::lpv_200();
let gps_result = araim(&reference_gps_geometry(), &reference_gps_ism(), &allocation)
.expect("GPS-only LPV-200 ARAIM");
assert!(gps_result.availability);
assert_eq!(
monitored_mode_keys(&gps_result),
(1..=6)
.map(|prn| sat(GnssSystem::Gps, prn).to_string())
.collect::<Vec<_>>()
);
let mixed_result = araim(
&reference_gps_galileo_geometry(),
&reference_gps_galileo_ism(),
&allocation,
)
.expect("GPS plus Galileo LPV-200 ARAIM");
assert!(mixed_result.availability);
let mut expected_mixed = (1..=6)
.map(|prn| sat(GnssSystem::Gps, prn).to_string())
.collect::<Vec<_>>();
expected_mixed.extend((1..=5).map(|prn| sat(GnssSystem::Galileo, prn).to_string()));
expected_mixed.push("Galileo*".to_string());
assert_eq!(monitored_mode_keys(&mixed_result), expected_mixed);
}
#[test]
fn hand_worked_s_matrix_and_fault_free_pl_are_pinned() {
let geometry = gps_geometry();
let weights = vec![1.0; geometry.rows.len()];
let gain = gain_matrix_enu(&geometry, &weights).expect("gain matrix");
let s = 0.75 * INV_SQRT_3;
let expected = [vec![-s, s, -s, s], vec![-s, s, s, -s], vec![-s, -s, s, s]];
for (coord, expected_row) in expected.iter().enumerate() {
for (idx, &expected_value) in expected_row.iter().enumerate() {
assert_abs_diff(gain.enu_rows[coord][idx], expected_value, 2.0e-15);
}
}
let sigmas = vec![1.0; geometry.rows.len()];
let biases = vec![0.25; geometry.rows.len()];
let expected_sigma = 2.0 * s;
let expected_bias = s;
let target = IntegrityAllocation::lpv_200().phmi_vert * 0.5;
let expected_pl = normal_q_inv(target).expect("valid PHMI") * expected_sigma + expected_bias;
let sigma = metric_sigma(&gain.enu_rows[2], &sigmas);
let bias = metric_bias(&gain.enu_rows[2], &biases);
assert_abs_diff(sigma, expected_sigma, 2.0e-15);
assert_abs_diff(bias, expected_bias, 2.0e-15);
assert_abs_diff(
normal_q_inv(target).expect("valid PHMI") * sigma + bias,
expected_pl,
2.0e-12,
);
}
#[test]
fn max_fault_order_one_counts_pair_and_higher_mass_unmonitored() {
let geometry = gps_geometry_with_extra_row();
let p_sat = 0.01;
let ism = Ism::new(
vec![ConstellationIsm::new(
GnssSystem::Gps,
0.0,
SatelliteIsmModel::new(2.0, 1.0, 0.25, p_sat),
)],
Vec::new(),
);
let allocation = IntegrityAllocation {
phmi_total: 1.0e-2,
phmi_vert: 9.8e-3,
phmi_hor: 2.0e-4,
pfa_vert: WG_C_LPV_200_PFA_VERT,
pfa_hor: WG_C_LPV_200_PFA_HOR,
p_threshold_unmonitored: 0.01,
p_emt: WG_C_LPV_200_PEMT,
max_fault_order: 1,
};
let result = araim(&geometry, &ism, &allocation).expect("ARAIM with unmonitored tail");
let expected_pair_plus =
10.0 * p_sat.powi(2) + 10.0 * p_sat.powi(3) + 5.0 * p_sat.powi(4) + p_sat.powi(5);
assert_abs_diff(result.p_unmonitored, expected_pair_plus, 2.0e-16);
}
#[test]
fn receiver_solution_adapter_rebuilds_geometry() {
let source_geometry = gps_geometry();
let receiver_ecef = [6_378_137.0, 0.0, 0.0];
let positions = source_geometry
.rows
.iter()
.map(|row| {
(
row.id,
[
receiver_ecef[0] + 20_200_000.0 * row.line_of_sight.e_x,
receiver_ecef[1] + 20_200_000.0 * row.line_of_sight.e_y,
receiver_ecef[2] + 20_200_000.0 * row.line_of_sight.e_z,
],
)
})
.collect();
let eph = StaticEphemeris { positions };
let solution = receiver_solution(
source_geometry.rows.iter().map(|row| row.id).collect(),
vec![GnssSystem::Gps],
);
let rebuilt = AraimGeometry::from_receiver_solution(&solution, &eph, 12_345.0)
.expect("rebuilt ARAIM geometry");
assert_eq!(rebuilt.clock_systems, vec![GnssSystem::Gps]);
assert_eq!(rebuilt.rows.len(), source_geometry.rows.len());
for (rebuilt, source) in rebuilt.rows.iter().zip(source_geometry.rows.iter()) {
assert_eq!(rebuilt.id, source.id);
assert_eq!(rebuilt.system, source.system);
assert_abs_diff(rebuilt.line_of_sight.e_x, source.line_of_sight.e_x, 2.0e-16);
assert_abs_diff(rebuilt.line_of_sight.e_y, source.line_of_sight.e_y, 2.0e-16);
assert_abs_diff(rebuilt.line_of_sight.e_z, source.line_of_sight.e_z, 2.0e-16);
assert!(rebuilt.elevation_rad.is_finite());
}
}
fn gps_geometry() -> AraimGeometry {
AraimGeometry {
rows: vec![
row(GnssSystem::Gps, 1, [INV_SQRT_3, INV_SQRT_3, INV_SQRT_3]),
row(GnssSystem::Gps, 2, [INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3]),
row(GnssSystem::Gps, 3, [-INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3]),
row(GnssSystem::Gps, 4, [-INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3]),
],
receiver: receiver(),
clock_systems: vec![GnssSystem::Gps],
}
}
fn gps_geometry_with_extra_row() -> AraimGeometry {
let mut geometry = gps_geometry();
geometry
.rows
.push(row(GnssSystem::Gps, 5, [0.2, 0.6, 0.774_596_669_241_483_4]));
geometry
}
fn mixed_geometry_for_enumeration() -> AraimGeometry {
AraimGeometry {
rows: vec![
row(GnssSystem::Gps, 1, [INV_SQRT_3, INV_SQRT_3, INV_SQRT_3]),
row(GnssSystem::Gps, 2, [INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3]),
row(
GnssSystem::Galileo,
1,
[-INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3],
),
],
receiver: receiver(),
clock_systems: vec![GnssSystem::Gps, GnssSystem::Galileo],
}
}
fn reference_gps_geometry() -> AraimGeometry {
AraimGeometry {
rows: vec![
row_az_el(GnssSystem::Gps, 1, 0.0, 65.0),
row_az_el(GnssSystem::Gps, 2, 60.0, 35.0),
row_az_el(GnssSystem::Gps, 3, 120.0, 55.0),
row_az_el(GnssSystem::Gps, 4, 190.0, 30.0),
row_az_el(GnssSystem::Gps, 5, 250.0, 50.0),
row_az_el(GnssSystem::Gps, 6, 310.0, 40.0),
],
receiver: receiver(),
clock_systems: vec![GnssSystem::Gps],
}
}
fn reference_gps_galileo_geometry() -> AraimGeometry {
let mut geometry = reference_gps_geometry();
geometry.rows.extend([
row_az_el(GnssSystem::Galileo, 1, 20.0, 55.0),
row_az_el(GnssSystem::Galileo, 2, 95.0, 35.0),
row_az_el(GnssSystem::Galileo, 3, 170.0, 60.0),
row_az_el(GnssSystem::Galileo, 4, 245.0, 40.0),
row_az_el(GnssSystem::Galileo, 5, 320.0, 50.0),
]);
geometry.clock_systems = vec![GnssSystem::Gps, GnssSystem::Galileo];
geometry
}
fn wg_c_add_v3_numerical_example_geometry() -> AraimGeometry {
AraimGeometry {
rows: WG_C_ADD_V3_ROWS
.into_iter()
.map(|(system, prn, design_enu, _, _)| row_from_wg_c_design(system, prn, design_enu))
.collect(),
receiver: receiver(),
clock_systems: vec![GnssSystem::Gps, GnssSystem::Galileo],
}
}
fn wg_c_add_v3_numerical_example_ism() -> Ism {
let model = SatelliteIsmModel::new(
WG_C_SIGMA_URA_M,
WG_C_SIGMA_URE_M,
WG_C_EXAMPLE_B_NOM_M,
WG_C_P_SAT,
);
let satellites = WG_C_ADD_V3_ROWS
.into_iter()
.map(|(system, prn, _, c_int_m2, c_acc_m2)| {
SatelliteIsm::new_with_effective_sigmas(
sat(system, prn),
WG_C_SIGMA_URA_M,
WG_C_SIGMA_URE_M,
WG_C_EXAMPLE_B_NOM_M,
WG_C_P_SAT,
c_int_m2.sqrt(),
c_acc_m2.sqrt(),
)
})
.collect();
Ism::new(
vec![
ConstellationIsm::new(GnssSystem::Gps, WG_C_EXAMPLE_P_CONST, model),
ConstellationIsm::new(GnssSystem::Galileo, WG_C_EXAMPLE_P_CONST, model),
],
satellites,
)
}
fn reference_gps_ism() -> Ism {
Ism::new(
vec![ConstellationIsm::new(
GnssSystem::Gps,
WG_C_P_CONST_GPS,
reference_sat_model(),
)],
Vec::new(),
)
}
fn reference_gps_galileo_ism() -> Ism {
Ism::new(
vec![
ConstellationIsm::new(GnssSystem::Gps, WG_C_P_CONST_GPS, reference_sat_model()),
ConstellationIsm::new(GnssSystem::Galileo, WG_C_P_CONST_GAL, reference_sat_model()),
],
Vec::new(),
)
}
const fn reference_sat_model() -> SatelliteIsmModel {
SatelliteIsmModel::new(WG_C_SIGMA_URA_M, WG_C_SIGMA_URE_M, WG_C_B_NOM_M, WG_C_P_SAT)
}
fn row(system: GnssSystem, prn: u8, los: [f64; 3]) -> AraimRow {
AraimRow {
id: sat(system, prn),
line_of_sight: LineOfSight::new(los[0], los[1], los[2]),
system,
elevation_rad: core::f64::consts::FRAC_PI_2,
}
}
fn row_az_el(system: GnssSystem, prn: u8, az_deg: f64, el_deg: f64) -> AraimRow {
AraimRow {
id: sat(system, prn),
line_of_sight: line_of_sight_from_az_el_deg(az_deg, el_deg, receiver())
.expect("valid az/el"),
system,
elevation_rad: el_deg.to_radians(),
}
}
fn sat(system: GnssSystem, prn: u8) -> GnssSatelliteId {
GnssSatelliteId::new(system, prn).expect("valid satellite")
}
fn receiver() -> Wgs84Geodetic {
Wgs84Geodetic::new(0.0, 0.0, 0.0).expect("valid receiver")
}
fn dot(row: &[f64], residuals: &[f64]) -> f64 {
row.iter().zip(residuals).map(|(a, b)| a * b).sum()
}
fn monitored_mode_keys(result: &super::AraimResult) -> Vec<String> {
result
.fault_modes
.iter()
.skip(1)
.filter(|mode| mode.monitorable)
.map(|mode| {
if let Some(system) = mode.excluded_constellation {
format!("{system:?}*")
} else {
mode.excluded
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("+")
}
})
.collect()
}
fn assert_constellation_intermediates(
result: &super::AraimResult,
system: GnssSystem,
k_fa_3: f64,
sigma_3_m: f64,
sigma_ss_3_m: f64,
b_3_m: f64,
) {
let mode = result
.fault_modes
.iter()
.find(|mode| mode.excluded_constellation == Some(system))
.expect("constellation mode");
assert_abs_diff(mode.sigma_int_enu_m[2], sigma_3_m, WG_C_INTERMEDIATE_TOL_M);
assert_abs_diff(
mode.threshold_enu_m[2] / k_fa_3,
sigma_ss_3_m,
WG_C_INTERMEDIATE_TOL_M,
);
assert_abs_diff(mode.bias_enu_m[2], b_3_m, WG_C_INTERMEDIATE_TOL_M);
}
fn row_from_wg_c_design(system: GnssSystem, prn: u8, design_enu: [f64; 3]) -> AraimRow {
let receiver = receiver();
let r = ecef_to_enu_rotation(receiver.lat_rad, receiver.lon_rad);
let east = -design_enu[0];
let north = -design_enu[1];
let up = -design_enu[2];
AraimRow {
id: sat(system, prn),
line_of_sight: LineOfSight::new(
r[0][0] * east + r[1][0] * north + r[2][0] * up,
r[0][1] * east + r[1][1] * north + r[2][1] * up,
r[0][2] * east + r[1][2] * north + r[2][2] * up,
),
system,
elevation_rad: core::f64::consts::FRAC_PI_2,
}
}
fn phmi_split_fits_total(phmi_vert: f64, phmi_hor: f64, phmi_total: f64) -> bool {
phmi_vert + phmi_hor <= phmi_total + phmi_total * 16.0 * f64::EPSILON
}
fn receiver_solution(
used_sats: Vec<GnssSatelliteId>,
systems: Vec<GnssSystem>,
) -> ReceiverSolution {
let used_count = used_sats.len();
let redundancy = used_count as isize - (3 + systems.len()) as isize;
ReceiverSolution {
position: ItrfPositionM::new(6_378_137.0, 0.0, 0.0).expect("valid receiver"),
geodetic: Some(receiver()),
rx_clock_s: 0.0,
system_clocks_s: systems.iter().map(|&system| (system, 0.0)).collect(),
dop: None,
system_tdops: Vec::new(),
residuals_m: vec![0.0; used_count],
used_sats,
rejected_sats: Vec::new(),
metadata: SolutionMetadata {
iterations: 1,
converged: true,
status: Status::StepTolerance,
ionosphere_applied: false,
troposphere_applied: false,
outer_iterations: 0,
final_robust_scale_m: None,
used_count,
systems,
redundancy,
raim_checkable: redundancy >= 1,
},
}
}
struct StaticEphemeris {
positions: Vec<(GnssSatelliteId, [f64; 3])>,
}
impl EphemerisSource for StaticEphemeris {
fn position_clock_at_j2000_s(
&self,
sat: GnssSatelliteId,
_t_j2000_s: f64,
) -> Option<([f64; 3], f64)> {
self.positions
.iter()
.find(|(id, _)| *id == sat)
.map(|&(_, position)| (position, 0.0))
}
}
fn assert_abs_diff(left: f64, right: f64, tol: f64) {
let diff = (left - right).abs();
assert!(
diff <= tol,
"left={left} right={right} diff={diff} tol={tol}"
);
}