FMS Lateral Navigation (avionics/flight-management/lateral-navigation)
Use when the task is the lateral track guidance math of a flight
management system between the flight plan legs: the great-circle
geometry from the current position to the next waypoint, the deviation
of the aircraft from the active leg, and the turn that carries it
through the waypoint onto the next leg. This leaf implements the LNAV
guidance quantities in pure Python, stdlib only: track and distance to
the waypoint, cross-track error, track angle error, intercept heading,
turn anticipation distance, and the fly-by versus fly-over transition
verdict. It is the lateral counterpart of the sibling vertical-
navigation leaf (which owns the VNAV descent path) and consumes legs
built by flight-planning; speed and cost policy come from performance-
computation.
Domain quick reference
- Spherical earth: great-circle formulas on a sphere of radius
R = 6371000 m, inputs in radians; all angles are radians, distances
meters, speed m/s, bank angle degrees.
- Initial great-circle track from A to B: track = atan2(sin(dLon) *
cos(latB), cos(latA)*sin(latB) - sin(latA)*cos(latB)cos(dLon)),
normalized to [0, 2pi). The track of a leg between two points on the
same parallel is not the parallel direction: (50N, 0E) to (50N, 10E)
starts at 86.17 deg because the arc bulges toward the pole.
- Distance: d = R * acos(sin(latA)*sin(latB) + cos(latA)cos(latB)
cos(dLon)), with the acos argument guarded to [-1, 1]; identical
points give 0 m while the direction functions raise ValueError.
- Cross-track error: xtk = asin(sin(d_AP/R) * sin(track_AB -
track_AP)), returned as meters with a sign element. The equation as
specified is positive when the position bears LEFT of the outbound
leg (bearing from the leg start to the position smaller than the leg
track angle); a display that defines positive = right of track
negates the value.
- Along-track: atd = acos(cos(d_AP/R) / cos(xtk)) * R measured from A,
projected with the spherical law of cosines; the distance to go to
the waypoint B is max(0, leg_length - atd).
- Track angle error: tke = wrap(track_desired - track_current) to
[-pi, pi), positive when a right turn (increasing track) closes the
error. wrap maps any angle into [-pi, pi).
- Intercept: with tke beyond the fixed intercept angle (module
constant 30 deg), the guidance heading is track_desired -
sign(tke) * limit, recapturing the track at the fixed intercept
angle from the closing side; inside the limit the aircraft holds the
desired track.
- Turn anticipation: the turn at a fly-by waypoint starts d_ant before
the waypoint, d_ant = R_turn * tan(|delta_track| / 2) with
R_turn = v^2 / (g * tan(bank)); module default bank 25 deg. A zero
track change is flown fly-over: d_ant = 0, turn at the waypoint.
- DO-178C frames the FMS function development context; the relations
above are standard spherical trigonometry, summary-only.
Workflow
- Take the active leg endpoints A, B and the aircraft position P from
the flight plan (flight-planning builds the legs).
- Get the guidance track and the leg length with
great_circle_track and great_circle_distance.
- Measure the deviation from the leg with cross_track_error (meters
plus sign) and the position along it with along_track_distance
(distance to go to the waypoint B).
- Compare the aircraft track against the leg with track_angle_error
and get the capture course with intercept_heading when the aircraft
has drifted off the leg.
- Size the turn into the next leg with turn_anticipation_distance
from the true airspeed and the track change at the waypoint, then
classify the transition with waypoint_transition (fly-by versus
fly-over and the turn start point).
- Collect the quantities in the lnav_guidance summary dict for the
guidance consumer.
- Confirm the deterministic checks with the contract test
scripts/test_lateral_navigation.py.
Worked example
Leg from A (50N, 0E) to B (50N, 10E), aircraft at P (51N, 5E) tracking
040 deg at 90 m/s, track change 30 deg at B, bank 25 deg.
- great_circle_track(A, B) = 1.5039 rad = 86.166 deg. The leg is not
flown along the 50N parallel: the great circle bulges north, so the
initial course is 3.83 deg north of east (the parallel shortcut
10 deg * cos(50) * R = 714.7 km would hold only for a rhumb line).
- great_circle_distance(A, B) = 714,214 m (714.2 km).
- cross_track_error at P: +99,239 m with sign +1. P bears left of the
outbound leg (bearing A to P is 70.7 deg, less than the 86.2 deg leg
track), and the specified equation is positive there. The mirror
point (49N, 5E) on the right side gives -123,151 m, sign -1; the
magnitudes differ because the great-circle arc runs north of the
50N parallel, closer to 51N than to 49N.
- along_track_distance to B from P: 357,107 m (357.1 km).
- track_angle_error: tke = wrap(86.166 - 40) = +46.166 deg, a right
turn of 46.2 deg onto the leg. intercept_heading returns 56.166 deg
(86.166 - 30), the fixed-angle capture course.
- turn_anticipation_distance(90, 30 deg, 25 deg): turn radius
90^2 / (9.80665 * tan 25 deg) = 1771.3 m and d_ant = 1771.3 *
tan(15 deg) = 474.6 m. waypoint_transition classifies the leg as
fly_by with the turn starting 474.6 m before B; a straight
continuation (0 deg change) would be fly_over with the turn at B.
Verification
- Confirm great_circle_distance(A, B) returns 714,214 m and
great_circle_track(A, B) returns 1.5039 rad (86.166 deg).
- Confirm cross_track_error at (51N, 5E) returns +99,239 m with sign
+1 and the mirror (49N, 5E) the opposite sign, and that on-track
points (the waypoint B, points on the same meridian) return zero.
- Confirm along_track_distance returns 357,107 m at P, the full leg
length at A, and zero at or beyond B.
- Confirm intercept_heading steers toward the leg: 56.166 deg from a
40 deg current track, 116.166 deg from 140 deg, and the desired
track itself when the error is within the 30 deg limit.
- Confirm turn_anticipation_distance(90, 30 deg, 25 deg) equals
474.6 m and scales with the square of speed.
- Confirm every out-of-range latitude, non-finite value, non-positive
speed, bank outside (0, 90) deg, |delta_track| at or beyond pi, and
identical leg endpoint raises ValueError.
- Run the contract test offline: python3
scripts/test_lateral_navigation.py (33 tests, deterministic).
Related leaves
- avionics/flight-management/flight-planning: builds and checks the
flight plan legs this leaf guides along.
- avionics/flight-management/vertical-navigation: the VNAV descent
path, the vertical counterpart of this lateral leaf.
- avionics/flight-management/performance-computation: ECON speed and
cost policy that set the speed input for the turn anticipation.
Pitfalls
- Taking the rhumb-line shortcut: a leg along a parallel is not flown
along the parallel - (50N, 0E) to (50N, 10E) opens at 86.17 deg
because the great circle bulges toward the pole, and the parallel
distance 10 deg * cos(50) * R = 714.7 km is not the 714,214 m
great-circle distance.
- Misreading the cross-track sign: the specified equation is positive
when the position bears LEFT of the outbound leg, so a display that
defines positive as right of track must negate the value; magnitudes
are also asymmetric on mirror points (99,239 m left at 51N versus
123,151 m right at 49N) because the arc runs closer to 51N.
- Mixing units: distances are meters, angles radians, speed m/s, and
only the bank angle is degrees - a bank in radians or a latitude in
degrees silently corrupts the turn radius and track formulas.
- Holding the desired track outside the intercept limit: with the track
angle error beyond the fixed 30 deg intercept angle the guidance
captures at track_desired - sign(tke) * 30 deg, not at the desired
track itself, and tke must be wrapped to [-pi, pi) first.
- Assuming every waypoint is a fly-by: a zero track change is flown
fly-over with d_ant = 0 and the turn at the waypoint; anticipation
only applies when the track actually changes, with
d_ant = R_turn * tan(|delta_track| / 2) growing with the square of
speed.
- Forgetting the identical-point rule: great_circle_distance returns
0 m for identical endpoints but the direction functions raise
ValueError there, and out-of-range latitudes, non-positive speed,
bank outside (0, 90) deg, or |delta_track| at or beyond pi also
raise - check leg validity before guiding on it.
Behavior contract (gate 3)
Run the deterministic contract test (stdlib unittest, offline):
python3 scripts/test_lateral_navigation.py
The test covers the worked great-circle leg (track 86.166 deg, 714.2
km), track normalization and reversal on symmetric legs, known
distances (quarter and half equator, meridian degrees), the cross-track
sign and magnitude at (51N, 5E) and its mirror, zero cross-track on
leg, the along-track distance to go at the leg start, midpoint and
beyond the waypoint, track angle error wrapping, the fixed-angle
intercept capture both sides of the track and across 0 deg, the turn
anticipation contract at 90 m/s with a 30 deg change at 25 deg bank,
the fly-by versus fly-over classification, the lnav_guidance summary
consistency, and ValueError rejection of non-physical inputs.
Compliance
- Standards referenced, not reproduced: DO-178C (RTCA) frames the
development of flight management functions; the great-circle
relations above are standard spherical trigonometry, summary-only
per standards-map.yaml.
- compliance: STANDARDS-REF, gated: false.
1---2name: lateral-navigation3description: Use when you must compute the lateral navigation (LNAV) guidance quantities of a flight management system between and along the flight plan legs: derive the great-circle track angle and distance from the current position to the next waypoint, determine the cross-track error to the active leg with its sign, compute the track angle error and the intercept heading that recaptures the desired track at a fixed intercept angle, size the turn anticipation distance before the fly-by waypoint from the speed and bank angle, and judge the along-track distance remaining on the leg. Produces the leg track and distance, the cross-track error, the intercept heading, and the fly-by versus fly-over transition point that gate FMS lateral guidance. Trigger: lateral navigation, lnav, cross-track error, track angle error, great-circle track, turn anticipation, fly-by waypoint, fly-over waypoint, intercept heading, fms lateral guidance.4license: Apache-2.05---67# FMS Lateral Navigation (avionics/flight-management/lateral-navigation)89Use when the task is the lateral track guidance math of a flight10management system between the flight plan legs: the great-circle11geometry from the current position to the next waypoint, the deviation12of the aircraft from the active leg, and the turn that carries it13through the waypoint onto the next leg. This leaf implements the LNAV14guidance quantities in pure Python, stdlib only: track and distance to15the waypoint, cross-track error, track angle error, intercept heading,16turn anticipation distance, and the fly-by versus fly-over transition17verdict. It is the lateral counterpart of the sibling vertical-18navigation leaf (which owns the VNAV descent path) and consumes legs19built by flight-planning; speed and cost policy come from performance-20computation.2122## Domain quick reference2324- Spherical earth: great-circle formulas on a sphere of radius25 R = 6371000 m, inputs in radians; all angles are radians, distances26 meters, speed m/s, bank angle degrees.27- Initial great-circle track from A to B: track = atan2(sin(dLon) *28 cos(latB), cos(latA)*sin(latB) - sin(latA)*cos(latB)*cos(dLon)),29 normalized to [0, 2*pi). The track of a leg between two points on the30 same parallel is not the parallel direction: (50N, 0E) to (50N, 10E)31 starts at 86.17 deg because the arc bulges toward the pole.32- Distance: d = R * acos(sin(latA)*sin(latB) + cos(latA)*cos(latB)*33 cos(dLon)), with the acos argument guarded to [-1, 1]; identical34 points give 0 m while the direction functions raise ValueError.35- Cross-track error: xtk = asin(sin(d_AP/R) * sin(track_AB -36 track_AP)), returned as meters with a sign element. The equation as37 specified is positive when the position bears LEFT of the outbound38 leg (bearing from the leg start to the position smaller than the leg39 track angle); a display that defines positive = right of track40 negates the value.41- Along-track: atd = acos(cos(d_AP/R) / cos(xtk)) * R measured from A,42 projected with the spherical law of cosines; the distance to go to43 the waypoint B is max(0, leg_length - atd).44- Track angle error: tke = wrap(track_desired - track_current) to45 [-pi, pi), positive when a right turn (increasing track) closes the46 error. wrap maps any angle into [-pi, pi).47- Intercept: with tke beyond the fixed intercept angle (module48 constant 30 deg), the guidance heading is track_desired -49 sign(tke) * limit, recapturing the track at the fixed intercept50 angle from the closing side; inside the limit the aircraft holds the51 desired track.52- Turn anticipation: the turn at a fly-by waypoint starts d_ant before53 the waypoint, d_ant = R_turn * tan(|delta_track| / 2) with54 R_turn = v^2 / (g * tan(bank)); module default bank 25 deg. A zero55 track change is flown fly-over: d_ant = 0, turn at the waypoint.56- DO-178C frames the FMS function development context; the relations57 above are standard spherical trigonometry, summary-only.5859## Workflow60611. Take the active leg endpoints A, B and the aircraft position P from62 the flight plan (flight-planning builds the legs).632. Get the guidance track and the leg length with64 great_circle_track and great_circle_distance.653. Measure the deviation from the leg with cross_track_error (meters66 plus sign) and the position along it with along_track_distance67 (distance to go to the waypoint B).684. Compare the aircraft track against the leg with track_angle_error69 and get the capture course with intercept_heading when the aircraft70 has drifted off the leg.715. Size the turn into the next leg with turn_anticipation_distance72 from the true airspeed and the track change at the waypoint, then73 classify the transition with waypoint_transition (fly-by versus74 fly-over and the turn start point).756. Collect the quantities in the lnav_guidance summary dict for the76 guidance consumer.777. Confirm the deterministic checks with the contract test78 scripts/test_lateral_navigation.py.7980## Worked example8182Leg from A (50N, 0E) to B (50N, 10E), aircraft at P (51N, 5E) tracking83040 deg at 90 m/s, track change 30 deg at B, bank 25 deg.8485- great_circle_track(A, B) = 1.5039 rad = 86.166 deg. The leg is not86 flown along the 50N parallel: the great circle bulges north, so the87 initial course is 3.83 deg north of east (the parallel shortcut88 10 deg * cos(50) * R = 714.7 km would hold only for a rhumb line).89- great_circle_distance(A, B) = 714,214 m (714.2 km).90- cross_track_error at P: +99,239 m with sign +1. P bears left of the91 outbound leg (bearing A to P is 70.7 deg, less than the 86.2 deg leg92 track), and the specified equation is positive there. The mirror93 point (49N, 5E) on the right side gives -123,151 m, sign -1; the94 magnitudes differ because the great-circle arc runs north of the95 50N parallel, closer to 51N than to 49N.96- along_track_distance to B from P: 357,107 m (357.1 km).97- track_angle_error: tke = wrap(86.166 - 40) = +46.166 deg, a right98 turn of 46.2 deg onto the leg. intercept_heading returns 56.166 deg99 (86.166 - 30), the fixed-angle capture course.100- turn_anticipation_distance(90, 30 deg, 25 deg): turn radius101 90^2 / (9.80665 * tan 25 deg) = 1771.3 m and d_ant = 1771.3 *102 tan(15 deg) = 474.6 m. waypoint_transition classifies the leg as103 fly_by with the turn starting 474.6 m before B; a straight104 continuation (0 deg change) would be fly_over with the turn at B.105106## Verification107108- Confirm great_circle_distance(A, B) returns 714,214 m and109 great_circle_track(A, B) returns 1.5039 rad (86.166 deg).110- Confirm cross_track_error at (51N, 5E) returns +99,239 m with sign111 +1 and the mirror (49N, 5E) the opposite sign, and that on-track112 points (the waypoint B, points on the same meridian) return zero.113- Confirm along_track_distance returns 357,107 m at P, the full leg114 length at A, and zero at or beyond B.115- Confirm intercept_heading steers toward the leg: 56.166 deg from a116 40 deg current track, 116.166 deg from 140 deg, and the desired117 track itself when the error is within the 30 deg limit.118- Confirm turn_anticipation_distance(90, 30 deg, 25 deg) equals119 474.6 m and scales with the square of speed.120- Confirm every out-of-range latitude, non-finite value, non-positive121 speed, bank outside (0, 90) deg, |delta_track| at or beyond pi, and122 identical leg endpoint raises ValueError.123- Run the contract test offline: python3124 scripts/test_lateral_navigation.py (33 tests, deterministic).125126## Related leaves127128- avionics/flight-management/flight-planning: builds and checks the129 flight plan legs this leaf guides along.130- avionics/flight-management/vertical-navigation: the VNAV descent131 path, the vertical counterpart of this lateral leaf.132- avionics/flight-management/performance-computation: ECON speed and133 cost policy that set the speed input for the turn anticipation.134135## Pitfalls136137- Taking the rhumb-line shortcut: a leg along a parallel is not flown138 along the parallel - (50N, 0E) to (50N, 10E) opens at 86.17 deg139 because the great circle bulges toward the pole, and the parallel140 distance 10 deg * cos(50) * R = 714.7 km is not the 714,214 m141 great-circle distance.142- Misreading the cross-track sign: the specified equation is positive143 when the position bears LEFT of the outbound leg, so a display that144 defines positive as right of track must negate the value; magnitudes145 are also asymmetric on mirror points (99,239 m left at 51N versus146 123,151 m right at 49N) because the arc runs closer to 51N.147- Mixing units: distances are meters, angles radians, speed m/s, and148 only the bank angle is degrees - a bank in radians or a latitude in149 degrees silently corrupts the turn radius and track formulas.150- Holding the desired track outside the intercept limit: with the track151 angle error beyond the fixed 30 deg intercept angle the guidance152 captures at track_desired - sign(tke) * 30 deg, not at the desired153 track itself, and tke must be wrapped to [-pi, pi) first.154- Assuming every waypoint is a fly-by: a zero track change is flown155 fly-over with d_ant = 0 and the turn at the waypoint; anticipation156 only applies when the track actually changes, with157 d_ant = R_turn * tan(|delta_track| / 2) growing with the square of158 speed.159- Forgetting the identical-point rule: great_circle_distance returns160 0 m for identical endpoints but the direction functions raise161 ValueError there, and out-of-range latitudes, non-positive speed,162 bank outside (0, 90) deg, or |delta_track| at or beyond pi also163 raise - check leg validity before guiding on it.164165## Behavior contract (gate 3)166167Run the deterministic contract test (stdlib unittest, offline):168169 python3 scripts/test_lateral_navigation.py170171The test covers the worked great-circle leg (track 86.166 deg, 714.2172km), track normalization and reversal on symmetric legs, known173distances (quarter and half equator, meridian degrees), the cross-track174sign and magnitude at (51N, 5E) and its mirror, zero cross-track on175leg, the along-track distance to go at the leg start, midpoint and176beyond the waypoint, track angle error wrapping, the fixed-angle177intercept capture both sides of the track and across 0 deg, the turn178anticipation contract at 90 m/s with a 30 deg change at 25 deg bank,179the fly-by versus fly-over classification, the lnav_guidance summary180consistency, and ValueError rejection of non-physical inputs.181182## Compliance183184- Standards referenced, not reproduced: DO-178C (RTCA) frames the185 development of flight management functions; the great-circle186 relations above are standard spherical trigonometry, summary-only187 per standards-map.yaml.188- compliance: STANDARDS-REF, gated: false.