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