Simulate a Falcon 9 Hoverslam Landing

Build a 1D physics simulation of a Falcon 9 booster hoverslam landing.

Introduction

30 Second Summary

You've probably watched a SpaceX rocket land itself on a tiny drone ship in the middle of the ocean. What you might not know is that the booster has no choice but to hit the ground at exactly the right speed. It can't hover. If it misses the timing by even a fraction of a second, it either crashes or flies back up and runs out of fuel.

In this project, you will build a Python simulation that models a Falcon 9 booster falling from 3,000 meters and executing a precision "hoverslam" landing using real-world rocket parameters. You'll crash it, fail to land it, then derive the physics that makes it work.

What You'll Build

You run your simulation, watch the booster slam into the ground with no engine, fire the engine at the wrong time (twice), then finally nail a perfect landing at walking speed and generate publication-quality plots of the entire descent.

By the end of this project, you'll have:

  • A physics simulation that shows a 22-ton booster cratering at 800+ km/h with no engine, proving why landing is a hard problem.
  • A hoverslam ignition formula you derived from first-principles kinematics, producing a touchdown at under 3 m/s (walking speed).
  • A full-physics model with atmospheric drag and fuel mass loss that you can compare against the basic model to see how real-world forces change the landing profile.
  • Secret Mission: a real-time animated visualization that shows the booster descending and landing live on your screen.

Are there any prerequisites?

You should be comfortable with Python basics (functions, loops, and simple math). No physics background is required. The project explains every equation from scratch before you implement it.

Before We Start

To simulate a Falcon 9 landing, you need two Python libraries. NumPy handles the physics calculations, and Matplotlib visualizes the descent profiles as publication-quality plots.

In this step, you'll create your project folder, install both libraries, and set up your simulation file with real Falcon 9 Block 5 rocket parameters.

In this step, get ready to:
  • Create your project folder and open it in VS Code.
  • Install NumPy and Matplotlib.
  • Define the Falcon 9 parameters and verify they compute correctly.
Create your project folder

Every simulation project starts with a dedicated folder. You'll create one on your Desktop so it's easy to find.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir falcon9-landing
cd falcon9-landing
  • Open the folder in VS Code by running:
code .
  • Confirm the folder is open by checking the Explorer sidebar in VS Code. You should see FALCON9-LANDING as the workspace name.

VS Code didn't open?

If you see "command not found", open VS Code manually, then press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows) and type "Shell Command: Install 'code' command in PATH". Then try code . again.

help me open my project folder in VS Code

Install NumPy and Matplotlib

NumPy provides fast array math for the physics engine. Matplotlib renders the altitude and velocity plots you'll generate throughout the project.

  • Install both libraries by running this command in your terminal:
pip install numpy matplotlib

You should see output ending with a line starting with Successfully installed followed by package names and version numbers.

  • Verify both packages imported correctly by running:
python -c "import numpy; import matplotlib; print(numpy.__version__, matplotlib.__version__)"

You should see two version numbers printed without any errors.

Seeing ModuleNotFoundError?

Make sure you ran pip install numpy matplotlib in the same terminal session. If you have multiple Python installations, try pip3 install numpy matplotlib instead.

help me fix my NumPy or Matplotlib installation

Create the simulation file with Falcon 9 parameters

The simulation needs real rocket parameters to produce accurate results. You'll define the Falcon 9 Block 5 first stage specifications as constants at the top of your file.

  • In VS Code, create a new file called falcon9_sim.py by clicking File in the top menu bar, then New File.
  • Save it immediately with Cmd+S (macOS) or Ctrl+S (Windows) and name it falcon9_sim.py.
  • Add the following code to define all the simulation parameters:
import numpy as np
import matplotlib.pyplot as plt

# === Falcon 9 First Stage Parameters (Block 5) ===
DRY_MASS = 22_200        # kg - empty booster mass
FUEL_MASS = 5_000        # kg - landing fuel reserve
THRUST_MIN = 482_000     # N - single Merlin 1D at minimum throttle (57%)
ISP = 282                # s - specific impulse at sea level
DIAMETER = 3.7           # m - booster diameter
CD = 1.2                 # drag coefficient (cylinder approximation)
G = 9.81                 # m/s^2 - gravitational acceleration

# Derived constants
AREA = np.pi * (DIAMETER / 2) ** 2   # cross-section area m^2
RHO = 1.225                          # air density at sea level kg/m^3
EXHAUST_VEL = ISP * G                # effective exhaust velocity m/s
MASS_FLOW = THRUST_MIN / EXHAUST_VEL # fuel burn rate kg/s

# === Initial Conditions (start of landing burn phase) ===
ALT_START = 3000.0   # m - altitude at start of simulation
VEL_START = -200.0   # m/s - velocity (negative = falling)
DT = 0.01            # s - simulation time step

if __name__ == '__main__':
    print(f'AREA: {AREA:.2f} m^2')
    print(f'EXHAUST_VEL: {EXHAUST_VEL:.2f} m/s')
    print(f'MASS_FLOW: {MASS_FLOW:.2f} kg/s')

What does this code do?

  • The first section defines the physical properties of a Falcon 9 Block 5 first stage: its empty mass (22,200 kg), landing fuel reserve (5,000 kg), and the minimum thrust of a single Merlin 1D engine (482 kN at 57% throttle).
  • The derived constants compute values from those parameters. AREA is the booster's cross-section for drag calculations. EXHAUST_VEL converts specific impulse into a velocity. MASS_FLOW tells you how fast the engine burns through fuel.
  • The initial conditions set the starting scenario: the booster is at 3,000 meters altitude, already falling at 200 m/s downward.
  • The if __name__ == '__main__' block prints the derived values so you can confirm the math is correct.
  • Save falcon9_sim.py with Cmd+S (macOS) or Ctrl+S (Windows).

Before you run this, what do you think the MASS_FLOW value will be? Think about what dividing 482,000 N by the exhaust velocity gives you.

  • Run the file by entering this command in your terminal:
python falcon9_sim.py

You should see three lines printed:

AREA: 10.75 m^2
EXHAUST_VEL: 2766.42 m/s
MASS_FLOW: 174.22 kg/s

What do these numbers tell you?

The booster's cross-section is about 10.75 square meters. Its exhaust leaves the engine at 2,766 m/s. And it burns through 174 kg of fuel every second. That last number means your 5,000 kg fuel reserve lasts roughly 29 seconds of engine burn. Every second of timing matters.

Seeing a SyntaxError or NameError?

Check that your file is saved as falcon9_sim.py (not .txt). Make sure the import numpy as np line is at the very top of the file with no extra characters before it.

help me fix my falcon9_sim.py file

✔️ Awesome, I've got everything!

Great. Double check you've saved your file.

ⓧ I'd like to double check the full code

Here is the complete falcon9_sim.py file at this point:

import numpy as np
import matplotlib.pyplot as plt

# === Falcon 9 First Stage Parameters (Block 5) ===
DRY_MASS = 22_200        # kg - empty booster mass
FUEL_MASS = 5_000        # kg - landing fuel reserve
THRUST_MIN = 482_000     # N - single Merlin 1D at minimum throttle (57%)
ISP = 282                # s - specific impulse at sea level
DIAMETER = 3.7           # m - booster diameter
CD = 1.2                 # drag coefficient (cylinder approximation)
G = 9.81                 # m/s^2 - gravitational acceleration

# Derived constants
AREA = np.pi * (DIAMETER / 2) ** 2   # cross-section area m^2
RHO = 1.225                          # air density at sea level kg/m^3
EXHAUST_VEL = ISP * G                # effective exhaust velocity m/s
MASS_FLOW = THRUST_MIN / EXHAUST_VEL # fuel burn rate kg/s

# === Initial Conditions (start of landing burn phase) ===
ALT_START = 3000.0   # m - altitude at start of simulation
VEL_START = -200.0   # m/s - velocity (negative = falling)
DT = 0.01            # s - simulation time step

if __name__ == '__main__':
    print(f'AREA: {AREA:.2f} m^2')
    print(f'EXHAUST_VEL: {EXHAUST_VEL:.2f} m/s')
    print(f'MASS_FLOW: {MASS_FLOW:.2f} kg/s')

Your simulation file is ready with real Falcon 9 data. Next up, you'll use these parameters to model what happens when this booster falls with no engine at all.

Simulate Freefall

In the previous step, you set up your project and defined the real Falcon 9 parameters.

Before you can solve the landing problem, you need to understand the problem itself. A 22-ton booster falling from 3,000 meters under pure gravity accelerates relentlessly. In this step, you'll simulate what happens with no engine at all.

You'll implement Euler integration to step through time and track how altitude and velocity change under gravity alone. Then you'll visualize the crash with a two-panel plot.

In this step, get ready to:
  • Implement a freefall simulation using Euler numerical integration.
  • Visualize the crash with a publication-quality two-panel plot.
  • Observe the booster hitting the ground at over 800 km/h.
Implement the freefall simulation

Euler integration is the simplest way to simulate physics over time. At each tiny time step, you update velocity based on acceleration, then update position based on velocity.

  • In falcon9_sim.py, find the existing if __name__ == '__main__': block at the bottom of the file.
  • Delete the entire __main__ block (the if line and all three print statements).
  • In its place, add the simulate_freefall() function by pasting this code:
def simulate_freefall():
    """Simulate pure freefall - no engine, no drag. Just gravity."""
    alt = ALT_START
    vel = VEL_START
    t = 0.0

    times, alts, vels = [t], [alt], [vel]

    while alt > 0:
        vel += -G * DT
        alt += vel * DT
        t += DT
        times.append(t)
        alts.append(alt)
        vels.append(vel)

    return np.array(times), np.array(alts), np.array(vels)

What does this code do?

  • alt, vel, and t start at the initial conditions you defined earlier (3,000 m altitude, -200 m/s velocity).
  • vel += -G * DT applies gravity each time step. Gravity is the only force, so acceleration is always -9.81 m/s².
  • alt += vel * DT moves the booster based on its current velocity. This is the core of Euler integration.
  • The while alt > 0 loop runs until the booster reaches ground level, recording every data point along the way.

What is Euler integration?

Instead of solving the physics equations analytically (with calculus), Euler integration takes tiny steps through time. At each 0.01-second step, it pretends the current acceleration is constant, updates velocity, then updates position.

With small enough steps (and DT = 0.01 gives you 100 calculations per second), the approximation is excellent for this simulation.

Visualize the crash

Numbers alone don't convey the violence of an uncontrolled descent. A plot makes the physics visible at a glance.

  • Below the simulate_freefall() function, add the plot_freefall() function by pasting this code:
def plot_freefall(times, alts, vels):
    """Visualize the freefall crash."""
    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8), sharex=True)

    ax1.plot(times, alts, 'r-', linewidth=2)
    ax1.axhline(y=0, color='brown', linestyle='--', label='Ground')
    ax1.set_ylabel('Altitude (m)')
    ax1.set_title('Falcon 9 Booster in Freefall')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

    ax2.plot(times, vels, 'b-', linewidth=2)
    ax2.set_xlabel('Time (s)')
    ax2.set_ylabel('Velocity (m/s)')
    ax2.set_title(f'Impact velocity: {vels[-1]:.1f} m/s ({abs(vels[-1]) * 3.6:.0f} km/h)')
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('freefall.png', dpi=150)
    plt.show()
    print(f"Impact at {vels[-1]:.1f} m/s ({abs(vels[-1]) * 3.6:.0f} km/h) "
          f"after {times[-1]:.1f} s")

What does this plotting code do?

  • plt.subplots(2, 1) creates a two-panel figure. The top panel shows altitude dropping to zero. The bottom panel shows velocity growing more negative.
  • The brown dashed line marks ground level (altitude = 0).
  • plt.savefig('freefall.png', dpi=150) saves a publication-quality image to your project folder.
  • The final print statement reports the impact speed in both m/s and km/h so you can feel the magnitude.
Run the simulation and observe the crash

With both functions written, you need a __main__ block that calls them in sequence.

  • At the very bottom of falcon9_sim.py (below plot_freefall), add this __main__ block:
if __name__ == '__main__':
    print("=" * 50)
    print("STEP 2: Freefall - No Engine")
    print("=" * 50)
    t_ff, a_ff, v_ff = simulate_freefall()
    plot_freefall(t_ff, a_ff, v_ff)

What does this code do?

The if __name__ == '__main__': guard ensures this code only runs when you execute the file directly. It calls simulate_freefall() to generate the physics data, then passes the arrays to plot_freefall() for visualization.

  • Save falcon9_sim.py.

Before you run this, take a moment to predict: how fast do you think a 22-ton booster will be going after falling 3,000 meters from an initial -200 m/s?

  • Run the simulation by executing this command in your terminal:
python falcon9_sim.py

A plot window opens showing two panels. The top panel shows altitude dropping from 3,000 m to 0 in a smooth curve. The bottom panel shows velocity growing more negative in a straight line (constant acceleration).

Your terminal prints the impact speed: approximately -230.5 m/s, which is 830 km/h. The booster hits the ground in about 11 seconds.

Why is the velocity line straight?

With only gravity acting, acceleration is constant at -9.81 m/s². Constant acceleration means velocity changes at a constant rate, which is a straight line on the plot. This is pure Newtonian gravity with no drag and no engine.

Plot window not appearing?

  • If you see the terminal output but no plot window, your Matplotlib backend may need configuring. Close the terminal, reopen it, and run the command again.
  • If you get a NameError for ALT_START or G, make sure your function is below the constants section, not above it.
  • Check that your import numpy as np and import matplotlib.pyplot as plt lines are still at the very top of the file.

Why isn't my freefall plot appearing when I run the script?

  • Close the plot window to return to your terminal.
  • Confirm that freefall.png was saved to your project folder by running:
ls

You should see falcon9_sim.py and freefall.png in the output.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file and that the plot showed the booster crashing at approximately 830 km/h.

ⓧ I'd like to double check the full code

Your falcon9_sim.py should contain these sections in order. Scroll up and confirm each is present:

  • The import numpy as np and import matplotlib.pyplot as plt lines at the top.
  • The Falcon 9 parameters block (DRY_MASS through DT) with derived constants.
  • The simulate_freefall() function with the while alt > 0 loop.
  • The plot_freefall() function that creates the two-panel figure and saves freefall.png.
  • The if __name__ == '__main__': block that calls both functions.

830 km/h into concrete. That is what happens when a booster has no landing strategy. Next up, you'll fire the engine to try to slow down. But you'll quickly discover that choosing when to fire is harder than it sounds.

Fire the Engine (and Fail)

In Step 2, you watched the booster smash into the ground at over 800 km/h with no engine. The obvious fix is to fire the engine to slow down.

But at what altitude do you ignite? In this step, you will try two fixed ignition altitudes and discover that both fail. Too late and the booster still crashes. Too early and it flies back up because the Merlin engine cannot throttle low enough to hover.

In this step, get ready to:
  • Implement an engine firing simulation with fuel consumption.
  • Try two ignition altitudes and watch both fail.
  • Discover the fundamental hoverslam constraint that makes fixed timing impossible.
Implement the engine firing simulation

The freefall simulation only applies gravity. Now you need a version that fires the engine at a specified altitude, tracks fuel consumption, and adjusts mass as fuel burns away.

  • In falcon9_sim.py, add the simulate_fixed_ignition function directly below simulate_freefall. Start with the function definition and state initialization:
def simulate_fixed_ignition(ignite_alt):
    """Fire the engine at a fixed altitude. Usually fails."""
    alt = ALT_START
    vel = VEL_START
    mass = DRY_MASS + FUEL_MASS
    fuel = FUEL_MASS
    t = 0.0
    engine_on = False

    times, alts, vels = [t], [alt], [vel]

What does this code do?

This sets up the same simulation state as freefall, but now tracks mass (dry mass plus fuel) and fuel separately. The engine_on flag starts as False and flips when altitude drops below the ignition threshold.

  • Now add the simulation loop and return statement directly below the initialization, still inside the same function:
    while alt > 0 and t < 300:
        f_gravity = -mass * G

        if not engine_on and alt <= ignite_alt:
            engine_on = True
        if engine_on and fuel > 0:
            f_thrust = THRUST_MIN
            dm = min(MASS_FLOW * DT, fuel)
            fuel -= dm
            mass -= dm
        else:
            f_thrust = 0.0

        acc = (f_gravity + f_thrust) / mass
        vel += acc * DT
        alt += vel * DT
        t += DT

        times.append(t)
        alts.append(alt)
        vels.append(vel)

        if alt > ALT_START * 1.5:
            break

    return np.array(times), np.array(alts), np.array(vels)

What does this code do?

This is the same Euler integration loop as freefall, with three additions:

  • When altitude drops below ignite_alt, the engine turns on at minimum throttle (482 kN).
  • Each timestep during the burn, fuel decreases by MASS_FLOW * DT and total mass shrinks accordingly.
  • A safety break stops the simulation if altitude exceeds 1.5x the starting height (the booster flew back up and is never coming down with thrust on).
  • Save falcon9_sim.py and confirm no syntax errors by running:
python falcon9_sim.py

You should still see the freefall plot and impact output from Step 2. The new function exists but nothing calls it yet.

Seeing a SyntaxError?

  • Make sure the while loop is indented one level inside the function (4 spaces from the left edge).
  • Check that the return statement sits at the same indentation level as the while keyword (inside the function but outside the loop).

Help me fix an indentation error in my simulate_fixed_ignition function.

Add the stopping distance helper and plotting function

Before you can plot the two failed burns, you need a helper that computes how much altitude is required to stop from a given speed. This is a direct application of kinematics: set final velocity to zero and solve for distance.

  • Below simulate_fixed_ignition, add the stopping_distance helper function:
def stopping_distance(velocity, mass):
    """
    Calculate altitude needed to decelerate from current velocity to zero.

    From kinematics: v_f^2 = v_i^2 + 2*a*d
    Setting v_f = 0: d = v_i^2 / (2 * a_net)
    where a_net = T/m - g (net upward deceleration)
    """
    a_net = THRUST_MIN / mass - G
    return velocity**2 / (2 * a_net)

What is stopping distance?

This function answers: "Starting from this speed, how many meters does the engine need to bring me to a stop?" It computes the net deceleration (thrust divided by mass, minus gravity), then uses the kinematic equation to find the required distance.

The plot_naive_burns function you will add next uses this to compute the theoretical ignition altitude from the initial velocity, then deliberately tries 50% (too late) and 180% (too early) of that value.

  • Now add the plot_naive_burns function below plot_freefall. Start with the setup and first panel:
def plot_naive_burns():
    """Show that fixed ignition altitude fails."""
    mass = DRY_MASS + FUEL_MASS
    h_correct = stopping_distance(abs(VEL_START), mass)

    t1, a1, v1 = simulate_fixed_ignition(h_correct * 0.5)
    t2, a2, v2 = simulate_fixed_ignition(h_correct * 1.8)

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 8))

    ax1.plot(t1, a1, 'r-', linewidth=2,
             label=f'Too late (fire at {h_correct * 0.5:.0f} m)')
    ax1.plot(t2, a2, 'orange', linewidth=2,
             label=f'Too early (fire at {h_correct * 1.8:.0f} m)')
    ax1.axhline(y=0, color='brown', linestyle='--', label='Ground')
    ax1.set_xlabel('Time (s)')
    ax1.set_ylabel('Altitude (m)')
    ax1.set_title('Fixed Ignition Altitude - Both Fail!')
    ax1.legend()
    ax1.grid(True, alpha=0.3)

What does this code do?

First it computes h_correct using the stopping distance formula with the initial velocity of 200 m/s. Then it runs two simulations: one igniting at half that altitude (too late) and one at 1.8 times that altitude (too early).

The first panel plots altitude vs time for both attempts. The red line will crash through the ground and the orange line will reverse direction and climb.

  • Add the second panel, save logic, and print statements directly below the code you just pasted, still inside plot_naive_burns:
    ax2.plot(t1, v1, 'r-', linewidth=2, label='Too late (crashes)')
    ax2.plot(t2, v2, 'orange', linewidth=2, label='Too early (flies back up)')
    ax2.axhline(y=0, color='green', linestyle='--', alpha=0.5,
                label='Zero velocity')
    ax2.set_xlabel('Time (s)')
    ax2.set_ylabel('Velocity (m/s)')
    ax2.set_title('Velocity - Neither hits 0 m/s at ground level')
    ax2.legend()
    ax2.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig('naive_burn.png', dpi=150)
    plt.show()

    print(f"Correct ignition alt (from initial vel): {h_correct:.0f} m")
    print(f"Too late: impact at {v1[-1]:.1f} m/s")
    print(f"Too early: rocket flies back up (wasted fuel)")

What does this code do?

The second panel plots velocity over time for both burns. A green dashed line marks zero velocity, the target for a successful landing.

After saving the plot as naive_burn.png, it prints the theoretically correct ignition altitude and confirms what actually happened to each attempt.

  • Save falcon9_sim.py and run it to confirm no errors with the new functions:
python falcon9_sim.py

You should still see the freefall output only. The new functions exist but the __main__ block does not call them yet.

Seeing a NameError?

  • Make sure stopping_distance is defined above plot_naive_burns in the file, since plot_naive_burns calls it.
  • Check that simulate_fixed_ignition is also defined above both of these functions.

Help me fix a NameError in my falcon9_sim.py file.

Run the simulation and observe the failure

Time to actually fire the engine and see what happens. You will update the __main__ block to call plot_naive_burns() and watch both attempts fail.

  • In falcon9_sim.py, scroll to the bottom and find this line at the end of the __main__ block:
    plot_freefall(t_ff, a_ff, v_ff)
  • Add the following code directly below that line, still inside the if __name__ == '__main__': block:
    print("\n" + "=" * 50)
    print("STEP 3: Naive Engine Timing (Fixed Altitude)")
    print("=" * 50)
    plot_naive_burns()

What does this code do?

This adds a section header to the terminal output, then calls plot_naive_burns() which runs both fixed-ignition simulations, produces the comparison plot, and prints results.

Before you run this, think about what you expect to happen. The engine fires at 50% of the computed stopping distance in one case and 180% in the other. Will either one land successfully?

  • Save falcon9_sim.py and run the simulation:
python falcon9_sim.py

Both fail. The "too late" burn (red line) crashes through the ground because the booster accelerated further during freefall before the engine turned on. The "too early" burn (orange line) reverses direction and flies back up.

Why does the booster fly back up?

The Merlin 1D at minimum throttle produces 482 kN of force. The nearly-empty booster weighs only about 267 kN (27,200 kg times 9.81 m/s squared). That gives a thrust-to-weight ratio of about 2.2.

The engine cannot throttle any lower, so it always pushes harder than gravity pulls. This means the Falcon 9 physically cannot hover. If the engine is on, the booster is accelerating upward. This is the fundamental hoverslam constraint.

Why does fixed timing fail?

The stopping_distance formula computed the correct altitude based on the initial velocity of -200 m/s. But during freefall, velocity keeps increasing. By the time the booster falls to that altitude, it is going much faster than -200 m/s.

A fixed ignition altitude computed from the initial velocity is always wrong because it ignores the acceleration that happens between now and then. The real solution is to check dynamically at every moment whether it is time to fire.

Your terminal output shows the theoretically correct ignition altitude and confirms both attempts missed the mark.

✔️ Awesome, I've got everything!

Great. Make sure you have saved falcon9_sim.py with all three new functions (simulate_fixed_ignition, stopping_distance, and plot_naive_burns) and the updated __main__ block.

ⓧ I'd like to double check the full code

Your falcon9_sim.py should now contain these sections in order. Scroll through your file and confirm each one is present:

  • Imports (numpy and matplotlib).
  • Falcon 9 parameter constants and derived constants.
  • Initial conditions (ALT_START, VEL_START, DT).
  • simulate_freefall() function.
  • simulate_fixed_ignition(ignite_alt) function (with the while loop, engine ignition, fuel tracking, and safety break).
  • stopping_distance(velocity, mass) function (two-line body computing a_net and returning the formula).
  • plot_freefall() function (from Step 2).
  • plot_naive_burns() function (computes h_correct, runs two simulations at 50% and 180%, plots both panels, saves naive_burn.png, prints results).
  • __main__ block calling plot_freefall and then plot_naive_burns.

Both burn strategies failed, and now you understand why. In the next step, you will solve this by computing the stopping distance dynamically at every single timestep, firing the engine at the last possible instant.

Derive the Hoverslam

You proved that a fixed ignition altitude always fails. Fire too late and you crash. Fire too early and the booster flies back up because the engine cannot throttle low enough to hover.

The real fix is to stop thinking in terms of a fixed altitude. Instead, at every single timestep, ask: "Given how fast I'm falling right now, how much distance do I need to stop?" The moment your altitude equals that stopping distance, you fire. This is continuous trajectory replanning, and it mirrors how the real Falcon 9 guidance computer works.

In this step, get ready to:
  • Derive the stopping-distance formula from kinematics.
  • Implement dynamic ignition timing that recalculates every timestep.
  • Visualize the successful hoverslam landing with a three-panel plot.
Implement the stopping distance formula

The core question at every moment during freefall is: "If I fire the engine right now, do I have enough altitude left to decelerate to zero?" The answer comes from a basic kinematics equation.

  • In falcon9_sim.py, find the end of the simulate_fixed_ignition() function.
  • Add the following function below it:
def stopping_distance(velocity, mass):
    """
    Calculate altitude needed to decelerate from current velocity to zero.

    From kinematics: v_f^2 = v_i^2 + 2*a*d
    Setting v_f = 0: d = v_i^2 / (2 * a_net)
    where a_net = T/m - g (net upward deceleration)
    """
    a_net = THRUST_MIN / mass - G
    return velocity**2 / (2 * a_net)

What does this formula mean?

Start with the kinematics equation: v_f² = v_i² + 2·a·d. You want final velocity (v_f) to be zero. Rearrange for distance: d = v_i² / (2·a).

The net deceleration a_net is thrust divided by mass, minus gravity pulling you down. This gives the minimum altitude at which the engine MUST fire to avoid a crash.

  • Save falcon9_sim.py and run it:
python falcon9_sim.py

The freefall and naive burns sections both run successfully. The naive burns plot now computes the correct reference altitude using your new function.

Seeing a NameError?

Make sure stopping_distance is defined ABOVE plot_naive_burns in the file. Python reads top to bottom, so the function must exist before any code calls it.

Help me fix a NameError in my falcon9_sim.py file.

Implement the dynamic ignition simulation

Now you have the formula. The simulation needs to call it at every timestep during freefall, comparing the result to current altitude. The moment altitude drops to meet the stopping distance, ignition happens automatically.

  • Below stopping_distance(), add the first part of the simulate_hoverslam() function. This sets up the simulation loop and the dynamic ignition check:
def simulate_hoverslam(use_drag=False, use_mass_loss=False):
    """
    Simulate with correct dynamic ignition timing.
    Fire engine when altitude equals stopping distance from current velocity.
    """
    alt = ALT_START
    vel = VEL_START
    mass = DRY_MASS + FUEL_MASS
    fuel = FUEL_MASS
    t = 0.0
    engine_on = False

    times, alts, vels, thrusts = [t], [alt], [vel], [0.0]

    while alt > 0 and t < 300:
        f_gravity = -mass * G

        if use_drag:
            f_drag = -0.5 * RHO * vel * abs(vel) * CD * AREA
        else:
            f_drag = 0.0

        if not engine_on:
            h_needed = stopping_distance(abs(vel), mass)
            if alt <= h_needed:
                engine_on = True

What does this code do?

  • The use_drag and use_mass_loss parameters default to False. You will enable them in Step 5 to add realism.
  • The key line is if alt <= h_needed. Every timestep, the code asks: "Is my altitude now equal to or below the distance I need to stop?" The instant the answer is yes, the engine fires.
  • Using abs(vel) converts the negative (downward) velocity into a positive speed for the distance formula.
  • Continue the function directly below (still inside the while loop). This handles thrust, Euler integration, and recording:
        if engine_on and fuel > 0:
            f_thrust = THRUST_MIN
            if use_mass_loss:
                dm = min(MASS_FLOW * DT, fuel)
                fuel -= dm
                mass -= dm
        else:
            f_thrust = 0.0

        f_net = f_gravity + f_drag + f_thrust
        acc = f_net / mass
        vel += acc * DT
        alt += vel * DT
        t += DT

        times.append(t)
        alts.append(alt)
        vels.append(vel)
        thrusts.append(f_thrust)

    return (np.array(times), np.array(alts),
            np.array(vels), np.array(thrusts))

What does this section do?

  • Once the engine is on, thrust is applied at the constant minimum value (482 kN). The use_mass_loss branch is inactive for now.
  • The Euler integration updates velocity from net acceleration, then updates altitude from velocity. Each timestep is 0.01 seconds.
  • The thrusts array records whether the engine was firing at each timestep. This lets you plot the thrust profile later.
  • Save falcon9_sim.py and run it to confirm no syntax errors:
python falcon9_sim.py

The existing freefall and naive burn plots still appear as before. No errors means your new function has valid syntax and is ready to use.

Seeing an IndentationError?

The second code block must be indented inside the while loop (two levels of indentation: one for the function, one for the loop). Make sure the if engine_on line starts at the same level as the if not engine_on line above it.

The return statement must be at one indent level (inside the function, outside the loop).

Help me fix indentation in my simulate_hoverslam function.

Visualize the successful landing

The simulation logic is complete. Now you need a plotting function that shows all three channels of the landing: altitude dropping to zero, velocity rising to zero, and thrust kicking in at the critical moment.

  • Below plot_naive_burns(), add the first part of the plot_hoverslam() function. This configures the three panels:
def plot_hoverslam(times, alts, vels, thrusts, title_suffix=""):
    """Plot the successful hoverslam landing."""
    fig, axes = plt.subplots(3, 1, figsize=(10, 10), sharex=True)

    axes[0].plot(times, alts, 'b-', linewidth=2)
    axes[0].axhline(y=0, color='brown', linestyle='--', label='Ground')
    axes[0].set_ylabel('Altitude (m)')
    axes[0].set_title(f'Falcon 9 Hoverslam Landing {title_suffix}')
    axes[0].legend()
    axes[0].grid(True, alpha=0.3)

    axes[1].plot(times, vels, 'r-', linewidth=2)
    axes[1].axhline(y=0, color='green', linestyle='--', alpha=0.5,
                    label='Zero (target)')
    axes[1].set_ylabel('Velocity (m/s)')
    axes[1].set_title(f'Touchdown velocity: {vels[-1]:.2f} m/s')
    axes[1].legend()
    axes[1].grid(True, alpha=0.3)

    axes[2].plot(times, np.array(thrusts) / 1000, color='orange', linewidth=2)
    axes[2].set_xlabel('Time (s)')
    axes[2].set_ylabel('Thrust (kN)')
    axes[2].set_title('Engine Thrust (482 kN = single Merlin at min throttle)')
    axes[2].grid(True, alpha=0.3)

What does this code do?

  • Three vertically-stacked panels share the same time axis. The top shows altitude falling to zero, the middle shows velocity rising from -200 m/s to near zero, and the bottom shows the thrust profile.
  • The green dashed line at zero velocity on the middle panel is the target. Landing means hitting that line at the same moment altitude hits zero.
  • Continue the function directly below. This saves the plot and prints results:
    plt.tight_layout()
    filename = 'hoverslam_full.png' if title_suffix else 'hoverslam.png'
    plt.savefig(filename, dpi=150)
    plt.show()

    burn_time = np.sum(np.array(thrusts) > 0) * DT
    print(f"Touchdown velocity: {vels[-1]:.2f} m/s")
    print(f"Burn duration: {burn_time:.1f} s")

What does this section do?

  • The filename switches between hoverslam.png and hoverslam_full.png based on title_suffix. You will use the suffix in Step 5 for the full-physics version.
  • Burn duration is computed by counting how many timesteps had non-zero thrust, multiplied by DT.
  • Now scroll down to the if __name__ == '__main__': block at the bottom of the file.
  • Find the last line of the block (after plot_naive_burns()) and add this below it:
    print("\n" + "=" * 50)
    print("STEP 4: Correct Hoverslam (basic physics)")
    print("=" * 50)
    t_hs, a_hs, v_hs, th_hs = simulate_hoverslam(
        use_drag=False, use_mass_loss=False)
    plot_hoverslam(t_hs, a_hs, v_hs, th_hs)

Before you run this, what do you think the touchdown velocity will be? The booster starts at 200 m/s downward and fires at the last possible instant.

  • Save falcon9_sim.py and run the simulation:
python falcon9_sim.py

Close the first two plot windows (freefall and naive burns) to reach the new hoverslam plot. You see a three-panel figure: altitude curves smoothly to zero, velocity rises from -200 m/s to near zero, and the thrust panel shows the engine igniting at exactly the right moment.

Your terminal prints a touchdown velocity between -1 and -3 m/s (walking speed) and a burn duration of approximately 25 seconds. The booster lands.

Why does dynamic timing work when fixed timing failed?

A fixed altitude is computed from the initial velocity. But during freefall, velocity keeps increasing. By the time the booster reaches that pre-computed altitude, it is falling faster than the formula assumed.

The dynamic check recomputes stopping distance from the actual current velocity at every timestep. It accounts for the acceleration that happened during freefall. This is why the real Falcon 9 continuously replans its trajectory rather than committing to a fixed altitude.

Booster crashes or flies back up?

If the touchdown velocity is large and negative (crash), check that your ignition condition uses abs(vel) inside stopping_distance(). Without the absolute value, a negative velocity produces a negative stopping distance and ignition never triggers.

If the booster reverses and flies up, confirm the condition is alt <= h_needed (less than or equal), not greater than.

Help me debug my hoverslam simulation. The booster isn't landing correctly.

✔️ Awesome, I've got everything!

Great. Double check you have saved falcon9_sim.py and confirm the terminal shows a touchdown velocity between -1 and -3 m/s.

ⓧ I'd like to double check the full code

Your file has grown over this step. Scroll through and confirm each section is present in this order:

  • Imports and constants (unchanged from earlier).
  • simulate_freefall() function (unchanged).
  • simulate_fixed_ignition() function (unchanged).
  • stopping_distance() function (new this step).
  • simulate_hoverslam() function (new this step).
  • plot_freefall() function (unchanged).
  • plot_naive_burns() function (unchanged).
  • plot_hoverslam() function (new this step).
  • if __name__ == '__main__': block now calls Steps 2, 3, and 4.

Your booster now lands at walking speed. The physics works perfectly in a vacuum with constant mass. Next up, you will add atmospheric drag and fuel mass loss to see how real-world conditions change the landing profile.

Add Real-World Physics

Your basic hoverslam works perfectly in a vacuum with a constant-mass booster. Reality is more complicated.

A real Falcon 9 booster pushes through air on the way down, which creates drag that slows it before ignition. It also burns fuel during the landing burn, getting lighter over time. In this step, you will understand both effects and run the full physics model to see how they produce a gentler, more realistic landing.

In this step, get ready to:
  • Understand the atmospheric drag formula in your simulation.
  • Understand how fuel mass loss improves thrust-to-weight ratio during the burn.
  • Run the full physics model and compare it to the basic model.
Understand the atmospheric drag formula

Your simulate_hoverslam() function already contains the drag calculation, controlled by the use_drag parameter. Let's understand what it does physically.

  • In falcon9_sim.py, find this code inside simulate_hoverslam():
        if use_drag:
            f_drag = -0.5 * RHO * vel * abs(vel) * CD * AREA
        else:
            f_drag = 0.0

What does this drag formula do?

This is the standard aerodynamic drag equation. Each variable maps to a physical quantity:

  • RHO (1.225 kg/m³) is air density at sea level.
  • vel * abs(vel) gives velocity-squared but preserves the sign, so drag always opposes the direction of motion.
  • CD (1.2) is the drag coefficient for a cylinder shape.
  • AREA is the booster's cross-section: pi * (3.7/2)² = 10.75 m².

The negative sign means drag slows the booster during freefall. At 200 m/s, this produces about 30 kN of upward force. Not enough to land, but enough to arrive at the burn altitude a bit slower.

Understand the fuel mass loss model

The second real-world effect is that the booster gets lighter as it burns fuel. Find this code inside the engine-firing block of simulate_hoverslam():

        if engine_on and fuel > 0:
            f_thrust = THRUST_MIN
            if use_mass_loss:
                dm = min(MASS_FLOW * DT, fuel)
                fuel -= dm
                mass -= dm
        else:
            f_thrust = 0.0

Why does mass loss help the landing?

The Merlin engine burns fuel at MASS_FLOW = 174 kg/s. Over a 25-second burn, that is about 4,350 kg of fuel consumed. The booster starts at 27,200 kg and finishes near 22,850 kg.

A lighter booster means higher deceleration from the same thrust (a = T/m, and m is shrinking). The thrust-to-weight ratio improves as fuel burns off, producing a gentler final approach to the ground.

Run the full physics model and compare results

Now you will call simulate_hoverslam() with both effects enabled and compare the results to the basic model.

  • In falcon9_sim.py, scroll to the bottom of your __main__ block. Find the last line:
    plot_hoverslam(t_hs, a_hs, v_hs, th_hs)
  • Add the following code below that line to run the full physics simulation and print a comparison:
    print("\n" + "=" * 50)
    print("STEP 5: Full Model (drag + fuel mass loss)")
    print("=" * 50)
    t_full, a_full, v_full, th_full = simulate_hoverslam(
        use_drag=True, use_mass_loss=True)
    plot_hoverslam(t_full, a_full, v_full, th_full,
                   title_suffix="(Full Physics)")

    print("\n" + "=" * 50)
    print("COMPARISON")
    print("=" * 50)
    print(f"Basic model touchdown:  {v_hs[-1]:.2f} m/s")
    print(f"Full model touchdown:   {v_full[-1]:.2f} m/s")
    print(f"Real Falcon 9 target:   ~2 m/s")

What does this code do?

  • simulate_hoverslam(use_drag=True, use_mass_loss=True) runs the same dynamic ignition logic but now includes air resistance during freefall and decreasing mass during the burn.
  • title_suffix="(Full Physics)" labels the plot so you can distinguish it from the basic hoverslam. It also triggers saving as hoverslam_full.png.
  • The comparison block prints both touchdown velocities side-by-side, plus the real Falcon 9 target of approximately 2 m/s.
  • Save falcon9_sim.py.

Before you run this, think about what you expect. Will the full physics model land harder or softer than the basic model? Why?

  • Run the simulation by running this command:
python falcon9_sim.py

You will see all the previous plots appear (freefall, naive burns, basic hoverslam), followed by a new three-panel plot titled "Falcon 9 Hoverslam Landing (Full Physics)". In the terminal, the comparison prints both touchdown velocities. The full model lands more gently than the basic model because drag pre-decelerates the booster and mass loss improves the thrust-to-weight ratio near touchdown.

Why is the full model softer?

Two effects combine. Drag slows the booster during freefall, so it arrives at the ignition point with less speed. The stopping distance is shorter, so ignition happens later (lower altitude). Meanwhile, mass loss means the booster is lighter at the end of the burn, giving higher deceleration from the same 482 kN thrust.

The real Falcon 9 targets approximately 2 m/s at touchdown. Your simulation should be in that neighbourhood.

Seeing an error or no comparison output?

  • Make sure the new code uses the same indentation (4 spaces) as the rest of the __main__ block. It must be inside the if __name__ == '__main__': section.
  • If you see a NameError for v_hs, check that the Step 4 call to simulate_hoverslam(use_drag=False, use_mass_loss=False) still assigns to t_hs, a_hs, v_hs, th_hs above your new code.
  • Close each plot window to proceed to the next one. The comparison only prints after the last plot closes.

Why isn't my full physics comparison printing?

✔️ Awesome, I've got everything!

Great. Double check you have saved falcon9_sim.py and that the terminal prints both touchdown velocities plus the real Falcon 9 target.

ⓧ I'd like to double check the full code

Your __main__ block should now contain five sections in this order:

  • STEP 2: Freefall, calling simulate_freefall() and plot_freefall().
  • STEP 3: Naive burns, calling plot_naive_burns().
  • STEP 4: Basic hoverslam with use_drag=False, use_mass_loss=False.
  • STEP 5: Full model with use_drag=True, use_mass_loss=True and title_suffix="(Full Physics)".
  • COMPARISON: prints basic model, full model, and real Falcon 9 target touchdown velocities.

The simulate_hoverslam() function itself should be unchanged from the previous step. Scroll up and confirm the use_drag and use_mass_loss branches are present inside the while loop.

You now have a physics simulation that closely mirrors real Falcon 9 telemetry. Drag pre-decelerates the booster, mass loss improves the final approach, and the dynamic ignition check holds everything together. Next up, the Secret Mission challenges you to bring this to life with a real-time animation.

Secret mission

Animate the Landing

Your static plots prove the physics works, but they miss the drama of watching a 22-ton booster fall for 25 seconds before the engine fires at the last possible instant. Can you build a live animation that shows the descent in real-time?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • The falcon9-landing project folder (contains falcon9_sim.py and generated PNG plots).
  • NumPy and Matplotlib Python packages installed via pip.

Keep everything running

No action needed. Choose this if you plan to keep experimenting with the simulation or want to tweak the physics parameters.

  • Your falcon9-landing folder stays on your Desktop with all plots intact.
  • NumPy and Matplotlib remain installed and available for other Python projects.

Pause - I'll come back to this later

Since this is a local project with no running processes, there is nothing to pause. Your files stay on disk until you decide to delete them.

  • Your project folder and installed packages take up minimal disk space (under 100 MB total).
  • You can return to the project at any time by opening the folder in VS Code.

Delete - I don't want to use this again

Remove all project files and uninstall the Python packages.

  • Delete the project folder by running this command in your terminal (macOS/Linux):
rm -rf ~/Desktop/falcon9-landing
  • On Windows, run this in PowerShell instead:
Remove-Item -Recurse -Force ~\Desktop\falcon9-landing
  • Uninstall the Python packages by running:
pip uninstall numpy matplotlib -y
  • If you use NumPy or Matplotlib in other projects, skip the uninstall step.

That's a Wrap!

That's a Wrap!

Nice work. You just built a physics simulation grounded in real Falcon 9 data that demonstrates why the hoverslam is the only viable landing strategy for a rocket that cannot hover.

You've learned how to:

  • Build a 1D physics simulation using real Falcon 9 Block 5 parameters (22,200 kg dry mass, 482 kN minimum thrust, 282s Isp) and Euler numerical integration to model forces over time.
  • Derive the hoverslam ignition formula from first-principles kinematics and implement dynamic trajectory planning that continuously reassesses stopping distance at every timestep.
  • Add atmospheric drag and fuel mass loss to produce a realistic descent profile, then compare both models with publication-quality plots.
  • Secret Mission: Created a live animation showing the booster descending and landing in real-time using Matplotlib's interactive mode.

Ready to quiz yourself?