Skip to main content

sidereon_core/araim/
fault_modes.rs

1use std::cmp::Ordering;
2
3use super::{AraimError, AraimGeometry, IntegrityAllocation};
4use crate::id::{GnssSatelliteId, GnssSystem};
5
6use super::ism::Ism;
7
8/// One ARAIM fault hypothesis.
9#[derive(Debug, Clone, PartialEq)]
10pub struct FaultHypothesis {
11    /// Satellites excluded by this hypothesis.
12    pub excluded: Vec<GnssSatelliteId>,
13    /// Constellation excluded by this hypothesis, if any.
14    pub excluded_constellation: Option<GnssSystem>,
15    /// Prior probability mass for this hypothesis.
16    pub prior: f64,
17}
18
19impl FaultHypothesis {
20    pub(crate) fn fault_free() -> Self {
21        Self {
22            excluded: Vec::new(),
23            excluded_constellation: None,
24            prior: 1.0,
25        }
26    }
27
28    pub(crate) fn excludes_satellite(&self, id: GnssSatelliteId, system: GnssSystem) -> bool {
29        self.excluded.contains(&id) || self.excluded_constellation == Some(system)
30    }
31}
32
33#[derive(Debug, Clone, PartialEq)]
34pub(crate) struct FaultEnumeration {
35    pub modes: Vec<FaultHypothesis>,
36    pub p_unenumerated: f64,
37}
38
39/// Enumerate ARAIM fault modes in the order used by the MHSS solve.
40pub fn enumerate_fault_modes(
41    geometry: &AraimGeometry,
42    ism: &Ism,
43    allocation: &IntegrityAllocation,
44) -> Result<Vec<FaultHypothesis>, AraimError> {
45    enumerate_fault_modes_checked(geometry, ism, allocation).map(|enumeration| enumeration.modes)
46}
47
48pub(crate) fn enumerate_fault_modes_checked(
49    geometry: &AraimGeometry,
50    ism: &Ism,
51    allocation: &IntegrityAllocation,
52) -> Result<FaultEnumeration, AraimError> {
53    ism.validate()?;
54    let mut modes = vec![FaultHypothesis::fault_free()];
55    let mut p_unenumerated = 0.0;
56
57    let mut sat_priors: Vec<(GnssSatelliteId, f64)> = Vec::with_capacity(geometry.rows.len());
58    for row in &geometry.rows {
59        let model = ism.effective_for(row)?;
60        sat_priors.push((row.id, model.p_sat));
61    }
62    let satellite_order_mass = satellite_order_masses(&sat_priors);
63    let constellation_priors = constellation_priors(geometry, ism)?;
64    let constellation_involving_order_mass =
65        constellation_involving_order_masses(&satellite_order_mass, &constellation_priors);
66
67    if allocation.max_fault_order >= 1 {
68        for &(id, prior) in &sat_priors {
69            if prior > 0.0 {
70                modes.push(FaultHypothesis {
71                    excluded: vec![id],
72                    excluded_constellation: None,
73                    prior,
74                });
75            }
76        }
77
78        for &(system, prior) in &constellation_priors {
79            if prior > 0.0 {
80                modes.push(FaultHypothesis {
81                    excluded: Vec::new(),
82                    excluded_constellation: Some(system),
83                    prior,
84                });
85            }
86        }
87    } else {
88        for &(_, prior) in &sat_priors {
89            p_unenumerated += prior;
90        }
91        for &(_, prior) in &constellation_priors {
92            p_unenumerated += prior;
93        }
94    }
95
96    let max_sat_order = sat_priors.len();
97    let max_enumerated_sat_order = allocation.max_fault_order.min(max_sat_order);
98    if max_enumerated_sat_order >= 2 {
99        let mut candidates = Vec::new();
100        for order in 2..=max_enumerated_sat_order {
101            collect_satellite_combinations(
102                &sat_priors,
103                order,
104                0,
105                &mut Vec::with_capacity(order),
106                1.0,
107                &mut candidates,
108            );
109        }
110        candidates.sort_by(compare_candidates);
111
112        let mut remaining: f64 = candidates.iter().map(|candidate| candidate.prior).sum();
113        for candidate in candidates {
114            if remaining < allocation.p_threshold_unmonitored {
115                break;
116            }
117            remaining -= candidate.prior;
118            modes.push(FaultHypothesis {
119                excluded: candidate.ids,
120                excluded_constellation: None,
121                prior: candidate.prior,
122            });
123        }
124        p_unenumerated += remaining;
125        p_unenumerated += satellite_order_mass
126            .iter()
127            .skip(max_enumerated_sat_order + 1)
128            .sum::<f64>();
129    } else {
130        p_unenumerated += satellite_order_mass.iter().skip(2).sum::<f64>();
131    }
132    // WG-C Reference ADD v3.0 Section 4.6 treats constellation faults as
133    // independent primary events, so multi-event tail mass must include any
134    // combination that contains at least one constellation event.
135    p_unenumerated += constellation_involving_order_mass
136        .iter()
137        .skip(2)
138        .sum::<f64>();
139
140    let monitored_fault_mass: f64 = modes.iter().skip(1).map(|mode| mode.prior).sum();
141    let total_fault_mass = monitored_fault_mass + p_unenumerated;
142    if !total_fault_mass.is_finite() || total_fault_mass > 1.0 + 1.0e-12 {
143        return Err(AraimError::InvalidIsm);
144    }
145    modes[0].prior = (1.0 - total_fault_mass).max(0.0);
146
147    debug_assert!(p_unenumerated >= 0.0);
148    debug_assert!((monitored_fault_mass + p_unenumerated + modes[0].prior - 1.0).abs() <= 1.0e-10);
149    Ok(FaultEnumeration {
150        modes,
151        p_unenumerated,
152    })
153}
154
155#[derive(Debug, Clone, PartialEq)]
156struct Candidate {
157    ids: Vec<GnssSatelliteId>,
158    prior: f64,
159}
160
161fn satellite_order_masses(sat_priors: &[(GnssSatelliteId, f64)]) -> Vec<f64> {
162    let mut masses = vec![0.0; sat_priors.len() + 1];
163    masses[0] = 1.0;
164    for (seen, &(_, prior)) in sat_priors.iter().enumerate() {
165        for order in (1..=seen + 1).rev() {
166            masses[order] += masses[order - 1] * prior;
167        }
168    }
169    masses
170}
171
172fn constellation_priors(
173    geometry: &AraimGeometry,
174    ism: &Ism,
175) -> Result<Vec<(GnssSystem, f64)>, AraimError> {
176    let mut systems: Vec<GnssSystem> = geometry.rows.iter().map(|row| row.system).collect();
177    systems.sort_unstable();
178    systems.dedup();
179    systems
180        .into_iter()
181        .map(|system| {
182            ism.constellation(system)
183                .map(|constellation| (system, constellation.p_const))
184                .ok_or(AraimError::InvalidIsm)
185        })
186        .collect()
187}
188
189fn constellation_involving_order_masses(
190    satellite_order_mass: &[f64],
191    constellation_priors: &[(GnssSystem, f64)],
192) -> Vec<f64> {
193    let constellation_order_mass =
194        order_masses(constellation_priors.iter().map(|&(_, prior)| prior));
195    let mut masses = vec![0.0; satellite_order_mass.len() + constellation_order_mass.len() - 1];
196    for (sat_order, &sat_mass) in satellite_order_mass.iter().enumerate() {
197        for (const_order, &const_mass) in constellation_order_mass.iter().enumerate().skip(1) {
198            masses[sat_order + const_order] += sat_mass * const_mass;
199        }
200    }
201    masses
202}
203
204fn order_masses(priors: impl IntoIterator<Item = f64>) -> Vec<f64> {
205    let mut masses = vec![1.0];
206    for prior in priors {
207        masses.push(0.0);
208        for order in (1..masses.len()).rev() {
209            masses[order] += masses[order - 1] * prior;
210        }
211    }
212    masses
213}
214
215fn collect_satellite_combinations(
216    sat_priors: &[(GnssSatelliteId, f64)],
217    order: usize,
218    start: usize,
219    selected: &mut Vec<GnssSatelliteId>,
220    prior: f64,
221    out: &mut Vec<Candidate>,
222) {
223    if selected.len() == order {
224        if prior > 0.0 {
225            out.push(Candidate {
226                ids: selected.clone(),
227                prior,
228            });
229        }
230        return;
231    }
232
233    let remaining_slots = order - selected.len();
234    let max_start = sat_priors.len().saturating_sub(remaining_slots);
235    for idx in start..=max_start {
236        let (id, p_sat) = sat_priors[idx];
237        selected.push(id);
238        collect_satellite_combinations(sat_priors, order, idx + 1, selected, prior * p_sat, out);
239        selected.pop();
240    }
241}
242
243fn compare_candidates(a: &Candidate, b: &Candidate) -> Ordering {
244    b.prior
245        .partial_cmp(&a.prior)
246        .unwrap_or(Ordering::Equal)
247        .then_with(|| a.ids.cmp(&b.ids))
248}