Capstone Planning: Requirements → Budgets → Trades

Module 6: Capstone Integration

This notebook is a planning tool.

Before you write a big simulator, you should be able to answer: - What mission are we building? - What constraints matter most? - What is our delta-v ((v)) budget and mass budget? - What are the biggest risks / unknowns?

What you’ll build here

  • A simple mission requirements sheet (as a Python dict + table)
  • A first-pass delta-v ((v)) budget (order-of-magnitude, not perfect)
  • A first-pass mass model using the rocket equation
  • A small trade table: how assumptions change feasibility

Important note

This is an educational template. Real mission design is more detailed — but the structure is real.

Code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from IPython.display import display

plt.style.use("dark_background")

G0 = 9.80665  # m/s^2

print("Environment ready. Let's plan a mission like an engineer.")
Environment ready. Let's plan a mission like an engineer.

1) Pick a mission (multi‑agency examples)

A capstone should be ambitious, but it must be bounded.

Here are a few example mission types (you can rename these to match NASA/ESA/JAXA/CNSA/ISRO/Roscosmos programs you’re studying): - LEO satellite mission (comms, navigation, Earth observation) - GEO communications satellite (large energy, long operations) - Lunar mission (orbiter, lander, crewed gateway support) - Mars transfer mission (cargo or crewed transfer)

We’ll store a “mission requirements sheet” as structured data.

Code
MISSION_LIBRARY = {
    "LEO Earth observation satellite": {
        "agency_context": "Examples: NASA Landsat, ESA Sentinel, ISRO Cartosat (varies by mission).",
        "crewed": False,
        "payload_kg": 1200,
        "target": "Sun-synchronous-ish LEO (simplified)",
        "delta_v_budget_m_s": {
            "launch_to_leo": 9400,
            "orbit_raise_and_phasing": 150,
            "stationkeeping_lifetime": 200,
            "deorbit_disposal": 150,
        },
        "max_g": 6.0,
        "notes": "LEO is crowded: debris mitigation and disposal matter.",
    },
    "GEO communications satellite": {
        "agency_context": "Examples: commercial + national GEO operators; ESA/NASA launches are common.",
        "crewed": False,
        "payload_kg": 6000,
        "target": "GEO (simplified)",
        "delta_v_budget_m_s": {
            "launch_to_leo_equivalent": 9400,
            "transfer_to_geo": 4300,
            "stationkeeping_lifetime": 1500,
            "graveyard_orbit": 20,
        },
        "max_g": 6.0,
        "notes": "Large energy requirement. Often uses an upper stage or onboard electric propulsion.",
    },
    "Lunar cargo lander": {
        "agency_context": "Examples: NASA CLPS, JAXA SLIM, CNSA Chang'e landers (varies by mission).",
        "crewed": False,
        "payload_kg": 3000,
        "target": "Lunar surface (simplified)",
        "delta_v_budget_m_s": {
            "launch_to_leo": 9400,
            "trans_lunar_injection": 3200,
            "lunar_orbit_insertion": 900,
            "descent_and_landing": 1900,
        },
        "max_g": 8.0,
        "notes": "No atmosphere: landing is propulsive. Precision + guidance matter.",
    },
    "Mars cargo transfer": {
        "agency_context": "Examples: NASA/ESA robotic Mars missions; future cargo/crewed concepts.",
        "crewed": False,
        "payload_kg": 20000,
        "target": "Mars transfer + surface arrival (simplified)",
        "delta_v_budget_m_s": {
            "launch_to_leo": 9400,
            "trans_mars_injection": 3600,
            "midcourse_corrections": 50,
            "entry_descent_landing": 1500,
        },
        "max_g": 5.0,
        "notes": "Mars has an atmosphere, so some energy can be removed aerodynamically.",
    },
}

mission_name = "Lunar cargo lander"  # try changing this
mission = MISSION_LIBRARY[mission_name]

# Flatten to a quick table for readability
budget_rows = [(k, v) for k, v in mission["delta_v_budget_m_s"].items()]

df_req = pd.DataFrame(
    {
        "field": ["agency_context", "target", "crewed", "payload_kg", "max_g", "notes"],
        "value": [
            mission["agency_context"],
            mission["target"],
            mission["crewed"],
            mission["payload_kg"],
            mission["max_g"],
            mission["notes"],
        ],
    }
)

df_dv = pd.DataFrame(budget_rows, columns=["phase", "delta_v_m_s"])

print("Mission:", mission_name)
display(df_req)
display(df_dv)
Mission: Lunar cargo lander
field value
0 agency_context Examples: NASA CLPS, JAXA SLIM, CNSA Chang'e l...
1 target Lunar surface (simplified)
2 crewed False
3 payload_kg 3000
4 max_g 8.0
5 notes No atmosphere: landing is propulsive. Precisio...
phase delta_v_m_s
0 launch_to_leo 9400
1 trans_lunar_injection 3200
2 lunar_orbit_insertion 900
3 descent_and_landing 1900

2) Build a delta-v budget (and identify the biggest terms)

A delta-v budget is just a list of velocity changes you expect to need.

Key idea: - If you forget a major term, your design will look “fine”… until it fails.

We’ll sum the phases and plot which parts dominate.

Code
dv = df_dv.copy()
dv["delta_v_km_s"] = dv["delta_v_m_s"] / 1000

total_dv = float(dv["delta_v_m_s"].sum())

print(f"Total delta-v budget: {total_dv/1000:.2f} km/s")

fig, ax = plt.subplots(figsize=(10.5, 4.2))
ax.barh(dv["phase"], dv["delta_v_km_s"], color="#00d4ff", alpha=0.85)
ax.set_title(f"Delta-v budget by phase — {mission_name}\nTotal: {total_dv/1000:.2f} km/s")
ax.set_xlabel("delta-v (km/s)")
ax.grid(True, axis="x", alpha=0.25)

# Show labels at bar ends
for i, row in dv.iterrows():
    ax.text(row["delta_v_km_s"] + 0.05, i, f"{row['delta_v_km_s']:.2f}", va="center")

plt.tight_layout()
plt.show()
Total delta-v budget: 15.40 km/s

3) Convert delta-v into a mass ratio (rocket equation)

The Tsiolkovsky rocket equation connects mission difficulty (delta-v) to how much propellant you need:

[v = I_{sp} g_0 ()]

Where: - (I_{sp}): specific impulse (seconds) - (g_0): standard gravity (9.80665 m/s²) - (m_0): initial mass (dry + payload + propellant) - (m_f): final mass (dry + payload)

We’ll use this for a first-pass estimate. Real vehicles use staging, engine throttling, gravity losses, and many other details.

Code
def mass_ratio(delta_v_m_s: float, isp_s: float) -> float:
    """Ideal rocket equation mass ratio m0/mf."""
    return float(np.exp(delta_v_m_s / (isp_s * G0)))


def propellant_fraction(mr: float) -> float:
    """Propellant fraction for an ideal single stage: (m0 - mf) / m0."""
    return float(1.0 - 1.0 / mr)


ISP_LIBRARY = {
    "Kerolox (RP-1/LOX) ~330s": 330,
    "Methalox (CH4/LOX) ~380s": 380,
    "Hydrolox (LH2/LOX) ~450s": 450,
}

rows = []
for label, isp in ISP_LIBRARY.items():
    mr = mass_ratio(total_dv, isp)
    rows.append(
        {
            "propellant": label,
            "Isp_s": isp,
            "mass_ratio_m0_mf": mr,
            "propellant_fraction": propellant_fraction(mr),
        }
    )

df_mr = pd.DataFrame(rows)
df_mr["propellant_fraction"] = (100 * df_mr["propellant_fraction"]).round(1)
df_mr["mass_ratio_m0_mf"] = df_mr["mass_ratio_m0_mf"].round(2)

display(df_mr)
propellant Isp_s mass_ratio_m0_mf propellant_fraction
0 Kerolox (RP-1/LOX) ~330s 330 116.59 99.1
1 Methalox (CH4/LOX) ~380s 380 62.34 98.4
2 Hydrolox (LH2/LOX) ~450s 450 32.78 96.9

4) A first-pass mass model (single stage)

To make the rocket equation concrete, we’ll assume a single “stage” must deliver the full delta-v budget.

This is not how real launch vehicles work (they stage, refuel, and split the job). But as a learning tool it’s perfect:

  • If the required propellant mass is ridiculous, that’s a signal you need staging, refueling, or a different mission.
Code
def propellant_required(delta_v_m_s: float, isp_s: float, dry_mass_kg: float, payload_kg: float) -> dict:
    """Compute propellant required for an ideal single stage delivering delta-v."""
    mf = float(dry_mass_kg + payload_kg)  # final mass
    mr = mass_ratio(delta_v_m_s, isp_s)
    m0 = mf * mr
    prop = m0 - mf
    return {
        "dry_mass_kg": float(dry_mass_kg),
        "payload_kg": float(payload_kg),
        "final_mass_mf_kg": mf,
        "initial_mass_m0_kg": m0,
        "propellant_kg": prop,
        "mass_ratio_m0_mf": mr,
    }


dry_mass_guess_kg = 15000  # try changing this
payload_kg = float(mission["payload_kg"])

rows = []
for label, isp in ISP_LIBRARY.items():
    out = propellant_required(total_dv, isp, dry_mass_guess_kg, payload_kg)
    out["propellant"] = label
    out["Isp_s"] = isp
    rows.append(out)

df_mass = pd.DataFrame(rows)

# Pretty formatting for display
for col in ["final_mass_mf_kg", "initial_mass_m0_kg", "propellant_kg"]:
    df_mass[col] = df_mass[col].round(0).astype(int)

df_mass["mass_ratio_m0_mf"] = df_mass["mass_ratio_m0_mf"].round(2)

display(df_mass[["propellant", "Isp_s", "dry_mass_kg", "payload_kg", "propellant_kg", "initial_mass_m0_kg", "mass_ratio_m0_mf"]])
propellant Isp_s dry_mass_kg payload_kg propellant_kg initial_mass_m0_kg mass_ratio_m0_mf
0 Kerolox (RP-1/LOX) ~330s 330 15000.0 3000.0 2080646 2098646 116.59
1 Methalox (CH4/LOX) ~380s 380 15000.0 3000.0 1104042 1122042 62.34
2 Hydrolox (LH2/LOX) ~450s 450 15000.0 3000.0 571967 589967 32.78

Interpreting the result

If you see a huge initial mass for a modest payload, that’s not “bad math” — it’s the core message of rocketry:

  • High delta-v missions quickly push you toward staging, on‑orbit refueling, and careful mass budgeting.

This is exactly why engineers obsess over every kilogram.

Code
# Visual: mass breakdown for one choice

choice = "Methalox (CH4/LOX) ~380s"
row = df_mass[df_mass["propellant"] == choice].iloc[0]

mf = float(row["final_mass_mf_kg"])
prop = float(row["propellant_kg"])

parts = {
    "dry + payload (mf)": mf,
    "propellant": prop,
}

fig, ax = plt.subplots(figsize=(10, 3.6))
ax.barh(list(parts.keys()), list(parts.values()), color=["#7CFC00", "#00d4ff"], alpha=0.85)
ax.set_title(f"Ideal single-stage mass breakdown — {mission_name}\nPropellant choice: {choice}")
ax.set_xlabel("mass (kg)")
ax.grid(True, axis="x", alpha=0.25)

for i, (name, val) in enumerate(parts.items()):
    ax.text(val * 1.01, i, f"{val:,.0f} kg", va="center")

plt.tight_layout()
plt.show()

5) Trade study: how sensitive is feasibility to assumptions?

A trade study is a structured way to ask: - “If I change Isp (engine choice), what happens?” - “If my dry mass grows by 20%, what happens?”

We’ll vary a few values and compare the required initial mass.

Code
dry_mass_options = [8000, 15000, 25000]  # kg
isp_options = [330, 380, 450]  # s

trade_rows = []
for dm in dry_mass_options:
    for isp in isp_options:
        out = propellant_required(total_dv, isp, dm, payload_kg)
        trade_rows.append(
            {
                "dry_mass_kg": dm,
                "Isp_s": isp,
                "initial_mass_m0_kg": out["initial_mass_m0_kg"],
                "propellant_kg": out["propellant_kg"],
                "mass_ratio_m0_mf": out["mass_ratio_m0_mf"],
            }
        )

df_trade = pd.DataFrame(trade_rows)

# Human-friendly formatting
for col in ["initial_mass_m0_kg", "propellant_kg"]:
    df_trade[col] = df_trade[col].round(0).astype(int)

df_trade["mass_ratio_m0_mf"] = df_trade["mass_ratio_m0_mf"].round(2)

print("Trade study (ideal single-stage). Payload:", int(payload_kg), "kg")
display(df_trade)

# Pivot view: initial mass vs Isp for each dry mass
pivot = df_trade.pivot(index="dry_mass_kg", columns="Isp_s", values="initial_mass_m0_kg")
pivot.columns = [f"Isp {c}s" for c in pivot.columns]
display(pivot)
Trade study (ideal single-stage). Payload: 3000 kg
dry_mass_kg Isp_s initial_mass_m0_kg propellant_kg mass_ratio_m0_mf
0 8000 330 1282506 1271506 116.59
1 8000 380 685693 674693 62.34
2 8000 450 360536 349536 32.78
3 15000 330 2098646 2080646 116.59
4 15000 380 1122042 1104042 62.34
5 15000 450 589967 571967 32.78
6 25000 330 3264560 3236560 116.59
7 25000 380 1745399 1717399 62.34
8 25000 450 917727 889727 32.78
Isp 330s Isp 380s Isp 450s
dry_mass_kg
8000 1282506 685693 360536
15000 2098646 1122042 589967
25000 3264560 1745399 917727

6) Capstone checklist (what to build after this notebook)

Once you have a mission definition + rough budgets, your capstone can be broken into clear components:

  • Trajectory
    • Orbit / transfer assumptions, time of flight, major burns
  • Propulsion / mass model
    • Isp assumptions, staging/refueling plan, margins
  • Human factors (if crewed)
    • max g, vibration/comfort model, abort modes
  • Policy / compliance
    • spectrum, debris disposal plan, licensing assumptions
  • Validation
    • compare to known public numbers (order-of-magnitude sanity checks)

What can I do next?

  • Pick a mission in MISSION_LIBRARY and rewrite the phase list until it tells a coherent story.
  • If your total delta-v is big: decide where staging happens or where refueling happens.
  • Then jump back to the existing projects and reuse what already works:
    • Module 2: Mars Mission Simulator + porkchop plot
    • Module 3: Propellant Explorer (delta-v + propellant trade-offs)
    • Module 4: Crew Safety + Policy calculators