pub mod fault_modes;
pub mod ism;
mod mhss;
pub mod protection;
#[cfg(test)]
mod tests;
pub use fault_modes::{enumerate_fault_modes, FaultHypothesis};
pub use ism::{ConstellationIsm, Ism, SatelliteIsm, SatelliteIsmModel};
pub use mhss::{araim, AraimResult, FaultMode};
use crate::astro::frames::transforms::geodetic_from_ecef_proj;
use crate::dop::{ecef_to_enu_rotation, LineOfSight};
use crate::frame::Wgs84Geodetic;
use crate::id::{GnssSatelliteId, GnssSystem};
use crate::spp::{EphemerisSource, ReceiverSolution};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AraimRow {
pub id: GnssSatelliteId,
pub line_of_sight: LineOfSight,
pub system: GnssSystem,
pub elevation_rad: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AraimGeometry {
pub rows: Vec<AraimRow>,
pub receiver: Wgs84Geodetic,
pub clock_systems: Vec<GnssSystem>,
}
impl AraimGeometry {
pub fn from_receiver_solution(
solution: &ReceiverSolution,
eph: &dyn EphemerisSource,
t_j2000_s: f64,
) -> Result<Self, AraimError> {
if !t_j2000_s.is_finite() {
return Err(AraimError::InsufficientGeometry);
}
let receiver = match solution.geodetic {
Some(receiver) => receiver,
None => geodetic_from_position(solution.position.as_array())?,
};
let clock_systems = receiver_solution_clock_systems(solution)?;
if solution.used_sats.len() < 3 + clock_systems.len() {
return Err(AraimError::InsufficientGeometry);
}
let rx_ecef_m = solution.position.as_array();
let enu = ecef_to_enu_rotation(receiver.lat_rad, receiver.lon_rad);
let mut rows = Vec::with_capacity(solution.used_sats.len());
for &id in &solution.used_sats {
let (sat_ecef_m, _) = eph
.position_clock_at_j2000_s(id, t_j2000_s)
.ok_or(AraimError::InsufficientGeometry)?;
let dx = sat_ecef_m[0] - rx_ecef_m[0];
let dy = sat_ecef_m[1] - rx_ecef_m[1];
let dz = sat_ecef_m[2] - rx_ecef_m[2];
let range_m = (dx * dx + dy * dy + dz * dz).sqrt();
if !range_m.is_finite() || range_m <= 0.0 {
return Err(AraimError::InsufficientGeometry);
}
let line_of_sight = LineOfSight::new(dx / range_m, dy / range_m, dz / range_m);
let up = enu[2][0] * line_of_sight.e_x
+ enu[2][1] * line_of_sight.e_y
+ enu[2][2] * line_of_sight.e_z;
let elevation_rad = up.clamp(-1.0, 1.0).asin();
if !elevation_rad.is_finite() {
return Err(AraimError::InsufficientGeometry);
}
rows.push(AraimRow {
id,
line_of_sight,
system: id.system,
elevation_rad,
});
}
Ok(Self {
rows,
receiver,
clock_systems,
})
}
}
fn geodetic_from_position(position_m: [f64; 3]) -> Result<Wgs84Geodetic, AraimError> {
let [lon_deg, lat_deg, height_m] =
geodetic_from_ecef_proj(position_m[0], position_m[1], position_m[2])
.map_err(|_| AraimError::InsufficientGeometry)?;
Wgs84Geodetic::new(lat_deg.to_radians(), lon_deg.to_radians(), height_m)
.map_err(|_| AraimError::InsufficientGeometry)
}
fn receiver_solution_clock_systems(
solution: &ReceiverSolution,
) -> Result<Vec<GnssSystem>, AraimError> {
let clock_systems = if !solution.system_clocks_s.is_empty() {
solution
.system_clocks_s
.iter()
.map(|&(system, _)| system)
.collect()
} else if !solution.metadata.systems.is_empty() {
solution.metadata.systems.clone()
} else {
crate::spp::clock_systems(&solution.used_sats)
};
if clock_systems.is_empty() {
return Err(AraimError::InsufficientGeometry);
}
for (idx, system) in clock_systems.iter().enumerate() {
if clock_systems[..idx].contains(system) {
return Err(AraimError::InsufficientGeometry);
}
}
Ok(clock_systems)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct IntegrityAllocation {
pub phmi_total: f64,
pub phmi_vert: f64,
pub phmi_hor: f64,
pub pfa_vert: f64,
pub pfa_hor: f64,
pub p_threshold_unmonitored: f64,
pub p_emt: f64,
pub max_fault_order: usize,
}
impl IntegrityAllocation {
pub const fn lpv_200() -> Self {
Self {
phmi_total: 1.0e-7,
phmi_vert: 9.8e-8,
phmi_hor: 2.0e-9,
pfa_vert: 3.9e-6,
pfa_hor: 9.0e-8,
p_threshold_unmonitored: 8.0e-8,
p_emt: 1.0e-5,
max_fault_order: 2,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AraimError {
#[error("insufficient ARAIM geometry")]
InsufficientGeometry,
#[error("unmonitorable ARAIM fault probability exceeds allocation")]
UnmonitorableFaultMass,
#[error("ARAIM numerical failure")]
NumericalFailure,
#[error("invalid ARAIM ISM")]
InvalidIsm,
#[error("invalid ARAIM allocation")]
InvalidAllocation,
}
pub(crate) fn clock_system_for_row(system: GnssSystem) -> GnssSystem {
match system {
GnssSystem::Sbas => GnssSystem::Gps,
other => other,
}
}
pub(crate) fn validate_probability(value: f64, allow_zero: bool) -> bool {
value.is_finite()
&& if allow_zero {
(0.0..1.0).contains(&value) || value == 0.0
} else {
(0.0..1.0).contains(&value)
}
}
pub(crate) fn validate_nonneg_finite(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
pub(crate) fn validate_positive_finite(value: f64) -> bool {
value.is_finite() && value > 0.0
}