1use crate::araim::ism::Ism;
9use crate::araim::protection::{design_row, gain_matrix_enu, ProtectionModel};
10use crate::astro::math::linear::{invert_symmetric_pd, normal_equations_weighted};
11use crate::astro::math::special::normal_q_inv;
12use crate::quality::{QualityError, DEFAULT_P_FA};
13use crate::validate;
14
15use super::{AraimError, AraimGeometry};
16
17const DEFAULT_BETA: f64 = 0.20;
18const DEFAULT_MIN_REDUNDANCY: f64 = 1.0e-9;
19const REDUNDANCY_TOL: f64 = 1.0e-10;
20
21#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
23pub struct RangeReliabilityRow {
24 pub id: String,
26 pub design_row: Vec<f64>,
28 pub sigma_m: f64,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
34pub struct ReliabilityOptions {
35 pub alpha: f64,
37 pub beta: f64,
39 pub lambda0_override: Option<f64>,
41 pub min_redundancy: f64,
43}
44
45impl Default for ReliabilityOptions {
46 fn default() -> Self {
47 Self {
48 alpha: DEFAULT_P_FA,
49 beta: DEFAULT_BETA,
50 lambda0_override: None,
51 min_redundancy: DEFAULT_MIN_REDUNDANCY,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
58pub struct ObservationReliability {
59 pub id: String,
61 pub redundancy: f64,
63 pub mdb_m: Option<f64>,
65 pub external_enu_m: Option<[f64; 3]>,
71 pub bias_to_noise: Option<f64>,
73 pub uncheckable: bool,
75}
76
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
79pub struct ReliabilitySummary {
80 pub n_obs: usize,
82 pub n_params: usize,
84 pub dof: usize,
86 pub sum_redundancy: f64,
88 pub lambda0: f64,
90 pub max_mdb_m: Option<(String, f64)>,
92 pub min_redundancy: (String, f64),
94 pub n_uncheckable: usize,
96}
97
98#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
100pub struct ReliabilityReport {
101 pub per_observation: Vec<ObservationReliability>,
103 pub summary: ReliabilitySummary,
105}
106
107pub fn wtest_noncentrality(alpha: f64, beta: f64) -> Result<f64, QualityError> {
113 Ok(wtest_noncentrality_components(alpha, beta)?.lambda0)
114}
115
116#[derive(Debug, Clone, Copy, PartialEq)]
121pub struct WtestNoncentralityComponents {
122 pub delta0: f64,
124 pub lambda0: f64,
126}
127
128pub fn wtest_noncentrality_components(
131 alpha: f64,
132 beta: f64,
133) -> Result<WtestNoncentralityComponents, QualityError> {
134 validate_probability(alpha)?;
135 if !(0.0..1.0).contains(&beta) || !beta.is_finite() {
136 return Err(QualityError::InvalidReliabilityParameter);
137 }
138 let k_alpha = normal_q_inv(0.5 * alpha).ok_or(QualityError::InvalidProbability)?;
139 let k_beta = normal_q_inv(beta).ok_or(QualityError::InvalidReliabilityParameter)?;
140 let delta0 = k_alpha + k_beta;
141 let lambda0 = delta0 * delta0;
142 if lambda0.is_finite() && lambda0 > 0.0 {
143 Ok(WtestNoncentralityComponents { delta0, lambda0 })
144 } else {
145 Err(QualityError::InvalidReliabilityParameter)
146 }
147}
148
149pub fn reliability_design(
154 rows: &[RangeReliabilityRow],
155 options: &ReliabilityOptions,
156) -> Result<ReliabilityReport, QualityError> {
157 let n_params = validate_reliability_rows(rows)?;
158 let lambda0 = reliability_lambda0(options)?;
159 let q_xx = design_covariance(rows, n_params)?;
160
161 finish_reliability_report(
162 rows,
163 n_params,
164 lambda0,
165 options.min_redundancy,
166 |idx, mdb_m| state_external(rows, &q_xx, idx, mdb_m),
167 )
168}
169
170pub fn reliability_araim(
175 geometry: &AraimGeometry,
176 model: &dyn ProtectionModel,
177 options: &ReliabilityOptions,
178) -> Result<ReliabilityReport, AraimError> {
179 let rows = araim_rows(geometry, model)?;
180 let lambda0 = reliability_lambda0(options).map_err(map_quality_error)?;
181 let weights = rows
182 .iter()
183 .map(|row| 1.0 / (row.sigma_m * row.sigma_m))
184 .collect::<Vec<_>>();
185 let gain = gain_matrix_enu(geometry, &weights)?;
186 let n_params = geometry_state_len(geometry)?;
187
188 finish_reliability_report(
189 &rows,
190 n_params,
191 lambda0,
192 options.min_redundancy,
193 |idx, mdb_m| {
194 Some([
195 gain.enu_rows[0][idx] * mdb_m,
196 gain.enu_rows[1][idx] * mdb_m,
197 gain.enu_rows[2][idx] * mdb_m,
198 ])
199 },
200 )
201 .map_err(map_quality_error)
202}
203
204impl ProtectionModel for Ism {
205 fn sigma_int_m(&self, row: &super::AraimRow) -> Result<f64, AraimError> {
206 Ok(self.effective_for(row)?.sigma_int_m)
207 }
208
209 fn sigma_acc_m(&self, row: &super::AraimRow) -> Result<f64, AraimError> {
210 Ok(self.effective_for(row)?.sigma_acc_m)
211 }
212}
213
214fn araim_rows(
215 geometry: &AraimGeometry,
216 model: &dyn ProtectionModel,
217) -> Result<Vec<RangeReliabilityRow>, AraimError> {
218 let n_params = geometry_state_len(geometry)?;
219 if geometry.rows.len() < n_params {
220 return Err(AraimError::InsufficientGeometry);
221 }
222 let mut rows = Vec::with_capacity(geometry.rows.len());
223 for row in &geometry.rows {
224 let sigma_int_m = model.sigma_int_m(row)?;
225 let sigma_acc_m = model.sigma_acc_m(row)?;
226 if !valid_positive_finite(sigma_int_m) || !valid_positive_finite(sigma_acc_m) {
227 return Err(AraimError::InvalidIsm);
228 }
229 rows.push(RangeReliabilityRow {
230 id: row.id.to_string(),
231 design_row: design_row(&geometry.clock_systems, row)?,
232 sigma_m: sigma_int_m,
233 });
234 }
235 Ok(rows)
236}
237
238fn reliability_lambda0(options: &ReliabilityOptions) -> Result<f64, QualityError> {
239 validate_reliability_options(options)?;
240 match options.lambda0_override {
241 Some(lambda0) => Ok(lambda0),
242 None => wtest_noncentrality(options.alpha, options.beta),
243 }
244}
245
246fn validate_reliability_options(options: &ReliabilityOptions) -> Result<(), QualityError> {
247 validate_probability(options.alpha)?;
248 if !(0.0..1.0).contains(&options.beta) || !options.beta.is_finite() {
249 return Err(QualityError::InvalidReliabilityParameter);
250 }
251 if let Some(lambda0) = options.lambda0_override {
252 if !lambda0.is_finite() || lambda0 <= 0.0 {
253 return Err(QualityError::InvalidReliabilityParameter);
254 }
255 }
256 if !options.min_redundancy.is_finite() || options.min_redundancy <= 0.0 {
257 return Err(QualityError::InvalidReliabilityParameter);
258 }
259 Ok(())
260}
261
262fn validate_reliability_rows(rows: &[RangeReliabilityRow]) -> Result<usize, QualityError> {
263 let first = rows.first().ok_or(QualityError::InvalidDesign)?;
264 let n_params = first.design_row.len();
265 if n_params == 0 || rows.len() < n_params {
266 return Err(QualityError::InvalidDesign);
267 }
268 for row in rows {
269 if row.design_row.len() != n_params {
270 return Err(QualityError::InvalidDesign);
271 }
272 validate::finite_slice(&row.design_row, "reliability design row")
273 .map_err(|_| QualityError::InvalidDesign)?;
274 validate::finite_positive(row.sigma_m, "reliability sigma")
275 .map_err(|_| QualityError::InvalidWeight)?;
276 let weight = 1.0 / (row.sigma_m * row.sigma_m);
277 if !weight.is_finite() || weight <= 0.0 {
278 return Err(QualityError::InvalidWeight);
279 }
280 }
281 Ok(n_params)
282}
283
284fn design_covariance(
285 rows: &[RangeReliabilityRow],
286 n_params: usize,
287) -> Result<Vec<Vec<f64>>, QualityError> {
288 let (normal, _) = normal_equations_weighted(
289 rows.iter()
290 .map(|row| (row.design_row.as_slice(), 0.0, 1.0 / row.sigma_m)),
291 n_params,
292 )
293 .ok_or(QualityError::InvalidDesign)?;
294 invert_symmetric_pd(&normal).ok_or(QualityError::SingularGeometry)
295}
296
297fn finish_reliability_report<F>(
298 rows: &[RangeReliabilityRow],
299 n_params: usize,
300 lambda0: f64,
301 min_redundancy: f64,
302 mut external: F,
303) -> Result<ReliabilityReport, QualityError>
304where
305 F: FnMut(usize, f64) -> Option<[f64; 3]>,
306{
307 let q_xx = design_covariance(rows, n_params)?;
308 let mut per_observation = Vec::with_capacity(rows.len());
309 let mut sum_redundancy = 0.0;
310 let mut max_mdb_m: Option<(String, f64)> = None;
311 let mut min_pair: Option<(String, f64)> = None;
312 let mut n_uncheckable = 0usize;
313
314 for (idx, row) in rows.iter().enumerate() {
315 let redundancy = redundancy_number(row, &q_xx)?;
316 sum_redundancy += redundancy;
317 update_min_redundancy(&mut min_pair, &row.id, redundancy);
318
319 let uncheckable = redundancy < min_redundancy;
320 let (mdb_m, external_enu_m, bias_to_noise) = if uncheckable {
321 n_uncheckable += 1;
322 (None, None, None)
323 } else {
324 let mdb = row.sigma_m * (lambda0 / redundancy).sqrt();
325 let bnr = (lambda0 * (1.0 - redundancy) / redundancy).sqrt();
326 update_max_mdb(&mut max_mdb_m, &row.id, mdb);
327 (Some(mdb), external(idx, mdb), Some(bnr))
328 };
329
330 per_observation.push(ObservationReliability {
331 id: row.id.clone(),
332 redundancy,
333 mdb_m,
334 external_enu_m,
335 bias_to_noise,
336 uncheckable,
337 });
338 }
339
340 Ok(ReliabilityReport {
341 per_observation,
342 summary: ReliabilitySummary {
343 n_obs: rows.len(),
344 n_params,
345 dof: rows.len().saturating_sub(n_params),
346 sum_redundancy,
347 lambda0,
348 max_mdb_m,
349 min_redundancy: min_pair.unwrap_or_else(|| (String::new(), 0.0)),
350 n_uncheckable,
351 },
352 })
353}
354
355fn redundancy_number(row: &RangeReliabilityRow, q_xx: &[Vec<f64>]) -> Result<f64, QualityError> {
356 let qh = mat_vec(q_xx, &row.design_row);
357 let leverage = dot(&row.design_row, &qh) / (row.sigma_m * row.sigma_m);
358 if !leverage.is_finite() {
359 return Err(QualityError::SingularGeometry);
360 }
361 let raw = 1.0 - leverage;
362 if raw >= 0.0 {
363 Ok(raw.min(1.0))
364 } else if raw.abs() <= REDUNDANCY_TOL {
365 Ok(0.0)
366 } else {
367 Err(QualityError::SingularGeometry)
368 }
369}
370
371fn state_external(
372 rows: &[RangeReliabilityRow],
373 q_xx: &[Vec<f64>],
374 idx: usize,
375 mdb_m: f64,
376) -> Option<[f64; 3]> {
377 if q_xx.len() < 3 {
378 return None;
379 }
380 let row = &rows[idx];
381 let scale = mdb_m / (row.sigma_m * row.sigma_m);
382 let qh = mat_vec(q_xx, &row.design_row);
383 Some([qh[0] * scale, qh[1] * scale, qh[2] * scale])
384}
385
386fn update_min_redundancy(pair: &mut Option<(String, f64)>, id: &str, value: f64) {
387 if match pair.as_ref() {
388 Some((_, current)) => value < *current,
389 None => true,
390 } {
391 *pair = Some((id.to_owned(), value));
392 }
393}
394
395fn update_max_mdb(pair: &mut Option<(String, f64)>, id: &str, value: f64) {
396 if match pair.as_ref() {
397 Some((_, current)) => value > *current,
398 None => true,
399 } {
400 *pair = Some((id.to_owned(), value));
401 }
402}
403
404fn geometry_state_len(geometry: &AraimGeometry) -> Result<usize, AraimError> {
405 if geometry.clock_systems.is_empty() {
406 return Err(AraimError::InsufficientGeometry);
407 }
408 Ok(3 + geometry.clock_systems.len())
409}
410
411fn map_quality_error(err: QualityError) -> AraimError {
412 match err {
413 QualityError::InvalidProbability | QualityError::InvalidReliabilityParameter => {
414 AraimError::InvalidAllocation
415 }
416 QualityError::InvalidWeight => AraimError::InvalidIsm,
417 QualityError::InvalidDesign | QualityError::SingularGeometry => {
418 AraimError::InsufficientGeometry
419 }
420 _ => AraimError::NumericalFailure,
421 }
422}
423
424fn validate_probability(value: f64) -> Result<(), QualityError> {
425 if (0.0..1.0).contains(&value) && value.is_finite() {
426 Ok(())
427 } else {
428 Err(QualityError::InvalidProbability)
429 }
430}
431
432fn valid_positive_finite(value: f64) -> bool {
433 value.is_finite() && value > 0.0
434}
435
436fn mat_vec(a: &[Vec<f64>], x: &[f64]) -> Vec<f64> {
437 a.iter().map(|row| dot(row, x)).collect()
438}
439
440fn dot(a: &[f64], b: &[f64]) -> f64 {
441 a.iter().zip(b).map(|(x, y)| x * y).sum()
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447 use crate::araim::protection::gain_matrix_enu;
448 use crate::araim::{AraimGeometry, AraimRow};
449 use crate::dop::{ecef_to_enu_rotation, LineOfSight};
450 use crate::frame::Wgs84Geodetic;
451 use crate::id::{GnssSatelliteId, GnssSystem};
452
453 const LAMBDA_BAARDA_4DP: f64 = 17.075;
454
455 #[test]
456 fn algebraic_identities_hold_for_weighted_design() {
457 let rows = algebraic_rows();
458 let options = options_with_lambda(LAMBDA_BAARDA_4DP);
459 let report = reliability_design(&rows, &options).expect("reliability report");
460 let n = rows.len();
461 let u = rows[0].design_row.len();
462 let q_xx = design_covariance(&rows, u).expect("covariance");
463 let r = residual_projector(&rows, &q_xx);
464 let h = design_matrix(&rows);
465 let w = weight_matrix(&rows);
466
467 assert_close(report.summary.sum_redundancy, (n - u) as f64, 2.0e-14);
468 assert_close(trace(&r), (n - u) as f64, 2.0e-14);
469 assert!(inf_norm(&mat_sub(&mat_mul(&r, &r), &r)) < 1.0e-9);
470 assert!(inf_norm(&mat_mul(&r, &h)) < 1.0e-9);
471 assert!(inf_norm(&mat_mul(&mat_mul(&transpose(&r), &w), &h)) < 1.0e-9);
472
473 let q_v = q_v_matrix(&rows, &q_xx);
474 let q_v_w = mat_mul(&q_v, &w);
475 let normal = normal_matrix(&rows);
476 for (idx, obs) in report.per_observation.iter().enumerate() {
477 assert_close(obs.redundancy, q_v_w[idx][idx], 2.0e-14);
478 let mdb = obs.mdb_m.expect("checkable MDB");
479 let dx = full_state_external(&rows[idx], &q_xx, mdb);
480 let bnr_full = quadratic_form(&normal, &dx).sqrt();
481 assert_close(bnr_full, obs.bias_to_noise.expect("checkable BNR"), 2.0e-12);
482 }
483 }
484
485 #[test]
486 fn baarda_wtest_constant_and_monotonicity_are_pinned() {
487 let lambda0 = wtest_noncentrality(1.0e-3, 0.20).expect("canonical lambda");
488 let delta0 = lambda0.sqrt();
489
490 assert_close(delta0, 4.132147965064809, 2.0e-15);
491 assert_close(lambda0, 17.074646805189243, 4.0e-15);
492 assert_eq!((delta0 * 100.0).round() / 100.0, 4.13);
493 assert_eq!((lambda0 * 100.0).round() / 100.0, 17.07);
494
495 let smaller_alpha = wtest_noncentrality(1.0e-4, 0.20).expect("smaller alpha");
496 let smaller_beta = wtest_noncentrality(1.0e-3, 0.10).expect("smaller beta");
497 assert!(smaller_alpha > lambda0);
498 assert!(smaller_beta > lambda0);
499 }
500
501 #[test]
502 fn equal_precision_single_unknown_closed_form_is_exact_to_roundoff() {
503 let lambda0 = wtest_noncentrality(1.0e-3, 0.20).expect("lambda");
504 let sigma_m = 1.0;
505 let rows = equal_precision_rows(2, sigma_m);
506 let report = reliability_design(&rows, &ReliabilityOptions::default()).expect("report");
507
508 for obs in &report.per_observation {
509 assert_close(obs.redundancy, 0.5, 1.0e-15);
510 assert_close(
511 obs.mdb_m.expect("MDB"),
512 sigma_m * (2.0 * lambda0).sqrt(),
513 1.0e-14,
514 );
515 assert_close(obs.bias_to_noise.expect("BNR"), lambda0.sqrt(), 1.0e-14);
516 }
517 assert_eq!(
518 (report.per_observation[0].mdb_m.expect("MDB") * 100.0).round() / 100.0,
519 5.84
520 );
521 assert_eq!(
522 (report.per_observation[0].bias_to_noise.expect("BNR") * 100.0).round() / 100.0,
523 4.13
524 );
525 assert_close(report.summary.sum_redundancy, 1.0, 1.0e-15);
526
527 let report = reliability_design(
528 &equal_precision_rows(1, sigma_m),
529 &ReliabilityOptions::default(),
530 )
531 .expect("one-row report");
532 assert_eq!(report.summary.n_uncheckable, 1);
533 assert!(report.per_observation[0].uncheckable);
534 assert_eq!(report.per_observation[0].mdb_m, None);
535 assert_eq!(report.per_observation[0].bias_to_noise, None);
536 assert_eq!(report.per_observation[0].external_enu_m, None);
537 }
538
539 #[test]
540 fn teunissen_leveling_connection_matches_published_formula() {
541 let n = 4usize;
542 let sigma_h = 2.0;
543 let sigma_h_cap = 3.0;
544 let rows = leveling_connection_rows(n, sigma_h, sigma_h_cap);
545 let report = reliability_design(&rows, &options_with_lambda(LAMBDA_BAARDA_4DP))
546 .expect("leveling report");
547
548 let variance_sum = sigma_h * sigma_h + sigma_h_cap * sigma_h_cap;
549 let common = 1.0 - 1.0 / n as f64;
550 let expected_r_h = sigma_h * sigma_h * common / variance_sum;
551 let expected_r_h_cap = sigma_h_cap * sigma_h_cap * common / variance_sum;
552 let expected_mdb = (LAMBDA_BAARDA_4DP * variance_sum / common).sqrt();
553
554 for obs in &report.per_observation[..n] {
555 assert_close(obs.redundancy, expected_r_h, 2.0e-14);
556 assert_close(obs.mdb_m.expect("MDB"), expected_mdb, 2.0e-13);
557 }
558 for obs in &report.per_observation[n..] {
559 assert_close(obs.redundancy, expected_r_h_cap, 2.0e-14);
560 assert_close(obs.mdb_m.expect("MDB"), expected_mdb, 2.0e-13);
561 }
562 assert_close(report.summary.sum_redundancy, n as f64 - 1.0, 2.0e-13);
563
564 let one = reliability_design(
565 &leveling_connection_rows(1, sigma_h, sigma_h_cap),
566 &options_with_lambda(LAMBDA_BAARDA_4DP),
567 )
568 .expect("one station report");
569 assert_eq!(one.summary.n_uncheckable, 2);
570 assert!(one.per_observation.iter().all(|obs| obs.mdb_m.is_none()));
571 }
572
573 #[test]
574 fn araim_external_reliability_reuses_gain_matrix_columns() {
575 let geometry = simple_araim_geometry();
576 let model = UnitProtectionModel;
577 let options = options_with_lambda(LAMBDA_BAARDA_4DP);
578 let report = reliability_araim(&geometry, &model, &options).expect("ARAIM report");
579 let weights = vec![1.0; geometry.rows.len()];
580 let gain = gain_matrix_enu(&geometry, &weights).expect("gain");
581
582 for (idx, obs) in report.per_observation.iter().enumerate() {
583 let mdb = obs.mdb_m.expect("MDB");
584 let external = obs.external_enu_m.expect("external ENU");
585 for (coord, value) in external.iter().enumerate() {
586 assert_close(*value / mdb, gain.enu_rows[coord][idx], 2.0e-12);
587 }
588 }
589
590 let design_rows = geometry
591 .rows
592 .iter()
593 .map(|row| RangeReliabilityRow {
594 id: row.id.to_string(),
595 design_row: design_row(&geometry.clock_systems, row).expect("design row"),
596 sigma_m: 1.0,
597 })
598 .collect::<Vec<_>>();
599 let design_report = reliability_design(&design_rows, &options).expect("design report");
600 let enu = ecef_to_enu_rotation(geometry.receiver.lat_rad, geometry.receiver.lon_rad);
601 for (idx, obs) in design_report.per_observation.iter().enumerate() {
602 let mdb = obs.mdb_m.expect("MDB");
603 let state = obs.external_enu_m.expect("state external");
604 let rotated = mat3_vec(enu, state);
605 for (coord, value) in rotated.iter().enumerate() {
606 assert_close(*value / mdb, gain.enu_rows[coord][idx], 2.0e-12);
607 }
608 }
609 }
610
611 #[test]
612 fn invalid_inputs_use_quality_error_domains() {
613 let mut rows = equal_precision_rows(2, 1.0);
614 rows[0].sigma_m = 0.0;
615 assert_eq!(
616 reliability_design(&rows, &ReliabilityOptions::default()),
617 Err(QualityError::InvalidWeight)
618 );
619
620 rows = equal_precision_rows(2, 1.0);
621 let mut options = ReliabilityOptions {
622 beta: 0.0,
623 ..ReliabilityOptions::default()
624 };
625 assert_eq!(
626 reliability_design(&rows, &options),
627 Err(QualityError::InvalidReliabilityParameter)
628 );
629 options = ReliabilityOptions {
630 lambda0_override: Some(f64::INFINITY),
631 ..ReliabilityOptions::default()
632 };
633 assert_eq!(
634 reliability_design(&rows, &options),
635 Err(QualityError::InvalidReliabilityParameter)
636 );
637 options = ReliabilityOptions {
638 min_redundancy: 0.0,
639 ..ReliabilityOptions::default()
640 };
641 assert_eq!(
642 reliability_design(&rows, &options),
643 Err(QualityError::InvalidReliabilityParameter)
644 );
645 options = ReliabilityOptions {
646 alpha: 1.0,
647 ..ReliabilityOptions::default()
648 };
649 assert_eq!(
650 reliability_design(&rows, &options),
651 Err(QualityError::InvalidProbability)
652 );
653
654 let singular = vec![
655 RangeReliabilityRow {
656 id: "a".to_owned(),
657 design_row: vec![1.0, 1.0],
658 sigma_m: 1.0,
659 },
660 RangeReliabilityRow {
661 id: "b".to_owned(),
662 design_row: vec![2.0, 2.0],
663 sigma_m: 1.0,
664 },
665 ];
666 assert_eq!(
667 reliability_design(&singular, &ReliabilityOptions::default()),
668 Err(QualityError::SingularGeometry)
669 );
670 }
671
672 struct UnitProtectionModel;
673
674 impl ProtectionModel for UnitProtectionModel {
675 fn sigma_int_m(&self, _row: &AraimRow) -> Result<f64, AraimError> {
676 Ok(1.0)
677 }
678
679 fn sigma_acc_m(&self, _row: &AraimRow) -> Result<f64, AraimError> {
680 Ok(1.0)
681 }
682 }
683
684 fn options_with_lambda(lambda0: f64) -> ReliabilityOptions {
685 ReliabilityOptions {
686 lambda0_override: Some(lambda0),
687 ..ReliabilityOptions::default()
688 }
689 }
690
691 fn algebraic_rows() -> Vec<RangeReliabilityRow> {
692 vec![
693 row("r1", &[1.0, 0.3, -0.2], 0.9),
694 row("r2", &[0.2, 1.1, 0.4], 1.1),
695 row("r3", &[-0.5, 0.7, 1.0], 1.3),
696 row("r4", &[1.2, -0.4, 0.6], 0.8),
697 row("r5", &[-0.7, -0.9, 0.3], 1.5),
698 row("r6", &[0.4, -0.2, -1.1], 1.2),
699 ]
700 }
701
702 fn equal_precision_rows(n: usize, sigma_m: f64) -> Vec<RangeReliabilityRow> {
703 (0..n)
704 .map(|idx| RangeReliabilityRow {
705 id: format!("obs{}", idx + 1),
706 design_row: vec![1.0],
707 sigma_m,
708 })
709 .collect()
710 }
711
712 fn leveling_connection_rows(
713 n: usize,
714 sigma_h: f64,
715 sigma_h_cap: f64,
716 ) -> Vec<RangeReliabilityRow> {
717 let mut rows = Vec::with_capacity(2 * n);
718 for idx in 0..n {
719 let mut design = vec![0.0; n + 1];
720 design[idx] = 1.0;
721 design[n] = 1.0;
722 rows.push(RangeReliabilityRow {
723 id: format!("h{}", idx + 1),
724 design_row: design,
725 sigma_m: sigma_h,
726 });
727 }
728 for idx in 0..n {
729 let mut design = vec![0.0; n + 1];
730 design[idx] = 1.0;
731 rows.push(RangeReliabilityRow {
732 id: format!("H{}", idx + 1),
733 design_row: design,
734 sigma_m: sigma_h_cap,
735 });
736 }
737 rows
738 }
739
740 fn row(id: &str, design_row: &[f64], sigma_m: f64) -> RangeReliabilityRow {
741 RangeReliabilityRow {
742 id: id.to_owned(),
743 design_row: design_row.to_vec(),
744 sigma_m,
745 }
746 }
747
748 fn simple_araim_geometry() -> AraimGeometry {
749 const INV_SQRT_3: f64 = 0.5773502691896258;
750 let receiver = Wgs84Geodetic::new(0.5, 0.2, 0.0).expect("valid receiver");
751 let vectors = [
752 [INV_SQRT_3, INV_SQRT_3, INV_SQRT_3],
753 [INV_SQRT_3, -INV_SQRT_3, -INV_SQRT_3],
754 [-INV_SQRT_3, INV_SQRT_3, -INV_SQRT_3],
755 [-INV_SQRT_3, -INV_SQRT_3, INV_SQRT_3],
756 [0.2, 0.6, 0.7745966692414834],
757 ];
758 let rows = vectors
759 .iter()
760 .enumerate()
761 .map(|(idx, los)| AraimRow {
762 id: GnssSatelliteId::new(GnssSystem::Gps, (idx + 1) as u8)
763 .expect("valid satellite"),
764 line_of_sight: LineOfSight::new(los[0], los[1], los[2]),
765 system: GnssSystem::Gps,
766 elevation_rad: 0.8,
767 })
768 .collect();
769 AraimGeometry {
770 rows,
771 receiver,
772 clock_systems: vec![GnssSystem::Gps],
773 }
774 }
775
776 fn design_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
777 rows.iter().map(|row| row.design_row.clone()).collect()
778 }
779
780 fn weight_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
781 let mut w = vec![vec![0.0; rows.len()]; rows.len()];
782 for (idx, row) in rows.iter().enumerate() {
783 w[idx][idx] = 1.0 / (row.sigma_m * row.sigma_m);
784 }
785 w
786 }
787
788 fn residual_projector(rows: &[RangeReliabilityRow], q_xx: &[Vec<f64>]) -> Vec<Vec<f64>> {
789 let h = design_matrix(rows);
790 let h_t = transpose(&h);
791 let w = weight_matrix(rows);
792 let h_q = mat_mul(&h, q_xx);
793 let h_q_h_t_w = mat_mul(&mat_mul(&h_q, &h_t), &w);
794 let mut out = identity(rows.len());
795 for i in 0..rows.len() {
796 for j in 0..rows.len() {
797 out[i][j] -= h_q_h_t_w[i][j];
798 }
799 }
800 out
801 }
802
803 fn q_v_matrix(rows: &[RangeReliabilityRow], q_xx: &[Vec<f64>]) -> Vec<Vec<f64>> {
804 let h = design_matrix(rows);
805 let h_t = transpose(&h);
806 let h_q_h_t = mat_mul(&mat_mul(&h, q_xx), &h_t);
807 let mut q_v = vec![vec![0.0; rows.len()]; rows.len()];
808 for i in 0..rows.len() {
809 q_v[i][i] = rows[i].sigma_m * rows[i].sigma_m;
810 for j in 0..rows.len() {
811 q_v[i][j] -= h_q_h_t[i][j];
812 }
813 }
814 q_v
815 }
816
817 fn normal_matrix(rows: &[RangeReliabilityRow]) -> Vec<Vec<f64>> {
818 let n_params = rows[0].design_row.len();
819 normal_equations_weighted(
820 rows.iter()
821 .map(|row| (row.design_row.as_slice(), 0.0, 1.0 / row.sigma_m)),
822 n_params,
823 )
824 .expect("normal matrix")
825 .0
826 }
827
828 fn full_state_external(row: &RangeReliabilityRow, q_xx: &[Vec<f64>], mdb_m: f64) -> Vec<f64> {
829 let scale = mdb_m / (row.sigma_m * row.sigma_m);
830 mat_vec(q_xx, &row.design_row)
831 .iter()
832 .map(|value| value * scale)
833 .collect()
834 }
835
836 fn identity(n: usize) -> Vec<Vec<f64>> {
837 let mut out = vec![vec![0.0; n]; n];
838 for (idx, row) in out.iter_mut().enumerate() {
839 row[idx] = 1.0;
840 }
841 out
842 }
843
844 fn mat_mul(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
845 let b_t = transpose(b);
846 a.iter()
847 .map(|row| b_t.iter().map(|col| dot(row, col)).collect())
848 .collect()
849 }
850
851 fn mat_sub(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
852 a.iter()
853 .zip(b)
854 .map(|(ra, rb)| ra.iter().zip(rb).map(|(x, y)| x - y).collect())
855 .collect()
856 }
857
858 fn transpose(a: &[Vec<f64>]) -> Vec<Vec<f64>> {
859 let cols = a[0].len();
860 (0..cols)
861 .map(|col| a.iter().map(|row| row[col]).collect())
862 .collect()
863 }
864
865 fn trace(a: &[Vec<f64>]) -> f64 {
866 a.iter().enumerate().map(|(idx, row)| row[idx]).sum()
867 }
868
869 fn inf_norm(a: &[Vec<f64>]) -> f64 {
870 a.iter()
871 .map(|row| row.iter().map(|value| value.abs()).sum::<f64>())
872 .fold(0.0, f64::max)
873 }
874
875 fn quadratic_form(a: &[Vec<f64>], x: &[f64]) -> f64 {
876 dot(x, &mat_vec(a, x))
877 }
878
879 fn mat3_vec(a: [[f64; 3]; 3], x: [f64; 3]) -> [f64; 3] {
880 [
881 a[0][0] * x[0] + a[0][1] * x[1] + a[0][2] * x[2],
882 a[1][0] * x[0] + a[1][1] * x[1] + a[1][2] * x[2],
883 a[2][0] * x[0] + a[2][1] * x[1] + a[2][2] * x[2],
884 ]
885 }
886
887 fn assert_close(actual: f64, expected: f64, tol: f64) {
888 assert!(
889 (actual - expected).abs() <= tol,
890 "actual={actual:.17e} expected={expected:.17e} tol={tol:.17e}"
891 );
892 }
893}