use crate::raim::{araim_integrity_risk, araim_protection_level, normal_quantile, AraimMode};
use serde::Deserialize;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ReferenceSatellite {
pub east: f64,
pub north: f64,
pub up: f64,
pub constellation: usize,
pub c_int_m2: f64,
pub c_acc_m2: f64,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ReferenceConstants {
pub p_hmi_vert: f64,
pub p_hmi_horz: f64,
pub p_fa_vert: f64,
pub p_fa_horz: f64,
pub p_emt: f64,
pub p_thres: f64,
pub tol_pl_m: f64,
}
impl ReferenceConstants {
pub const LPV_200: ReferenceConstants = ReferenceConstants {
p_hmi_vert: 9.8e-8,
p_hmi_horz: 1e-7 - 9.8e-8,
p_fa_vert: 3.9e-6,
p_fa_horz: 9e-8,
p_emt: 1e-5,
p_thres: 8e-8,
tol_pl_m: 5e-2,
};
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReferenceCase {
pub satellites: Vec<ReferenceSatellite>,
pub constellations: usize,
pub b_nom_m: f64,
pub p_sat: f64,
pub p_const: f64,
pub constants: ReferenceConstants,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReferenceModeReport {
pub label: String,
pub p_fault: f64,
pub sigma_up_m: f64,
pub sigma_ss_up_m: f64,
pub bias_up_m: f64,
pub threshold_up_m: f64,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReferenceResult {
pub n_fault_max: usize,
pub n_fault_modes: usize,
pub p_fault_not_monitored: f64,
pub risk_allocation_factor: f64,
pub k_fa_vert: f64,
pub k_fa_horz: f64,
pub sigma_up_m: f64,
pub bias_up_m: f64,
pub vpl_m: f64,
pub hpl_m: f64,
pub hpl_east_m: f64,
pub hpl_north_m: f64,
pub emt_m: f64,
pub sigma_v_acc_m: f64,
pub achieved_risk_vert: f64,
pub allocated_risk_vert: f64,
pub constellation_modes: Vec<ReferenceModeReport>,
}
fn invert(a: &[Vec<f64>]) -> Option<Vec<Vec<f64>>> {
let n = a.len();
let mut m: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut row = a[i].clone();
row.extend((0..n).map(|j| if i == j { 1.0 } else { 0.0 }));
row
})
.collect();
for col in 0..n {
let mut pivot = col;
for r in col + 1..n {
if m[r][col].abs() > m[pivot][col].abs() {
pivot = r;
}
}
if m[pivot][col].abs() < 1e-14 {
return None;
}
m.swap(col, pivot);
let d = m[col][col];
for v in m[col].iter_mut() {
*v /= d;
}
let pivot_row = m[col].clone();
for (r, row) in m.iter_mut().enumerate() {
if r == col {
continue;
}
let f = row[col];
if f == 0.0 {
continue;
}
for (v, p) in row.iter_mut().zip(pivot_row.iter()).skip(col) {
*v -= f * p;
}
}
}
Some(m.into_iter().map(|row| row[n..].to_vec()).collect())
}
struct SubSolution {
s: [Vec<f64>; 3],
sigma: [f64; 3],
}
fn subset_solution(case: &ReferenceCase, keep: &[bool]) -> Option<SubSolution> {
let n = case.satellites.len();
let m = 3 + case.constellations;
let w: Vec<f64> = (0..n)
.map(|i| {
if keep[i] && case.satellites[i].c_int_m2 > 0.0 {
1.0 / case.satellites[i].c_int_m2
} else {
0.0
}
})
.collect();
let g = |i: usize, j: usize| -> f64 {
let s = case.satellites[i];
match j {
0 => s.east,
1 => s.north,
2 => s.up,
_ => {
if s.constellation + 3 == j {
1.0
} else {
0.0
}
}
}
};
let live: Vec<usize> = (0..m)
.filter(|&j| (0..n).any(|i| w[i] != 0.0 && g(i, j) != 0.0))
.collect();
if !(0..3).all(|q| live.contains(&q)) {
return None;
}
let l = live.len();
let mut ata = vec![vec![0.0; l]; l];
for (a, &ja) in live.iter().enumerate() {
for (b, &jb) in live.iter().enumerate() {
ata[a][b] = (0..n).map(|i| g(i, ja) * w[i] * g(i, jb)).sum();
}
}
let cov = invert(&ata)?;
let mut s = [vec![0.0; n], vec![0.0; n], vec![0.0; n]];
let mut sigma = [0.0; 3];
for q in 0..3 {
let a = live.iter().position(|&j| j == q)?;
if cov[a][a] < 0.0 {
return None;
}
sigma[q] = cov[a][a].sqrt();
for i in 0..n {
s[q][i] = (0..l).map(|b| cov[a][b] * g(i, live[b])).sum::<f64>() * w[i];
}
}
Some(SubSolution { s, sigma })
}
fn quadrature(row: &[f64], c: &[f64]) -> f64 {
row.iter()
.zip(c)
.map(|(r, ci)| r * r * ci)
.sum::<f64>()
.max(0.0)
.sqrt()
}
fn n_fault_max(sum_p_event: f64, p_thres: f64) -> usize {
let mut factorial = 1.0_f64;
for r in 1..=20usize {
factorial *= (r + 1) as f64;
let edge = (factorial * p_thres).powf(1.0 / (r + 1) as f64);
if sum_p_event <= edge {
return r;
}
}
21
}
pub fn araim_reference_protection_levels(case: &ReferenceCase) -> Result<ReferenceResult, String> {
let n = case.satellites.len();
if n == 0 {
return Err("a reference case needs at least one satellite".to_string());
}
if case.constellations == 0 {
return Err("a reference case needs at least one constellation".to_string());
}
for (i, s) in case.satellites.iter().enumerate() {
if s.constellation >= case.constellations {
return Err(format!(
"satellite {i} names constellation {} but only {} are declared",
s.constellation, case.constellations
));
}
if s.c_int_m2 <= 0.0
|| s.c_acc_m2 <= 0.0
|| !s.c_int_m2.is_finite()
|| !s.c_acc_m2.is_finite()
{
return Err(format!("satellite {i} has a non-positive error variance"));
}
}
let k = &case.constants;
if k.p_hmi_vert <= 0.0 || k.p_hmi_horz <= 0.0 {
return Err("both integrity-risk allocations must be positive".to_string());
}
let mut p_event: Vec<f64> = vec![case.p_sat; n];
p_event.extend(vec![case.p_const; case.constellations]);
let sum_p_event: f64 = p_event.iter().sum();
let nfm = n_fault_max(sum_p_event, k.p_thres);
if nfm != 1 {
return Err(format!(
"this reference implementation monitors single-event fault modes only, but the \
case's priors require N_fault,max = {nfm} (simultaneous multi-event subsets). \
Refusing rather than under-counting fault modes."
));
}
let c_acc: Vec<f64> = case.satellites.iter().map(|s| s.c_acc_m2).collect();
let all = vec![true; n];
let s0 = subset_solution(case, &all).ok_or("the all-in-view geometry is singular")?;
let bias0: [f64; 3] =
std::array::from_fn(|q| s0.s[q].iter().map(|v| v.abs()).sum::<f64>() * case.b_nom_m);
let sigma_v_acc = quadrature(&s0.s[2], &c_acc);
struct Mode {
label: String,
p_fault: f64,
sigma: [f64; 3],
sigma_ss: [f64; 3],
bias: [f64; 3],
constellation: Option<usize>,
}
let mut modes: Vec<Mode> = Vec::new();
let mut p_unobservable = 0.0_f64;
let mut push = |label: String, p: f64, keep: Vec<bool>, constellation: Option<usize>| {
let kept = keep.iter().filter(|b| **b).count();
let used: std::collections::BTreeSet<usize> = (0..n)
.filter(|&i| keep[i])
.map(|i| case.satellites[i].constellation)
.collect();
match subset_solution(case, &keep) {
Some(sk) if kept >= 3 + used.len() => {
let sigma_ss = std::array::from_fn(|q| {
let d: Vec<f64> = (0..n).map(|i| sk.s[q][i] - s0.s[q][i]).collect();
quadrature(&d, &c_acc)
});
let bias = std::array::from_fn(|q| {
sk.s[q].iter().map(|v| v.abs()).sum::<f64>() * case.b_nom_m
});
modes.push(Mode {
label,
p_fault: p,
sigma: sk.sigma,
sigma_ss,
bias,
constellation,
});
}
_ => p_unobservable += p,
}
};
for i in 0..n {
let keep: Vec<bool> = (0..n).map(|j| j != i).collect();
push(format!("sat-{}", i + 1), case.p_sat, keep, None);
}
for j in 0..case.constellations {
let keep: Vec<bool> = (0..n)
.map(|i| case.satellites[i].constellation != j)
.collect();
push(
format!("constellation-{}", j + 1),
case.p_const,
keep,
Some(j),
);
}
if modes.is_empty() {
return Err("no fault mode is observable on this geometry".to_string());
}
let p_no_fault: f64 = p_event.iter().map(|p| 1.0 - p).product();
let p_exactly_one: f64 = p_no_fault * p_event.iter().map(|p| p / (1.0 - p)).sum::<f64>();
let p_two_or_more = (1.0 - p_no_fault - p_exactly_one).max(0.0);
let p_not_monitored = p_two_or_more + p_unobservable;
let factor = 1.0 - p_not_monitored / (k.p_hmi_vert + k.p_hmi_horz);
if factor <= 0.0 {
return Err(
"the unmonitored fault risk exhausts the whole integrity budget; no finite \
protection level exists"
.to_string(),
);
}
let nmodes = modes.len() as f64;
let k_fa_vert = normal_quantile(1.0 - k.p_fa_vert / (2.0 * nmodes));
let k_fa_horz = normal_quantile(1.0 - k.p_fa_horz / (4.0 * nmodes));
let k_fa = [k_fa_horz, k_fa_horz, k_fa_vert];
let axis_modes = |q: usize| -> Vec<AraimMode> {
let mut v = vec![AraimMode {
p_fault: 2.0,
threshold_m: 0.0,
bias_m: bias0[q],
sigma_m: s0.sigma[q],
}];
v.extend(modes.iter().map(|m| AraimMode {
p_fault: m.p_fault,
threshold_m: k_fa[q] * m.sigma_ss[q],
bias_m: m.bias[q],
sigma_m: m.sigma[q],
}));
v
};
let modes_e = axis_modes(0);
let modes_n = axis_modes(1);
let modes_v = axis_modes(2);
let allocated_vert = k.p_hmi_vert * factor;
let allocated_horz = 0.5 * k.p_hmi_horz * factor;
let vpl = araim_protection_level(&modes_v, allocated_vert);
let hpl_e = araim_protection_level(&modes_e, allocated_horz);
let hpl_n = araim_protection_level(&modes_n, allocated_horz);
let emt = modes
.iter()
.filter(|m| m.p_fault >= k.p_emt)
.map(|m| k_fa_vert * m.sigma_ss[2])
.fold(0.0_f64, f64::max);
let constellation_modes = modes
.iter()
.filter(|m| m.constellation.is_some())
.map(|m| ReferenceModeReport {
label: m.label.clone(),
p_fault: m.p_fault,
sigma_up_m: m.sigma[2],
sigma_ss_up_m: m.sigma_ss[2],
bias_up_m: m.bias[2],
threshold_up_m: k_fa_vert * m.sigma_ss[2],
})
.collect();
Ok(ReferenceResult {
n_fault_max: nfm,
n_fault_modes: modes.len(),
p_fault_not_monitored: p_not_monitored,
risk_allocation_factor: factor,
k_fa_vert,
k_fa_horz,
sigma_up_m: s0.sigma[2],
bias_up_m: bias0[2],
vpl_m: vpl,
hpl_m: hpl_e.hypot(hpl_n),
hpl_east_m: hpl_e,
hpl_north_m: hpl_n,
emt_m: emt,
sigma_v_acc_m: sigma_v_acc,
achieved_risk_vert: araim_integrity_risk(vpl, &modes_v),
allocated_risk_vert: allocated_vert,
constellation_modes,
})
}
const GEOMETRY: [(f64, f64, f64, usize); 10] = [
(0.0225, 0.9951, -0.0966, 0),
(0.6750, -0.6900, -0.2612, 0),
(0.0723, -0.6601, -0.7477, 0),
(-0.9398, 0.2553, -0.2269, 0),
(-0.5907, -0.7539, -0.2877, 0),
(-0.3236, -0.0354, -0.9455, 1),
(-0.6748, 0.4356, -0.5957, 1),
(0.0938, -0.7004, -0.7075, 1),
(0.5571, 0.3088, -0.7709, 1),
(0.6622, 0.6958, -0.2780, 1),
];
pub const MILESTONE3_ROW3_UP_AS_PRINTED: f64 = 0.7477;
const ADD_V31_C_INT: [f64; 10] = [
3.2899, 1.2792, 0.7901, 1.4430, 1.1847, 0.7737, 0.8233, 0.7962, 0.7871, 1.2166,
];
const ADD_V31_C_ACC: [f64; 10] = [
2.9774, 0.9667, 0.4776, 1.1305, 0.8722, 0.4612, 0.5108, 0.4837, 0.4746, 0.9041,
];
const MS3_C_INT: [f64; 10] = [
3.8865, 1.4377, 0.8604, 1.6383, 1.3229, 0.8434, 0.8963, 0.8669, 0.8573, 1.3616,
];
const MS3_C_ACC: [f64; 10] = [
3.5740, 1.1252, 0.5479, 1.3258, 1.0104, 0.5309, 0.5838, 0.5544, 0.5448, 1.0491,
];
fn case_with(c_int: &[f64; 10], c_acc: &[f64; 10], row3_up: Option<f64>) -> ReferenceCase {
let satellites = GEOMETRY
.iter()
.enumerate()
.map(
|(i, &(east, north, up, constellation))| ReferenceSatellite {
east,
north,
up: if i == 2 { row3_up.unwrap_or(up) } else { up },
constellation,
c_int_m2: c_int[i],
c_acc_m2: c_acc[i],
},
)
.collect();
ReferenceCase {
satellites,
constellations: 2,
b_nom_m: 0.5,
p_sat: 1e-5,
p_const: 1e-4,
constants: ReferenceConstants::LPV_200,
}
}
#[derive(Clone, Debug)]
pub struct PublishedVector {
pub id: &'static str,
pub citation: &'static str,
pub url: &'static str,
pub retrieved: &'static str,
pub source_sha256: &'static str,
pub location: &'static str,
pub tolerance_m: f64,
pub tolerance_source: &'static str,
pub vpl_m: f64,
pub hpl_m: f64,
pub emt_m: f64,
pub sigma_v_acc_m: f64,
pub k_fa_vert: f64,
pub n_fault_modes_in_k_fa: usize,
pub constellation_sigma_up_m: [f64; 2],
pub constellation_sigma_ss_up_m: [f64; 2],
pub constellation_bias_up_m: [f64; 2],
pub protection_levels_reproducible: bool,
pub note: &'static str,
pub case: ReferenceCase,
}
pub fn published_vectors() -> Vec<PublishedVector> {
vec![
PublishedVector {
id: "add-v3.1-appendix-d",
citation: "EU-U.S. Cooperation on Satellite Navigation, Working Group C, ARAIM \
Technical Subgroup, Reference Airborne Algorithm Description Document, \
Version 3.1, 20 June 2019",
url: "https://web.stanford.edu/group/scpnt/gpslab/website_files/maast/\
ARAIM_TSG_Reference_ADD_v3.1.pdf",
retrieved: "2026-09-20",
source_sha256: "7f42934488c5c2261363439fabd48385e39526df34d514f395e22b6990b6bdb7",
location: "Appendix D, 'Numerical example for LPV-200', equations (79)-(84), \
pp. 30-31",
tolerance_m: 5e-2,
tolerance_source: "TOL_PL, 'tolerance for the computation of the Protection Level', \
Table 4 (constants derived from the navigation requirements); the \
text requires the output VPL to be within TOL_PL of the solution \
of the protection-level equation",
vpl_m: 18.3,
hpl_m: 13.45,
emt_m: 7.2998,
sigma_v_acc_m: 1.3694,
k_fa_vert: 5.1083,
n_fault_modes_in_k_fa: 12,
constellation_sigma_up_m: [2.4600, 2.4359],
constellation_sigma_ss_up_m: [1.4290, 1.4217],
constellation_bias_up_m: [2.8915, 2.0875],
protection_levels_reproducible: true,
note: "Self-consistent: N_fault,max = 1 gives 12 monitored fault modes (10 \
single-satellite + 2 constellation) and the printed K_fa,3 is evaluated at \
2 x 12, matching. This is the acceptance vector.",
case: case_with(&ADD_V31_C_INT, &ADD_V31_C_ACC, None),
},
PublishedVector {
id: "milestone3-annex-a-ix",
citation: "EU-U.S. Cooperation on Satellite Navigation, Working Group C, ARAIM \
Technical Subgroup, Milestone 3 Report, Final Version, 25 February 2016",
url: "https://www.gps.gov/sites/default/files/2025-09/ARAIM-milestone-3-report.pdf",
retrieved: "2026-09-20",
source_sha256: "156147e1e7aa9514cf7c9b7c7df9762daa7845981ec7037ad3e8aa5baeae13d8",
location: "Annex A, section A.IX 'Numerical example', equations (60)-(65), pp. 95-97",
tolerance_m: 5e-2,
tolerance_source: "TOL_PL, 'tolerance for the computation of the Protection Level', \
Table 24 (list of constants), Annex A section A.III.3",
vpl_m: 19.2,
hpl_m: 14.5,
emt_m: 8.3,
sigma_v_acc_m: 1.47,
k_fa_vert: 5.3953,
n_fault_modes_in_k_fa: 57,
constellation_sigma_up_m: [2.5760, 2.5577],
constellation_sigma_ss_up_m: [1.5307, 1.5292],
constellation_bias_up_m: [2.8935, 2.0875],
protection_levels_reproducible: false,
note: "TWO INTERNAL INCONSISTENCIES, both demonstrated rather than assumed in \
tests/araim_reference_vectors.rs. (1) Row 3's Up component is printed as \
+0.7477; the document's own sigma_v,acc = 1.47 m is only reproducible with \
-0.7477, the sign the 2019 description prints. (2) The document states \
N_fault,max = 1, which yields 12 monitored fault modes, but its printed \
K_fa,3 = 5.3953 is evaluated at 2 x 57 and its EMT = 8.3 m follows that \
K_fa,3, while its VPL and HPL do not. A conforming implementation therefore \
cannot reproduce all of this vector's outputs at once; the geometry-derived \
intermediates, which carry no such ambiguity, are reproduced exactly.",
case: case_with(&MS3_C_INT, &MS3_C_ACC, None),
},
]
}
fn arc_default_vector() -> String {
"all".to_string()
}
#[derive(Deserialize)]
pub struct AraimReferenceCheckScenario {
#[serde(default = "arc_default_vector")]
pub vector: String,
}
impl Default for AraimReferenceCheckScenario {
fn default() -> Self {
AraimReferenceCheckScenario {
vector: arc_default_vector(),
}
}
}
impl AraimReferenceCheckScenario {
pub fn run_json(&self) -> Result<(String, String), String> {
let all = published_vectors();
let wanted: Vec<&PublishedVector> = if self.vector == "all" {
all.iter().collect()
} else {
let hit: Vec<&PublishedVector> = all.iter().filter(|v| v.id == self.vector).collect();
if hit.is_empty() {
let ids: Vec<&str> = all.iter().map(|v| v.id).collect();
return Err(format!(
"unknown reference vector '{}'; known vectors: {} (or 'all')",
self.vector,
ids.join(", ")
));
}
hit
};
let mut rows = Vec::new();
let mut worst_acceptance = 0.0_f64;
let mut acceptance_checked = 0usize;
for v in &wanted {
let r = araim_reference_protection_levels(&v.case)?;
let d_vpl = (r.vpl_m - v.vpl_m).abs();
let d_hpl = (r.hpl_m - v.hpl_m).abs();
let d_emt = (r.emt_m - v.emt_m).abs();
let d_sig = (r.sigma_v_acc_m - v.sigma_v_acc_m).abs();
if v.protection_levels_reproducible {
acceptance_checked += 1;
worst_acceptance = worst_acceptance.max(d_vpl).max(d_hpl);
}
let const_rows: Vec<serde_json::Value> = r
.constellation_modes
.iter()
.enumerate()
.map(|(j, m)| {
serde_json::json!({
"label": m.label,
"p_fault": m.p_fault,
"sigma_up_m": m.sigma_up_m,
"sigma_up_published_m": v.constellation_sigma_up_m.get(j),
"sigma_ss_up_m": m.sigma_ss_up_m,
"sigma_ss_up_published_m": v.constellation_sigma_ss_up_m.get(j),
"bias_up_m": m.bias_up_m,
"bias_up_published_m": v.constellation_bias_up_m.get(j),
"threshold_up_m": m.threshold_up_m,
})
})
.collect();
rows.push(serde_json::json!({
"id": v.id,
"citation": v.citation,
"url": v.url,
"retrieved": v.retrieved,
"source_sha256": v.source_sha256,
"location": v.location,
"tolerance_m": v.tolerance_m,
"tolerance_source": v.tolerance_source,
"protection_levels_reproducible": v.protection_levels_reproducible,
"note": v.note,
"n_fault_max": r.n_fault_max,
"n_fault_modes": r.n_fault_modes,
"n_fault_modes_in_published_k_fa": v.n_fault_modes_in_k_fa,
"p_fault_not_monitored": r.p_fault_not_monitored,
"risk_allocation_factor": r.risk_allocation_factor,
"k_fa_vert": r.k_fa_vert,
"k_fa_vert_published": v.k_fa_vert,
"k_fa_horz": r.k_fa_horz,
"vpl_m": r.vpl_m,
"vpl_published_m": v.vpl_m,
"vpl_abs_error_m": d_vpl,
"vpl_within_tolerance": d_vpl <= v.tolerance_m,
"hpl_m": r.hpl_m,
"hpl_published_m": v.hpl_m,
"hpl_abs_error_m": d_hpl,
"hpl_within_tolerance": d_hpl <= v.tolerance_m,
"hpl_east_m": r.hpl_east_m,
"hpl_north_m": r.hpl_north_m,
"emt_m": r.emt_m,
"emt_published_m": v.emt_m,
"emt_abs_error_m": d_emt,
"sigma_v_acc_m": r.sigma_v_acc_m,
"sigma_v_acc_published_m": v.sigma_v_acc_m,
"sigma_v_acc_abs_error_m": d_sig,
"sigma_up_m": r.sigma_up_m,
"bias_up_m": r.bias_up_m,
"achieved_risk_vert": r.achieved_risk_vert,
"allocated_risk_vert": r.allocated_risk_vert,
"constellation_modes": const_rows,
}));
}
let units = serde_json::json!({
"vectors[].tolerance_m": {"unit": "m", "provenance": "published", "note": "TOL_PL as stated by the source document"},
"vectors[].n_fault_max": {"unit": "count", "provenance": "computed"},
"vectors[].n_fault_modes": {"unit": "count", "provenance": "computed"},
"vectors[].n_fault_modes_in_published_k_fa": {"unit": "count", "provenance": "published"},
"vectors[].p_fault_not_monitored": {"unit": "probability per approach (dimensionless)", "provenance": "computed"},
"vectors[].risk_allocation_factor": {"unit": "fraction (dimensionless)", "provenance": "computed"},
"vectors[].k_fa_vert": {"unit": "sigma multiplier (dimensionless)", "provenance": "computed"},
"vectors[].k_fa_vert_published": {"unit": "sigma multiplier (dimensionless)", "provenance": "published"},
"vectors[].k_fa_horz": {"unit": "sigma multiplier (dimensionless)", "provenance": "computed"},
"vectors[].vpl_m": {"unit": "m", "provenance": "computed"},
"vectors[].vpl_published_m": {"unit": "m", "provenance": "published"},
"vectors[].vpl_abs_error_m": {"unit": "m", "provenance": "computed", "note": "|computed - published|"},
"vectors[].hpl_m": {"unit": "m", "provenance": "computed"},
"vectors[].hpl_published_m": {"unit": "m", "provenance": "published"},
"vectors[].hpl_abs_error_m": {"unit": "m", "provenance": "computed", "note": "|computed - published|"},
"vectors[].hpl_east_m": {"unit": "m", "provenance": "computed"},
"vectors[].hpl_north_m": {"unit": "m", "provenance": "computed"},
"vectors[].emt_m": {"unit": "m", "provenance": "computed"},
"vectors[].emt_published_m": {"unit": "m", "provenance": "published"},
"vectors[].emt_abs_error_m": {"unit": "m", "provenance": "computed"},
"vectors[].sigma_v_acc_m": {"unit": "m", "provenance": "computed"},
"vectors[].sigma_v_acc_published_m": {"unit": "m", "provenance": "published"},
"vectors[].sigma_v_acc_abs_error_m": {"unit": "m", "provenance": "computed"},
"vectors[].sigma_up_m": {"unit": "m", "provenance": "computed"},
"vectors[].bias_up_m": {"unit": "m", "provenance": "computed"},
"vectors[].achieved_risk_vert": {"unit": "probability per approach (dimensionless)", "provenance": "computed"},
"vectors[].allocated_risk_vert": {"unit": "probability per approach (dimensionless)", "provenance": "computed"},
"vectors[].constellation_modes[].p_fault": {"unit": "probability per approach (dimensionless)", "provenance": "input"},
"vectors[].constellation_modes[].sigma_up_m": {"unit": "m", "provenance": "computed"},
"vectors[].constellation_modes[].sigma_up_published_m": {"unit": "m", "provenance": "published"},
"vectors[].constellation_modes[].sigma_ss_up_m": {"unit": "m", "provenance": "computed"},
"vectors[].constellation_modes[].sigma_ss_up_published_m": {"unit": "m", "provenance": "published"},
"vectors[].constellation_modes[].bias_up_m": {"unit": "m", "provenance": "computed"},
"vectors[].constellation_modes[].bias_up_published_m": {"unit": "m", "provenance": "published"},
"vectors[].constellation_modes[].threshold_up_m": {"unit": "m", "provenance": "computed"},
"worst_acceptance_error_m": {"unit": "m", "provenance": "computed", "note": "the largest |computed - published| over the VPL and HPL of every vector whose protection levels the source states consistently"},
"acceptance_tolerance_m": {"unit": "m", "provenance": "published", "note": "TOL_PL"},
"vectors_checked": {"unit": "count", "provenance": "computed"},
"acceptance_vectors": {"unit": "count", "provenance": "computed"},
});
let tol = wanted
.iter()
.filter(|v| v.protection_levels_reproducible)
.map(|v| v.tolerance_m)
.fold(f64::INFINITY, f64::min);
let tol = if tol.is_finite() { tol } else { 0.0 };
let mut json = serde_json::json!({
"kind": "araim-reference-check",
"label": "EXTERNALLY CHECKED — the engine's ARAIM protection levels run against the \
published worked numerical examples of the EU-U.S. Working Group C ARAIM \
Technical Subgroup reference airborne algorithm, at the tolerance (TOL_PL) \
those documents themselves state. Every published figure carries its \
source URL, retrieval date, file SHA-256 and page. This reproduces a \
reference algorithm's worked example; it is NOT a certification, an \
airworthiness artefact or an approval.",
"vectors": rows,
"vectors_checked": wanted.len(),
"acceptance_vectors": acceptance_checked,
"acceptance_tolerance_m": tol,
"worst_acceptance_error_m": worst_acceptance,
"acceptance_met": acceptance_checked > 0 && worst_acceptance <= tol,
"acceptance_definition": "For every vector whose source states its protection levels \
consistently, |computed - published| for both VPL and HPL \
must not exceed TOL_PL, the tolerance the source itself \
specifies for a protection-level computation. Vectors \
flagged protection_levels_reproducible = false are reported \
in full but excluded from the acceptance figure; their \
reason is in their note.",
});
json["units"] = units;
let summary = format!(
"araim-reference-check: {} published WG-C reference vector(s); worst VPL/HPL \
agreement {:.4} m against the reference's own TOL_PL = {:.2} m over {} acceptance \
vector(s)",
wanted.len(),
worst_acceptance,
tol,
acceptance_checked,
);
let json = serde_json::to_string_pretty(&json).map_err(|e| e.to_string())?;
Ok((json, summary))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vector(id: &str) -> PublishedVector {
published_vectors()
.into_iter()
.find(|v| v.id == id)
.expect("known vector id")
}
#[test]
fn the_published_variance_diagonals_differ_by_exactly_the_published_ura_ure_gap() {
let gap = 0.75f64.powi(2) - 0.50f64.powi(2);
for v in published_vectors() {
for (i, s) in v.case.satellites.iter().enumerate() {
assert!(
(s.c_int_m2 - s.c_acc_m2 - gap).abs() < 5e-13,
"{}: satellite {i} C_int - C_acc = {}, expected {gap}",
v.id,
s.c_int_m2 - s.c_acc_m2
);
}
}
}
#[test]
fn the_published_geometry_rows_are_unit_line_of_sight_vectors() {
for v in published_vectors() {
for (i, s) in v.case.satellites.iter().enumerate() {
let norm = (s.east * s.east + s.north * s.north + s.up * s.up).sqrt();
assert!(
(norm - 1.0).abs() < 1e-4,
"{}: row {i} has norm {norm}, not a unit line of sight",
v.id
);
}
}
}
#[test]
fn the_reference_rule_gives_single_event_fault_modes_for_the_published_priors() {
assert_eq!(n_fault_max(3e-4, 8e-8), 1);
assert!(n_fault_max(5e-3, 8e-8) > 1);
let mut heavy = vector("add-v3.1-appendix-d").case;
heavy.p_sat = 1e-3;
heavy.p_const = 1e-3;
let err = araim_reference_protection_levels(&heavy).expect_err("must refuse");
assert!(err.contains("N_fault,max"), "{err}");
}
#[test]
fn the_scenario_runs_every_vector_and_reports_the_published_tolerance() {
let (json, summary) = AraimReferenceCheckScenario::default()
.run_json()
.expect("the reference check runs");
let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
assert_eq!(v["kind"], "araim-reference-check");
assert_eq!(v["vectors_checked"], 2);
assert_eq!(v["acceptance_vectors"], 1);
assert_eq!(v["acceptance_tolerance_m"], 5e-2);
assert_eq!(v["acceptance_met"], true);
assert!(summary.contains("araim-reference-check"));
let one = AraimReferenceCheckScenario {
vector: "milestone3-annex-a-ix".to_string(),
};
let (json, _) = one.run_json().expect("single vector runs");
let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
assert_eq!(v["vectors_checked"], 1);
assert_eq!(v["vectors"][0]["id"], "milestone3-annex-a-ix");
let err = AraimReferenceCheckScenario {
vector: "nope".to_string(),
}
.run_json()
.expect_err("unknown vector");
assert!(err.contains("add-v3.1-appendix-d"), "{err}");
}
#[test]
fn every_reported_figure_carries_a_unit_and_a_provenance_class() {
let (json, _) = AraimReferenceCheckScenario::default()
.run_json()
.expect("run");
let v: serde_json::Value = serde_json::from_str(&json).expect("valid JSON");
let units = v["units"].as_object().expect("a units block");
assert!(!units.is_empty());
for (field, meta) in units {
assert!(meta["unit"].is_string(), "{field} has no unit");
assert!(
meta["provenance"].is_string(),
"{field} has no provenance class"
);
}
let described: std::collections::HashSet<&str> = units.keys().map(|k| k.as_str()).collect();
for (key, value) in v.as_object().expect("an object") {
if value.is_number() {
assert!(described.contains(key.as_str()), "{key} has no unit entry");
}
}
for (key, value) in v["vectors"][0].as_object().expect("a vector row") {
if value.is_number() {
let path = format!("vectors[].{key}");
assert!(
described.contains(path.as_str()),
"{path} has no unit entry"
);
if key.contains("published") {
assert_eq!(units[&path]["provenance"], "published", "{path}");
}
}
}
for (key, value) in v["vectors"][0]["constellation_modes"][0]
.as_object()
.expect("a mode row")
{
if value.is_number() {
let path = format!("vectors[].constellation_modes[].{key}");
assert!(
described.contains(path.as_str()),
"{path} has no unit entry"
);
}
}
}
#[test]
fn the_returned_vpl_actually_meets_the_budget_it_was_solved_against() {
for v in published_vectors() {
let r = araim_reference_protection_levels(&v.case).expect("runs");
assert!(
r.achieved_risk_vert <= r.allocated_risk_vert * (1.0 + 1e-9),
"{}: achieved {} exceeds allocated {}",
v.id,
r.achieved_risk_vert,
r.allocated_risk_vert
);
assert!(r.vpl_m.is_finite() && r.vpl_m > 0.0);
assert!(r.hpl_m.is_finite() && r.hpl_m > 0.0);
}
}
}