1use std::cmp::Ordering;
2
3use super::{AraimError, AraimGeometry, IntegrityAllocation};
4use crate::id::{GnssSatelliteId, GnssSystem};
5
6use super::ism::Ism;
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct FaultHypothesis {
11 pub excluded: Vec<GnssSatelliteId>,
13 pub excluded_constellation: Option<GnssSystem>,
15 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
39pub 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 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}