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