1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
// SPDX-License-Identifier: Apache-2.0
//! Detection-miss → integrity-impact mapping (context-aware alert limits).
//!
//! A resilience detector (spoof / jamming / RAIM) that *misses* a fault leaves an
//! undetected position bias `b` in the navigation solution. This module maps that
//! miss into the language the integrity/safety community uses: the bias inflates the
//! effective position error, and — held against a **context-specific alert limit**
//! (open-sky vs urban, horizontal HAL vs vertical VAL) — it lands in one of the
//! Stanford regions (available / unavailable / misleading / hazardous).
//!
//! It composes the shipped, externally-anchored RAIM core
//! ([`crate::raim::classify_stanford`] / [`crate::raim::StanfordRegion`]); it adds no
//! new integrity mathematics, only the detection-miss → protection-level → alert-limit
//! bridge.
//!
//! ### Honesty
//! The protection level here is the *nominal* (no-fault) PL from the ARAIM engine; a
//! missed detection means the true error can reach `nominal_error + undetected_bias`
//! while the reported PL stays at nominal — exactly the **Misleading-Information**
//! risk a monitor exists to bound. This is a modelled mapping of a modelled bias; it
//! is not a certified integrity allocation.
use serde::Serialize;
use crate::raim::{classify_stanford, StanfordRegion};
/// A named operational context with its horizontal/vertical alert limits (m). Tighter
/// limits (urban / precision approach) make the same undetected bias more dangerous.
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct IntegrityContext {
pub name: String,
/// Horizontal alert limit HAL (m).
pub hal_m: f64,
/// Vertical alert limit VAL (m).
pub val_m: f64,
}
impl IntegrityContext {
/// Open-sky en-route-style limits (loose). Illustrative, not a certified ALERT
/// LIMIT allocation — the caller should supply the operation's real limits.
pub fn open_sky() -> Self {
IntegrityContext {
name: "open-sky (en-route, illustrative)".into(),
hal_m: 1852.0, // ~1 NM, RNP en-route order of magnitude
val_m: 50.0,
}
}
/// Urban / terminal-style limits (tight). Illustrative.
pub fn urban() -> Self {
IntegrityContext {
name: "urban / terminal (illustrative)".into(),
hal_m: 40.0,
val_m: 20.0,
}
}
}
/// The integrity impact of one detection miss, on one axis (horizontal or vertical).
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct AxisImpact {
/// `"horizontal"` or `"vertical"`.
pub axis: String,
/// Nominal (no-fault) error on this axis (m).
pub nominal_error_m: f64,
/// Undetected bias added by the missed detection (m).
pub undetected_bias_m: f64,
/// Effective error = nominal + bias (m).
pub effective_error_m: f64,
/// The nominal protection level on this axis (m) — unchanged by an *undetected*
/// fault (the monitor did not flag it), which is the whole danger.
pub protection_level_m: f64,
/// The alert limit for this axis in this context (m).
pub alert_limit_m: f64,
/// The Stanford region the effective error + nominal PL fall into.
pub region: StanfordRegion,
/// Signed margin to the alert limit (m): `alert_limit − effective_error`. Negative
/// means the alert limit is exceeded.
pub margin_to_al_m: f64,
}
/// The full context-aware integrity impact of a detection miss (both axes).
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct DetectionMissImpact {
pub context: IntegrityContext,
pub horizontal: AxisImpact,
pub vertical: AxisImpact,
/// True if either axis is hazardously misleading (HMI) — the unsafe outcome.
pub any_hazardous: bool,
/// True if either axis is misleading (MI or HMI) — an integrity event occurred.
pub any_misleading: bool,
pub caveat: String,
}
#[allow(clippy::too_many_arguments)]
fn axis_impact(
axis: &str,
nominal_error_m: f64,
undetected_bias_m: f64,
protection_level_m: f64,
alert_limit_m: f64,
) -> AxisImpact {
let effective = nominal_error_m + undetected_bias_m;
let region = classify_stanford(effective, protection_level_m, alert_limit_m);
AxisImpact {
axis: axis.to_string(),
nominal_error_m,
undetected_bias_m,
effective_error_m: effective,
protection_level_m,
alert_limit_m,
region,
margin_to_al_m: alert_limit_m - effective,
}
}
/// Map a detection miss to its context-aware integrity impact. `h_bias_m` / `v_bias_m`
/// are the undetected biases the missed detection leaves on each axis (m);
/// `hpl_m` / `vpl_m` are the nominal protection levels; nominal errors default the
/// no-fault truth on each axis.
#[allow(clippy::too_many_arguments)]
pub fn detection_miss_impact(
context: IntegrityContext,
h_nominal_error_m: f64,
v_nominal_error_m: f64,
h_bias_m: f64,
v_bias_m: f64,
hpl_m: f64,
vpl_m: f64,
) -> Result<DetectionMissImpact, String> {
for (n, v) in [
("h_nominal_error", h_nominal_error_m),
("v_nominal_error", v_nominal_error_m),
("h_bias", h_bias_m),
("v_bias", v_bias_m),
("hpl", hpl_m),
("vpl", vpl_m),
] {
if !v.is_finite() || v < 0.0 {
return Err(format!("{n} must be finite and non-negative"));
}
}
let horizontal = axis_impact(
"horizontal",
h_nominal_error_m,
h_bias_m,
hpl_m,
context.hal_m,
);
let vertical = axis_impact(
"vertical",
v_nominal_error_m,
v_bias_m,
vpl_m,
context.val_m,
);
let hazardous =
|r: &StanfordRegion| matches!(r, StanfordRegion::HazardouslyMisleadingInformation);
let misleading = |r: &StanfordRegion| {
matches!(
r,
StanfordRegion::MisleadingInformation
| StanfordRegion::HazardouslyMisleadingInformation
)
};
Ok(DetectionMissImpact {
any_hazardous: hazardous(&horizontal.region) || hazardous(&vertical.region),
any_misleading: misleading(&horizontal.region) || misleading(&vertical.region),
context,
horizontal,
vertical,
caveat: "MODELLED detection-miss → integrity mapping over the externally-anchored RAIM \
Stanford classification; alert limits are caller/context inputs (illustrative \
defaults provided), not a certified integrity allocation."
.into(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn small_bias_open_sky_is_available() {
// 5 m nominal error, a 10 m undetected bias (15 m effective), PL 30 m,
// open-sky HAL ~1852 m → PL bounds the inflated error and PL < AL → Available.
let r = detection_miss_impact(
IntegrityContext::open_sky(),
5.0,
3.0,
10.0,
4.0,
30.0,
25.0,
)
.unwrap();
assert_eq!(r.horizontal.region, StanfordRegion::Available);
assert!(!r.any_hazardous);
assert!(!r.any_misleading);
// effective horizontal error = 5 + 10 = 15.
assert!((r.horizontal.effective_error_m - 15.0).abs() < 1e-9);
assert!(r.horizontal.margin_to_al_m > 0.0);
}
#[test]
fn same_miss_in_urban_context_becomes_misleading_or_hazardous() {
// The SAME 30 m effective horizontal error, but urban HAL = 40 m and a PL that
// does NOT bound it (PL 20 < 30 effective): PL<error and error<AL → MI;
// push the bias higher and it crosses AL → HMI. Context changes the verdict.
let mi = detection_miss_impact(
IntegrityContext::urban(),
5.0,
2.0,
25.0, // effective h = 30
3.0,
20.0, // hpl < 30
18.0,
)
.unwrap();
assert_eq!(mi.horizontal.region, StanfordRegion::MisleadingInformation);
assert!(mi.any_misleading && !mi.any_hazardous);
let hmi = detection_miss_impact(
IntegrityContext::urban(),
5.0,
2.0,
50.0, // effective h = 55 > HAL 40
3.0,
20.0,
18.0,
)
.unwrap();
assert_eq!(
hmi.horizontal.region,
StanfordRegion::HazardouslyMisleadingInformation
);
assert!(hmi.any_hazardous && hmi.any_misleading);
assert!(
hmi.horizontal.margin_to_al_m < 0.0,
"AL exceeded → negative margin"
);
}
#[test]
fn conservative_pl_with_large_bias_is_unavailable_not_hazardous() {
// PL is large (300 m) and bounds the 55 m effective error, but PL > urban HAL
// 40 → SystemUnavailable (safe, just not usable) — the conservative outcome.
let r = detection_miss_impact(IntegrityContext::urban(), 5.0, 2.0, 50.0, 3.0, 300.0, 300.0)
.unwrap();
assert_eq!(r.horizontal.region, StanfordRegion::SystemUnavailable);
assert!(!r.any_hazardous);
}
#[test]
fn vertical_axis_is_classified_independently() {
// Horizontal safe, vertical hazardous (tight VAL): any_hazardous must catch it.
let r = detection_miss_impact(
IntegrityContext::urban(),
1.0,
5.0,
2.0, // h effective 3, well under HAL 40, PL bounds
40.0, // v effective 45 > VAL 20
10.0,
15.0, // vpl 15 < 45 → PL fails to bound
)
.unwrap();
assert_eq!(r.horizontal.region, StanfordRegion::Available);
assert_eq!(
r.vertical.region,
StanfordRegion::HazardouslyMisleadingInformation
);
assert!(r.any_hazardous);
}
#[test]
fn rejects_negative_or_nonfinite_inputs() {
assert!(
detection_miss_impact(IntegrityContext::urban(), -1.0, 0.0, 0.0, 0.0, 10.0, 10.0)
.is_err()
);
assert!(detection_miss_impact(
IntegrityContext::urban(),
0.0,
0.0,
f64::NAN,
0.0,
10.0,
10.0
)
.is_err());
}
#[test]
fn serializes_with_regions_and_caveat() {
let r = detection_miss_impact(
IntegrityContext::open_sky(),
5.0,
3.0,
10.0,
4.0,
30.0,
25.0,
)
.unwrap();
let json = serde_json::to_string(&r).unwrap();
assert!(json.contains("region"));
assert!(json.contains("MODELLED"));
assert!(json.contains("alert_limit_m"));
}
}