fix(routing): kill globe-spanning splices + strip computer-nav fixes
Two correctness fixes exposed by a coverage batch where "1 ROUTE130" near-misses turned out to be catastrophically broken routes (Paris-> Frankfurt routed via Alaska) hidden by IFPS error-masking: - airway_path now rejects a spliced sub-path that balloons past SPLICE_MAX_RATIO x the direct distance. At a fragmented FL the graph could route two nearby fixes the long way around the whole network; that garbage then showed only 1 (masked) error and beat sane routes in the monotonic best-tracking. - route_item15 strips computer-navigation fixes (unnamed ARINC waypoints whose ident carries a digit, e.g. GT27A) proactively, collapsing same-airway legs, so IFPS never sees a ROUTE130 designator it would reject and stop evaluating at. Net: coverage numbers are now honest (masked garbage no longer counts as a near-miss); the real blocker is RAD PROF204/205 on upper/FRA routes. 49 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -58,11 +58,33 @@ pub struct DiscoverResult {
|
|||||||
pub log: Vec<String>,
|
pub log: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A computer-navigation fix: an unnamed ARINC waypoint our navdata carries on an
|
||||||
|
/// airway (e.g. `GT27A`, `BS25B`). Their idents contain a digit; real ICAO enroute
|
||||||
|
/// points are all-letter 5LNCs (`SOVOS`) or letter navaids (`EPL`). IFPS rejects
|
||||||
|
/// these designators (`ROUTE130`) because the airway already implies them.
|
||||||
|
fn is_computer_fix(ident: &str) -> bool {
|
||||||
|
ident.chars().any(|c| c.is_ascii_digit())
|
||||||
|
}
|
||||||
|
|
||||||
/// ICAO item-15 (enroute) string for a route: start at the SID exit fix, then
|
/// ICAO item-15 (enroute) string for a route: start at the SID exit fix, then
|
||||||
/// `airway to` for each enroute leg, ending at the STAR entry fix. The leading
|
/// `airway to` for each enroute leg, ending at the STAR entry fix. The leading
|
||||||
/// SID and trailing STAR (first/last leg) are omitted — IFPS derives them.
|
/// SID and trailing STAR (first/last leg) are omitted — IFPS derives them.
|
||||||
|
/// Computer-nav fixes are stripped (collapsing same-airway legs) so we never file
|
||||||
|
/// a designator IFPS would reject.
|
||||||
pub fn route_item15(route: &Route) -> String {
|
pub fn route_item15(route: &Route) -> String {
|
||||||
let legs = &route.legs;
|
let cn: std::collections::HashSet<String> = route
|
||||||
|
.legs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|l| [l.from.clone(), l.to.clone()])
|
||||||
|
.filter(|id| is_computer_fix(id))
|
||||||
|
.collect();
|
||||||
|
let collapsed;
|
||||||
|
let legs: &[Leg] = if cn.is_empty() {
|
||||||
|
&route.legs
|
||||||
|
} else {
|
||||||
|
collapsed = drop_designators(&route.legs, &cn);
|
||||||
|
&collapsed
|
||||||
|
};
|
||||||
match legs.len() {
|
match legs.len() {
|
||||||
0 => String::new(),
|
0 => String::new(),
|
||||||
1 => "DCT".to_owned(),
|
1 => "DCT".to_owned(),
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ use graph::{EdgeData, NodeData, RouteGraph};
|
|||||||
const CONNECT_MAX_NM: f64 = 100.0;
|
const CONNECT_MAX_NM: f64 = 100.0;
|
||||||
const CONNECT_K: usize = 30;
|
const CONNECT_K: usize = 30;
|
||||||
|
|
||||||
|
/// A spliced airway sub-path is rejected if it exceeds this multiple of the direct
|
||||||
|
/// distance between its endpoints (guards against a fragmented FL graph routing
|
||||||
|
/// two nearby fixes the long way around the network).
|
||||||
|
const SPLICE_MAX_RATIO: f64 = 3.0;
|
||||||
|
/// Floor for the splice bound so a short splice (a few nm direct) still has room
|
||||||
|
/// to route around terminal airspace.
|
||||||
|
const SPLICE_MIN_NM: f64 = 40.0;
|
||||||
|
|
||||||
/// One leg of a computed route.
|
/// One leg of a computed route.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Leg {
|
pub struct Leg {
|
||||||
@@ -412,12 +420,20 @@ pub fn airway_path(
|
|||||||
|e| e.weight().dist_nm,
|
|e| e.weight().dist_nm,
|
||||||
|n| rg.g[n].pos.distance_nm(&target),
|
|n| rg.g[n].pos.distance_nm(&target),
|
||||||
);
|
);
|
||||||
let Some((_, path)) = result else {
|
let Some((total, path)) = result else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
if path.len() < 2 {
|
if path.len() < 2 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
// Reject a splice that balloons far beyond the direct distance: at a fragmented
|
||||||
|
// FL the airway graph can force A* to wander across the network (even around
|
||||||
|
// the globe) to connect two nearby fixes. Such a path is never the real fix —
|
||||||
|
// better to leave the original leg for another repair than file garbage.
|
||||||
|
let direct = rg.g[a].pos.distance_nm(&target);
|
||||||
|
if total > SPLICE_MAX_RATIO * direct.max(SPLICE_MIN_NM) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
let mut legs = Vec::with_capacity(path.len() - 1);
|
let mut legs = Vec::with_capacity(path.len() - 1);
|
||||||
for pair in path.windows(2) {
|
for pair in path.windows(2) {
|
||||||
let (x, y) = (pair[0], pair[1]);
|
let (x, y) = (pair[0], pair[1]);
|
||||||
|
|||||||
Reference in New Issue
Block a user