Have you ever wondered how a launched missile guides itself to its target?
In this short tutorial, I would like to explain the basics behind navigation and guidance algorithms.
The basic theory of navigation and guidance can be summarized as follows:
- An interceptor must try to keep the LOS (line of sight) angle to the target constant.
- An interceptor must have sufficient kinetic energy to reach the target.
I'll walk through three small Python simulations, in increasing order of realism: CBDR (the geometric intuition), Proportional Navigation in 2D (true proportional navigation in a plane), and Proportional Navigation in 3D (the same law generalized to 3D with vectors)
Sailors know CBDR as a warning sign: if another vessel's bearing from your own ship stays constant while the range closes, you are on a collision course.
CBDR.py turns that warning into a guidance rule — instead of avoiding the constant bearing, the interceptor deliberately holds it.
We know the equation of a straight line:
At first, the interceptor tries to determine the straight line passing through the target and itself. It does it by determining the two parameters of a straight line: the slope
m = (target_coordinates_xy[1] - interceptor_coordinates_xy[1]) / (target_coordinates_xy[0] - interceptor_coordinates_xy[0])
d_0 = target_coordinates_xy[1] - m * target_coordinates_xy[0]That slope
d_1 = d_0 + target_velocity_xy[1]*dt - m*target_velocity_xy[0]*dtThen the interceptor asks: where on that line can it land, given it can only travel a distance of
a = m*m + 1
b = 2*m*d_1 - 2*m*interceptor_coordinates_xy[1] - 2*interceptor_coordinates_xy[0]
c = (d_1 - interceptor_coordinates_xy[1])**2 + interceptor_coordinates_xy[0]**2 - (interceptor_velocity_magnitude*dt)**2Solving the quadratic, two roots (
Why this works and why it's just a teaching tool rather than a real guidance law: By construction, the interceptor is forced onto the constant-bearing line every single step. There's no sensor, no rate estimate, no control loop - pure geometry. It's a clean way to demonstrate the core idea (constant LOS angle results in collision) before introducing a law that a real seeker could actually implement using only what it can measure: range rate and LOS angle rate.
Real interceptors can't solve a line-circle intersection against a target's future position, they only know their own LOS angle to the target and how fast it's changing. True Proportional Navigation turns that limited information into a steering command:
Where:
-
$\lambda$ — the LOS angle, the bearing from interceptor to target -
$\dot\lambda$ — how fast that bearing is rotating (this is what we're trying to drive to zero) -
$V_c$ — closing velocity, how fast the range is shrinking -
$N$ — the navigation constant (a gain) -
$a_n$ — commanded acceleration, applied perpendicular to the interceptor's velocity
The intuition is that if the LOS is changing, you're not on a collision course, so turn harder in proportion to both how fast the bearing is rotating and how fast you're closing.
In ProNav2D.py, each of these is computed directly from position history, so nothing about the target's future path is assumed:
lambda_los = math.atan2(target_coordinates_xy[1] - interceptor_coordinates_xy[1], target_coordinates_xy[0] - interceptor_coordinates_xy[0])
V_c = -(distance - prev_distance) / dt
angle_diff = (lambda_los - prev_lambda_los + math.pi) % (2 * math.pi) - math.pi
lambda_dot = angle_diff / dt
a_n = N * V_c * lambda_dot
prev_lambda_los = lambda_los
prev_distance = distance(The % (2*math.pi) - math.pi wrap keeps the angle difference in
That scalar
theta_V = math.atan2(interceptor_velocity_xy[1], interceptor_velocity_xy[0])
theta_a_n = theta_V + math.pi / 2
interceptor_velocity_xy[0] += a_n * math.cos(theta_a_n) * dt
interceptor_velocity_xy[1] += a_n * math.sin(theta_a_n) * dtThe target itself isn't flying straight either. This also demonstrates PN's robustness against a maneuvering target, which is exactly where CBDR's rigid geometric solution would fall apart.
Missiles don't fly in a plane, so the last step is generalizing the scalar PN law to full 3D. The trick is that
since
where
The commanded acceleration follows the same cross-product pattern, replacing the scalar multiplication
The implementation in ProNav3D is as follows:
omega_xyz_times_RdotR = cross3D(R_xyz, Vr_xyz)
# or RdotR = math.hypot(R_xyz[0], R_xyz[1], R_xyz[2])**2
RdotR = dot3D(R_xyz, R_xyz)
omega_xyz[0] = omega_xyz_times_RdotR[0] / RdotR
omega_xyz[1] = omega_xyz_times_RdotR[1] / RdotR
omega_xyz[2] = omega_xyz_times_RdotR[2] / RdotR
a_xyz_divide_N = cross3D(Vr_xyz, omega_xyz)
a_xyz = [a_xyz_divide_N[0] * N, a_xyz_divide_N[1] * N, a_xyz_divide_N[2] * N]This single vector a_xyz automatically comes out perpendicular to the relative velocity, so just like in 2D, it only needs to be added to the interceptor's velocity and then rescaled back to constant speed:
interceptor_velocity_xyz[0] += a_xyz[0] * dt
interceptor_velocity_xyz[1] += a_xyz[1] * dt
interceptor_velocity_xyz[2] += a_xyz[2] * dtNo separate "which way is normal" trigonometry is needed here, because the cross product handles direction and magnitude together. That's the real payoff of moving to vector notation. The target in this version also maneuvers in 3D, weaving its heading in the y–z plane while flying forward in x, and you can watch the interceptor's LOS line (the dashed line in the animation) staying roughly fixed in direction as the range collapses. The same CBDR condition from part 1, now emerging as a consequence of the PN law rather than being imposed directly.
- CBDR shows why constant bearing implies collision — a geometric proof, not a controllable guidance law.
- ProNav2D shows how a real seeker, using only LOS angle and range measurements, can enforce that same condition through feedback.
- ProNav3D shows that the 2D law isn't a special case — it's a vector identity that extends cleanly once you replace scalar bearing rate with an angular velocity vector.
Same principle throughout: null the LOS rotation rate, and you're on a collision course.



