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>,
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// `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.
|
||||
/// 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 {
|
||||
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() {
|
||||
0 => String::new(),
|
||||
1 => "DCT".to_owned(),
|
||||
|
||||
@@ -23,6 +23,14 @@ use graph::{EdgeData, NodeData, RouteGraph};
|
||||
const CONNECT_MAX_NM: f64 = 100.0;
|
||||
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.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Leg {
|
||||
@@ -412,12 +420,20 @@ pub fn airway_path(
|
||||
|e| e.weight().dist_nm,
|
||||
|n| rg.g[n].pos.distance_nm(&target),
|
||||
);
|
||||
let Some((_, path)) = result else {
|
||||
let Some((total, path)) = result else {
|
||||
return Ok(None);
|
||||
};
|
||||
if path.len() < 2 {
|
||||
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);
|
||||
for pair in path.windows(2) {
|
||||
let (x, y) = (pair[0], pair[1]);
|
||||
|
||||
Reference in New Issue
Block a user