feat(rad): FRA engine phase 2b — mandatory arrival transitions + top-2 repair
Parse Annex 2B mandatory arrival routings and repair the best few
candidates (not just the lowest-error one):
- core::rad: MandatoryArr + RadData.mandatory_arr +
mandatory_arr_transitions(ades) -> allowed point-sequences.
- rad crate: parse_mandatory_arr() (VIA-clauses in NOT-AVBL-EXC rows,
scoped to ARR airports). via_sequences() now scans all parenthesised
transition groups (contain DCT/airway), not just contiguous "VIA (",
because sub-option markers separate them ("VIA i) (KARDU DCT KALMO)").
rad-tool `manarr [ADES]`. LIML now yields KARDU KALMO etc.
- discover: "manarr" candidates route to the transition's on-airway
anchor then splice the (often off-airway) transition points in via DCT.
- attempt_once repairs the top-2 candidates and keeps the best result:
the lowest-raw-error candidate isn't always the most repairable (a
mandate-satisfying route may carry more mechanical errors than a route
stuck on an unfixable mandatory).
Collapses the dense-FRA near-misses: EGLL-EDDM 7->1 err, EDDF-LIRF 3->1
(mandatory PROF205 resolved; residual is a single ROUTE49). Coverage 6/10,
no regression (passing pairs short-circuit on accept). 49 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+47
-6
@@ -10,7 +10,8 @@ use calamine::{open_workbook, Data, Reader, Xlsx};
|
||||
// The RAD data model lives in `core` (so routing/validation can use it without
|
||||
// pulling in `calamine`); this crate produces those types.
|
||||
pub use flightplanner_core::rad::{
|
||||
Area, DctKind, DctRestriction, ForbiddenSeg, FraEdge, FraPoint, LevelCap, MandatoryDep, RadData,
|
||||
Area, DctKind, DctRestriction, ForbiddenSeg, FraEdge, FraPoint, LevelCap, MandatoryArr,
|
||||
MandatoryDep, RadData,
|
||||
};
|
||||
|
||||
/// Parse the official EUROCONTROL "FRA Points" list (a separate `.xlsx` — see the
|
||||
@@ -119,9 +120,35 @@ pub fn parse(path: &str) -> Result<RadData> {
|
||||
fra_points: Vec::new(), // loaded separately via parse_fra_points
|
||||
forbidden_segs: parse_forbidden_segments(path)?,
|
||||
mandatory_dep: parse_mandatory_dep(path)?,
|
||||
mandatory_arr: parse_mandatory_arr(path)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Annex 2B — mandatory arrival routings. Rows whose utilization lists allowed
|
||||
/// `VIA (...)` transitions as exceptions to a `NOT AVBL FOR TFC` ban, scoped to
|
||||
/// one or more `ARR <airport>`s (e.g. LI2137: `ARR LIML VIA (KARDU DCT KALMO)`).
|
||||
/// v1 associates every VIA-sequence in a row with every ARR airport in it (the
|
||||
/// DEP gating is dropped — candidates are additive, so the oracle filters).
|
||||
pub fn parse_mandatory_arr(path: &str) -> Result<Vec<MandatoryArr>> {
|
||||
let Ok(rows) = rows(path, "Annex 2B") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for r in rows.iter().skip(1) {
|
||||
let up = cell(r, 8).to_uppercase().replace(['\r'], " ");
|
||||
if !up.contains("NOT AVBL FOR TFC") || !up.contains("ARR ") || !up.contains("VIA (") {
|
||||
continue;
|
||||
}
|
||||
let airports = apt_after(&up, "ARR");
|
||||
let allowed: Vec<Vec<String>> = via_sequences(&up).into_iter().filter(|s| !s.is_empty()).collect();
|
||||
if airports.is_empty() || allowed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.push(MandatoryArr { id: cell(r, 3), airports, allowed });
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Annex 3A DEP — mandatory departure routings (`COMPULSORY FOR TFC`). Extracts
|
||||
/// the allowed transition point-sequences from the `VIA (...)` clauses, chaining
|
||||
/// sub-fragments (`d. VIA (OPALE DCT KESAX)` + `ii) VIA (KESAX DCT DIMAL DCT
|
||||
@@ -151,14 +178,20 @@ pub fn parse_mandatory_dep(path: &str) -> Result<Vec<MandatoryDep>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The point-sequences inside every `VIA (...)` clause (balanced parens), dropping
|
||||
/// `DCT`/airways/keywords — just the ordered enroute points.
|
||||
/// Point-sequences from every parenthesised transition clause (balanced parens
|
||||
/// that contain a `DCT` or an airway token — which skips airport lists like
|
||||
/// `(LIMJ, LIRP)` and engine-type notes like `(J)`). We can't anchor on `VIA (`
|
||||
/// because sub-option markers separate them (`VIA i) (KARDU DCT KALMO)`).
|
||||
fn via_sequences(up: &str) -> Vec<Vec<String>> {
|
||||
let bytes = up.as_bytes();
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0;
|
||||
while let Some(p) = up[i..].find("VIA (") {
|
||||
let start = i + p + 5;
|
||||
while i < bytes.len() {
|
||||
if bytes[i] != b'(' {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
let start = i + 1;
|
||||
let mut depth = 1;
|
||||
let mut j = start;
|
||||
while j < bytes.len() && depth > 0 {
|
||||
@@ -170,7 +203,15 @@ fn via_sequences(up: &str) -> Vec<Vec<String>> {
|
||||
j += 1;
|
||||
}
|
||||
let inner = &up[start..j.saturating_sub(1)];
|
||||
out.extend(expand_via(inner));
|
||||
let is_transition = inner.split(|c: char| !c.is_ascii_alphanumeric()).any(|t| {
|
||||
t == "DCT"
|
||||
|| (t.len() >= 2
|
||||
&& t.chars().next().is_some_and(|c| c.is_ascii_alphabetic())
|
||||
&& t.chars().any(|c| c.is_ascii_digit()))
|
||||
});
|
||||
if is_transition {
|
||||
out.extend(expand_via(inner));
|
||||
}
|
||||
i = j;
|
||||
}
|
||||
out
|
||||
|
||||
+14
-1
@@ -186,7 +186,20 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => println!("usage: rad-tool sheets | dump | find | summary | fra [point] | caps [FROM TO] | frapts [file] [POINT] | forbidden [ADEP ADES FL] | mandep [ADEP]"),
|
||||
Some("manarr") => {
|
||||
// manarr [ADES] — mandatory arrival transitions (Annex 2B).
|
||||
let rad = flightplanner_rad::parse(&path)?;
|
||||
println!("Mandatory arrival routings (Annex 2B): {}", rad.mandatory_arr.len());
|
||||
if let Some(a) = args.get(2) {
|
||||
let a = a.to_uppercase();
|
||||
let seqs = rad.mandatory_arr_transitions(&a);
|
||||
println!("{a}: {} allowed transition sequences", seqs.len());
|
||||
for s in seqs.iter().take(40) {
|
||||
println!(" {}", s.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => println!("usage: rad-tool sheets | dump | find | summary | fra [point] | caps [FROM TO] | frapts [file] [POINT] | forbidden [ADEP ADES FL] | mandep [ADEP] | manarr [ADES]"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user