1use std::collections::BTreeSet;
2
3use super::fault_modes::{enumerate_fault_modes_checked, FaultHypothesis};
4use super::ism::Ism;
5use super::protection::{
6 gain_matrix_enu, gain_matrix_enu_for_clock_systems, k_false_alert, metric_bias, metric_sigma,
7 separation_sigma, solve_protection_level, FalseAlertAxis, ProtectionEquationTerm,
8 ProtectionLevelSolution,
9};
10use super::{
11 clock_system_for_row, validate_probability, AraimError, AraimGeometry, IntegrityAllocation,
12};
13use crate::id::{GnssSatelliteId, GnssSystem};
14
15const HORIZONTAL_PL_TOL_M: f64 = 5.0e-2;
17const VERTICAL_PL_TOL_M: f64 = 1.0e-4;
18
19#[derive(Debug, Clone, PartialEq)]
21pub struct FaultMode {
22 pub excluded: Vec<GnssSatelliteId>,
24 pub excluded_constellation: Option<GnssSystem>,
26 pub prior: f64,
28 pub sigma_int_enu_m: [f64; 3],
30 pub bias_enu_m: [f64; 3],
32 pub threshold_enu_m: [f64; 3],
34 pub monitorable: bool,
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub struct AraimResult {
41 pub hpl_m: f64,
43 pub vpl_m: f64,
45 pub sigma_acc_h_m: f64,
47 pub sigma_acc_v_m: f64,
49 pub emt_m: f64,
51 pub fault_modes: Vec<FaultMode>,
53 pub p_unmonitored: f64,
55 pub availability: bool,
57}
58
59pub fn araim(
61 geometry: &AraimGeometry,
62 ism: &Ism,
63 allocation: &IntegrityAllocation,
64) -> Result<AraimResult, AraimError> {
65 validate_geometry(geometry)?;
66 validate_allocation(allocation)?;
67 ism.validate()?;
68
69 let effective = geometry
70 .rows
71 .iter()
72 .map(|row| ism.effective_for(row))
73 .collect::<Result<Vec<_>, _>>()?;
74 let sigma_int_m: Vec<f64> = effective.iter().map(|model| model.sigma_int_m).collect();
75 let sigma_acc_m: Vec<f64> = effective.iter().map(|model| model.sigma_acc_m).collect();
76 let bias_m: Vec<f64> = effective.iter().map(|model| model.b_nom_m).collect();
77 let weights_int: Vec<f64> = sigma_int_m
78 .iter()
79 .map(|sigma| 1.0 / (sigma * sigma))
80 .collect();
81
82 let enumeration = enumerate_fault_modes_checked(geometry, ism, allocation)?;
83 let n_fault_modes = enumeration.modes.len().saturating_sub(1);
84 let k_h = k_false_alert(
85 allocation.pfa_hor,
86 n_fault_modes,
87 FalseAlertAxis::Horizontal,
88 )?;
89 let k_v = k_false_alert(allocation.pfa_vert, n_fault_modes, FalseAlertAxis::Vertical)?;
90
91 let fault_free_int = gain_matrix_enu(geometry, &weights_int)?;
92 let sigma_acc_e = metric_sigma(&fault_free_int.enu_rows[0], &sigma_acc_m);
93 let sigma_acc_n = metric_sigma(&fault_free_int.enu_rows[1], &sigma_acc_m);
94 let sigma_acc_u = metric_sigma(&fault_free_int.enu_rows[2], &sigma_acc_m);
95
96 let mut p_unmonitored = enumeration.p_unenumerated;
97 let mut fault_modes = Vec::with_capacity(enumeration.modes.len());
98 for (mode_idx, hypothesis) in enumeration.modes.iter().enumerate() {
99 let mode = if mode_idx == 0 {
100 compute_monitorable_mode(
101 hypothesis,
102 MonitorInputs {
103 gain_int: &fault_free_int,
104 fault_free_int: &fault_free_int,
105 sigma_int_m: &sigma_int_m,
106 sigma_acc_m: &sigma_acc_m,
107 bias_m: &bias_m,
108 k: [0.0, 0.0, 0.0],
109 },
110 )
111 } else {
112 let weights_int_k = zeroed_weights(geometry, &weights_int, hypothesis);
113 let clock_systems_k = active_clock_systems(geometry, hypothesis);
114 match gain_matrix_enu_for_clock_systems(geometry, &weights_int_k, &clock_systems_k) {
115 Ok(gain_int) => compute_monitorable_mode(
116 hypothesis,
117 MonitorInputs {
118 gain_int: &gain_int,
119 fault_free_int: &fault_free_int,
120 sigma_int_m: &sigma_int_m,
121 sigma_acc_m: &sigma_acc_m,
122 bias_m: &bias_m,
123 k: [k_h, k_h, k_v],
124 },
125 ),
126 _ => {
127 p_unmonitored += hypothesis.prior;
128 unmonitorable_mode(hypothesis)
129 }
130 }
131 };
132 fault_modes.push(mode);
133 }
134
135 if p_unmonitored > allocation.p_threshold_unmonitored {
136 return Err(AraimError::UnmonitorableFaultMass);
137 }
138
139 let budget_scale = integrity_budget_scale(allocation, p_unmonitored)?;
140 let pl_e = solve_coord_pl(
141 &fault_modes,
142 0,
143 0.5 * allocation.phmi_hor * budget_scale,
144 HORIZONTAL_PL_TOL_M,
145 )?;
146 let pl_n = solve_coord_pl(
147 &fault_modes,
148 1,
149 0.5 * allocation.phmi_hor * budget_scale,
150 HORIZONTAL_PL_TOL_M,
151 )?;
152 let pl_u = solve_coord_pl(
153 &fault_modes,
154 2,
155 allocation.phmi_vert * budget_scale,
156 VERTICAL_PL_TOL_M,
157 )?;
158 let roots_converged = pl_e.converged && pl_n.converged && pl_u.converged;
159 let emt_m = fault_modes
160 .iter()
161 .filter(|mode| mode.monitorable)
162 .filter(|mode| mode.prior >= allocation.p_emt)
163 .map(|mode| mode.threshold_enu_m[2])
164 .fold(0.0_f64, f64::max);
165 let fault_free_full_rank = fault_modes
166 .first()
167 .map(|mode| mode.monitorable)
168 .unwrap_or(false);
169
170 Ok(AraimResult {
171 hpl_m: (pl_e.value_m * pl_e.value_m + pl_n.value_m * pl_n.value_m).sqrt(),
172 vpl_m: pl_u.value_m,
173 sigma_acc_h_m: (sigma_acc_e * sigma_acc_e + sigma_acc_n * sigma_acc_n).sqrt(),
174 sigma_acc_v_m: sigma_acc_u,
175 emt_m,
176 fault_modes,
177 p_unmonitored,
178 availability: fault_free_full_rank && roots_converged,
179 })
180}
181
182struct MonitorInputs<'a> {
183 gain_int: &'a super::protection::GainMatrix,
184 fault_free_int: &'a super::protection::GainMatrix,
185 sigma_int_m: &'a [f64],
186 sigma_acc_m: &'a [f64],
187 bias_m: &'a [f64],
188 k: [f64; 3],
189}
190
191fn compute_monitorable_mode(hypothesis: &FaultHypothesis, inputs: MonitorInputs<'_>) -> FaultMode {
192 let mut sigma_int_enu_m = [0.0_f64; 3];
193 let mut bias_enu_m = [0.0_f64; 3];
194 let mut threshold_enu_m = [0.0_f64; 3];
195 for coord in 0..3 {
196 sigma_int_enu_m[coord] = metric_sigma(&inputs.gain_int.enu_rows[coord], inputs.sigma_int_m);
197 bias_enu_m[coord] = metric_bias(&inputs.gain_int.enu_rows[coord], inputs.bias_m);
198 threshold_enu_m[coord] = inputs.k[coord]
199 * separation_sigma(
200 &inputs.gain_int.enu_rows[coord],
201 &inputs.fault_free_int.enu_rows[coord],
202 inputs.sigma_acc_m,
203 );
204 }
205
206 FaultMode {
207 excluded: hypothesis.excluded.clone(),
208 excluded_constellation: hypothesis.excluded_constellation,
209 prior: hypothesis.prior,
210 sigma_int_enu_m,
211 bias_enu_m,
212 threshold_enu_m,
213 monitorable: true,
214 }
215}
216
217fn unmonitorable_mode(hypothesis: &FaultHypothesis) -> FaultMode {
218 FaultMode {
219 excluded: hypothesis.excluded.clone(),
220 excluded_constellation: hypothesis.excluded_constellation,
221 prior: hypothesis.prior,
222 sigma_int_enu_m: [f64::INFINITY; 3],
223 bias_enu_m: [f64::INFINITY; 3],
224 threshold_enu_m: [f64::INFINITY; 3],
225 monitorable: false,
226 }
227}
228
229fn zeroed_weights(
230 geometry: &AraimGeometry,
231 weights: &[f64],
232 hypothesis: &FaultHypothesis,
233) -> Vec<f64> {
234 geometry
235 .rows
236 .iter()
237 .zip(weights)
238 .map(|(row, &weight)| {
239 if hypothesis.excludes_satellite(row.id, row.system) {
240 0.0
241 } else {
242 weight
243 }
244 })
245 .collect()
246}
247
248fn active_clock_systems(geometry: &AraimGeometry, hypothesis: &FaultHypothesis) -> Vec<GnssSystem> {
249 geometry
250 .clock_systems
251 .iter()
252 .copied()
253 .filter(|&clock_system| {
254 geometry.rows.iter().any(|row| {
255 !hypothesis.excludes_satellite(row.id, row.system)
256 && clock_system_for_row(row.system) == clock_system
257 })
258 })
259 .collect()
260}
261
262fn solve_coord_pl(
263 modes: &[FaultMode],
264 coord: usize,
265 integrity_target: f64,
266 tolerance_m: f64,
267) -> Result<ProtectionLevelSolution, AraimError> {
268 let fault_free = modes.first().ok_or(AraimError::NumericalFailure)?;
269 if !fault_free.monitorable {
270 return Err(AraimError::InsufficientGeometry);
271 }
272 let fault_free_term = ProtectionEquationTerm {
273 prior: 1.0,
274 sigma_m: fault_free.sigma_int_enu_m[coord],
275 bias_m: fault_free.bias_enu_m[coord],
276 threshold_m: 0.0,
277 };
278 let fault_terms: Vec<ProtectionEquationTerm> = modes
279 .iter()
280 .skip(1)
281 .filter(|mode| mode.monitorable)
282 .map(|mode| ProtectionEquationTerm {
283 prior: mode.prior,
284 sigma_m: mode.sigma_int_enu_m[coord],
285 bias_m: mode.bias_enu_m[coord],
286 threshold_m: mode.threshold_enu_m[coord],
287 })
288 .collect();
289 solve_protection_level(fault_free_term, &fault_terms, integrity_target, tolerance_m)
290}
291
292fn integrity_budget_scale(
293 allocation: &IntegrityAllocation,
294 p_unmonitored: f64,
295) -> Result<f64, AraimError> {
296 let phmi_split = allocation.phmi_vert + allocation.phmi_hor;
297 let scale = 1.0 - p_unmonitored / phmi_split;
298 if scale > 0.0 && scale.is_finite() {
299 Ok(scale)
300 } else {
301 Err(AraimError::UnmonitorableFaultMass)
302 }
303}
304
305fn validate_geometry(geometry: &AraimGeometry) -> Result<(), AraimError> {
306 if geometry.clock_systems.is_empty() {
307 return Err(AraimError::InsufficientGeometry);
308 }
309 let n_state = 3 + geometry.clock_systems.len();
310 if geometry.rows.len() < n_state {
311 return Err(AraimError::InsufficientGeometry);
312 }
313 let mut clock_systems = BTreeSet::new();
314 for &system in &geometry.clock_systems {
315 if !clock_systems.insert(system) {
316 return Err(AraimError::InsufficientGeometry);
317 }
318 }
319
320 let mut ids = BTreeSet::new();
321 for row in &geometry.rows {
322 if row.id.system != row.system || !ids.insert(row.id) {
323 return Err(AraimError::InsufficientGeometry);
324 }
325 if !(-core::f64::consts::FRAC_PI_2..=core::f64::consts::FRAC_PI_2)
326 .contains(&row.elevation_rad)
327 {
328 return Err(AraimError::InsufficientGeometry);
329 }
330 let los = row.line_of_sight;
331 let norm = (los.e_x * los.e_x + los.e_y * los.e_y + los.e_z * los.e_z).sqrt();
332 if !norm.is_finite() || (norm - 1.0).abs() > 1.0e-3 {
333 return Err(AraimError::InsufficientGeometry);
334 }
335 }
336 Ok(())
337}
338
339fn validate_allocation(allocation: &IntegrityAllocation) -> Result<(), AraimError> {
340 let phmi_split = allocation.phmi_vert + allocation.phmi_hor;
341 let phmi_split_tolerance = allocation.phmi_total * 16.0 * f64::EPSILON;
342 let valid = validate_probability(allocation.phmi_total, false)
343 && validate_probability(allocation.phmi_vert, false)
344 && validate_probability(allocation.phmi_hor, false)
345 && validate_probability(allocation.pfa_vert, false)
346 && validate_probability(allocation.pfa_hor, false)
347 && validate_probability(allocation.p_threshold_unmonitored, true)
348 && validate_probability(allocation.p_emt, false)
349 && phmi_split <= allocation.phmi_total + phmi_split_tolerance;
350 if valid {
351 Ok(())
352 } else {
353 Err(AraimError::InvalidAllocation)
354 }
355}