import numpy as np
from scipy.optimize import root_scalar
from matplotlib import pyplot as plt
from pint import UnitRegistry
ureg = UnitRegistry()
Q_ = ureg.QuantityNotebook Cell
# these lines are only for helping improve the display
import matplotlib_inline.backend_inline
matplotlib_inline.backend_inline.set_matplotlib_formats('pdf', 'png')
plt.rcParams['figure.dpi']= 300
plt.rcParams['savefig.dpi'] = 300
plt.rcParams['mathtext.fontset'] = 'cm'Thrust¶
In the last module, the expression for thrust from a rocket (or any propulsion system relying on an exhaust jet) was given. Let’s now derive that, by considering a rocket undergoing a static test fire as shown in Figure 1, where it is held in place by a test stand that counteracts the thrust force.
Figure 1:Rocket on a horizontal test stand, with specified properties of velocity , area , pressure in the combustion chamber (subscript ), at the throat (subscript ), at the exit (subscript ), and ambient pressure . The test stand support legs impart a horizontal reaction force , and the drag force since the system is not moving.
We draw a control surface just around the rocket, with the exhaust jet crossing the boundary at the exit of hte nozzle, and apply conservation of momentum in the horizontal direction:
where the reaction force imparted by the test stand (to hold the rocket in place) is the same as the thrust force: . Then, for thrust, we have:
where refers to the momentum flux thrust and refers to the pressure thrust terms. If , or the nozzle is overexpanded, then the pressure thrust is negative and reduces the overall thrust. Typically, though, nozzles are designed such that .
In the vacuum of space (or near-vacuum),
Effective exhaust velocity¶
The effective exhaust velocity, , is defined as the actual thrust divided by the mass flow rate:
This allows us to express thrust as
and we can relate effective exhaust velocity to specific impulse:
The rocket equation¶
We now have an expression for thrust of a rocket, considering static firing on a test stand. What about a rocket in motion, which is continuously exhausting propellant mass to generate thrust? If we apply Netwon’s second law, we can obtain the Tsiolkovsky rocket equation.
Consider a rocket in motion in some arbitrary flight direction as shown in Figure 2, where we assume
thrust in the direction of flight (no thrust vectoring)
steady thrust, and
no lift generated by the rocket body.
Figure 2:Free-body-diagram of a rocket in flight at velocity and some angle , with steady thrust , drag force , weight , and with nozzle exit velocity .
Applying Newton’s second law:
where the subscript indicates in the direction of flight, and
where the subscript indicates in the direction normal to flight. Now, let’s insert our expression for thrust into Equation (7), and recall that the rate of change of the rocket’s mass is related to the mass flow rate of exhausting propellant: :
We can integrate this from time to any time , or the burnout time when the rocket stops thrusting and no longer exhausts propellant:
or, to obtain Tsiolkovsky’s rocket equation:
where refers to the initial vehicle mass, refers to the final vehicle mass at time , and in general drag force, gravity, and flight direction may vary with time: , , and . The integral involving drag represents the net reduction in due to drag, while the integral involving gravity is the “gravity loss”, or the net loss due to carrying the rocket’s mass in gravity (sometimes called “G-T loss”.)
If we neglect the gravity and drag terms, we can simplify:
and, we can define the rocket mass ratio ( or MR):
In addition, recalling the definition of effective exhaust velocity :
which is the famous form of the rocket equation. This relates specific impulse of the propulsion system (which comes from propellant choice), mass ratio of the rocket, and the resulting total change in velocity, which is driven by mission requirements.
Another commonly used dimensionless quantity is the propellant mass fraction, or the ratio of (initial) propellant mass to overall initial mass:
Example: propellant mass fraction¶
Now that we have the rocket equation, let’s see how much propellant is needed for a launch vehicle taking off from sea level to achieve escape velocity from Earth, which is around 12 km/s.
Find: What percentage of a launch vehicle’s initial mass must be devoted to propellant, if the vehicle’s rocket engine has a specific impulse of 300 s? Neglect gravity and drag effects.
The vehicle is starting at rest on the ground (), so m/s. Using Equation (15) and solving for the mass fraction:
Isp = Q_(300, 's')
g0 = Q_(9.81, 'm/s^2')
delta_V = Q_(12, 'km/s')
R = np.exp(-delta_V / (Isp * g0))
print(f'R = {R.magnitude:.3f}')
propellant_fraction = 1 - R
print(f'propellant mass fraction = {propellant_fraction.magnitude:.3f}')R = 0.017
propellant mass fraction = 0.983
So, assuming all propellant is expended when the burn is completed (when has been achieved), then 98.3% of the inital mass of the vehicle must be propellant!
In general, is difficult to achieve physically, and is probably the upper limit realistically. This is why a single-stage-to-orbit launch vehicle does not exist, and why in general we need multiple stages for launch vehicles.
Effects of gravity¶
Now, let’s consider the vertical launch of a rocket perpendicular to the planet’s surface (in other words, a sounding rocket), considering the effect of gravity but still neglecting drag. The flight/thrust direction is aligned with gravity, with no to worry about. Our goal is to find the velocity at burnout (when thrust has stopped) and the maximum height reached.
There are two flight regimes:
burn (when undergoing thrust), and
coast.
Burnout velocity and height¶
First, let’s find the velocity and height associated with the burn phase. Take Equation (10), dropping the drag term, and integrate from (where and ) to a general time where :
where the final term is called the gravity loss term, or “G-T loss”. This represents the loss in performance caused by thrusting under the effect of gravity. Let’s assume is constant with time (and height); this is not strictly true, as the acceleration due to gravity varies with altitude somewhat, but assuming as constant is a reasonable approximation for now. However, mass does vary substantially with time, based on the propellant mass flow rate :
Integrating the above equation gives velocity as a function of time:
The velocity at the burnout time is then:
To find the height at burnout, we can integrate velocity with respect to time:
so
where the final mass is .
To examine the effects of gravity, let’s look at burnout velocity and height for , a specific impulse of 300 s, and a range of burnout times:
Isp = Q_(300, 's')
mass_ratio = 0.05
burnout_times = Q_(np.linspace(10, 100), 's')
burnout_heights = (
burnout_times * Isp * g0 * (mass_ratio / (1. - mass_ratio) * np.log(mass_ratio) + 1) -
burnout_times**2 * g0 / 2.
)
burnout_velocities = (
-Isp * g0 * np.log(mass_ratio) - g0 * burnout_times
)
burnout_heights_nog = (
burnout_times * Isp * g0 * (mass_ratio / (1. - mass_ratio) * np.log(mass_ratio) + 1)
)
burnout_velocities_nog = (
-Isp * g0 * np.log(mass_ratio)
) * np.ones_like(burnout_times)
fig, axes = plt.subplots(nrows=2, layout='constrained')
axes[0].plot(burnout_times.magnitude, burnout_velocities.to('km/s').magnitude, label='with gravity')
axes[0].plot(burnout_times.magnitude, burnout_velocities_nog.to('km/s').magnitude, label='no gravity')
axes[0].legend(fontsize=14, loc='lower left')
axes[0].set_ylabel('Burnout velocity (km/s)')
axes[1].plot(burnout_times.magnitude, burnout_heights.to('km').magnitude, label='with gravity')
axes[1].plot(burnout_times.magnitude, burnout_heights_nog.to('km').magnitude, label='no gravity')
axes[1].legend(fontsize=14)
axes[1].set_xlabel('Burnout time (s)')
axes[1].set_ylabel('Burnout height (km)')
plt.show()The gravity loss (or gravity “drag”) reduces the burnout velocity, or ΔV, with increasing burnout time. So, in theory, this says that we’d want the shortest possible burnout time.
(Looking at burnout height, the result is a bit more complex: clearly, increasing burnout time means that gravity loss grows with respect to thrusting without the effect of gravity, but overall increasing burnout time leads to increasing burnout height. This is because we acheive height by thrusting over time, since it’s the time integral of velocity.)
Coast height¶
Now, to find the maximum height, we need to consider the coast regime. After the rocket stops thrusting (at burnout), it still has kinetic energy. The coast distance is then based on conversion of this kinetic energy (KE) to potential energy (PE), with subscript b representing burnout location and m representing the maximum height:
and the maximum height is then
Example: escape velocity considering gravity loss, max acceleration¶
Now, let’s take another look at a single-stage rocket trying to achieve escape velocity from Earth (around 12 km/s).
Find: The percentage of a launch vehicle’s initial mass that must be devoted to propellant to achieve escape velocity, including the effects of gravity. Neglect drag, and assume a specific impulse of 300 s. In addition, human passengers onboard must not be exposed to greater than of acceleration.
This problem has some additional complexity and restrictions! First, let’s introduce the concept of thrust-to-weight ratio:
which we can see also indicates the acceleration of the vehicle with respect to gravitational acceleration. For our problem, we are restricted to .
If we assume a constant thrust, due to the decreasing mass of the vehicle the peak acceleration will occur at burnout (), since the mass will be lowest:
We need to find an expression for burnout velocity, based on Equation (21), that incorporates these restrictions, and solve for the mass ratio .
Recalling the definition of thrust, we can express mass flow rate using:
and then express the burnout time based on this mass flow rate expression:
Finally, incorporating this into the burnout velocity Equation (21):
We can attempt to solve this numerically for the mass ratio .
def burnout_velocity_max_accel(mass_ratio, spec_impulse, eta_max):
'''Calculates burnout velocity with a maximum acceleration.
'''
g0 = Q_(9.81, 'm/s^2')
return (spec_impulse * g0 * (
np.log(1./mass_ratio) - (1./mass_ratio - 1.) / eta_max
)
)
def root_burnout_velocity_max_accel(mass_ratio, burnout_velocity, spec_impulse, eta_max):
'''Uses to find root (mass ratio) of burnout velocity equation.
'''
return (
burnout_velocity -
burnout_velocity_max_accel(mass_ratio, spec_impulse, eta_max)
).to('m/s').magnitude
Isp = Q_(300, 's')
delta_V = Q_(12, 'km/s')
eta_max = 6.
sol = root_scalar(
root_burnout_velocity_max_accel, x0=0.05, x1=0.02,
args=(delta_V, Isp, eta_max)
)
print(sol) converged: False
flag: convergence error
function_calls: 52
iterations: 50
root: nan
method: secant
This means that the algorithm is not able to find a solution. Practically, a single-stage rocket is not able to reach this velocity while keeping acceleration limited to a value survivable by humans.
What burnout velocity can we get with acceleration limited to , using the lowest possible practical mass ratio value of 0.05?
mass_ratio = 0.1
Isp = Q_(300, 's')
eta_max = 6.
delta_V = burnout_velocity_max_accel(mass_ratio, Isp, eta_max)
print(f'Highest reasonable ΔV = {delta_V.to('km/s'): .2f}')Highest reasonable ΔV = 2.36 kilometer / second
This is much lower than the escape velocity!
What does this tell us? That a single-stage rocket is not practical for a launch vehicle aiming to leave Earth’s orbit and contain humans.
Effects of drag¶
Looking at Equations (21) and (24) for maximum burnout velocity and height in vacuum (meaning, neglecting the effects of drag caused by atmosphere), it would appear that we want to have the shortest possible burn time, getting the rocket to the burnout velocity faster and earlier.
However, drag force is a strong function of altitude, since air is more dense at lower altitudes. So, we want lower velocity at lower altitudes, because from fluid dynamics, we know how to calculate drag force in general:
where is the atmospheric density (a function of altitude), is the cross-sectional area facing the oncoming flow, and is the drag coefficient, which depends on angle of attack, geometry, and Mach number. We can express Mach number as
For example, Figure 3 shows how the coefficients of lift and drag vary for the German V-2 rocket with Mach number and angle of attack.

Figure 3:Variation of coefficients of drag and lift with Mach number and angle of attack, for the German V-2 rocket. Source: Sutton & Biblarz (2016).
Variation of atmospheric density¶
Atmospheric density varies strongly with altitude, and this needs to be incorporated into the calculation of drag. There are different models for atmospheric properties, but we can approximate the variation of density with a relatively simple model:
where is the height scale of the exponential fall for density (also approximately the troposphere) and is the standard density of air at sea level.
Assuming the atmsophere is hydrostatic, and acceleration due to gravity remains constant, then
Alternatively, a similar exponential expression directly for pressure can be used:
where 101325 Pa is the sea-level standard atmospheric pressure and 8.4 km.
Equations (34) and (35), or (36),
give simple and reasonably useful expressions for the variation of density and pressure
in the atmosphere.
However, more-accurate calculations should use established models. These include the
1976 U.S. Standard Atmosphere NOAA, NASA, and USAF, 1976 and 1975 ISO Standard Atmosphere International Organization for Standardization, 1975, which model how pressure, temperature, density, etc., vary
with altitude in the atmosphere, and provides reasonable answers for up to about 86 km.
The fluids package provides a convenient interface to the 1976 U.S. Standard Atmosphere model in Python Bell, 2020,
along with other models such as the NRLMSISE-00 model Picone et al., 2002, which applies up to 1000 km.
Let’s examine the variation of pressure and temperature through the atmosphere, up to 86 km, using the 1976 U.S. Standard Atmosphere model:
Variation of gravity¶
Acceleration due to gravity also changes with altitude, but this is less significant until reaching more-significant altitudes:
where = 6378.388 km is the mean radius of Earth, and = 9.80665 m/s2 is the standard gravitational acceleration at sea level.
Of course, the Earth neither has a constant radius, nor is it actually a sphere. (In fact, it is an oblate spheroid, with a slight bulge at the Equator and flatter at the poles.)
However, even neglecting these features, gravity only varies a small amount with altitude. Consider variation from the surface to the approximate orbit of the International Space Station (around 250 miles):
R0 = Q_(6378.388, 'km')
h_ISS = Q_(250, 'miles').to('km')
altitudes = np.linspace(0., h_ISS.magnitude, num=100) * ureg.km
gravities = g0 * (R0 / (R0 + altitudes))**2
fig, ax = plt.subplots(layout='constrained')
ax.plot(altitudes.magnitude, gravities.to('m/s^2').magnitude)
ax.set_xlabel('Altitude (km)')
ax.set_ylabel(r'Gravitational acceleration (m/s$^2$)')
plt.show()In other words, even in orbit, the astronauts on the space station are experiencing ~88% the same acceleration due to gravity that we are on the surface.
The difference is that they are orbiting at a substantial angular velocity, with the resulting centrifugal force counteracting gravity and leading to the experience of weightlessness.
Example: Numerical integration of vertical launch¶
Determine the burnout velocity and maximum height achieved by launching the German V-2 rocket, assuming a vertical launch. Take into account drag, the variation of atmospheric density with altitude, and the variation of gravity with altitude. Use an explicit numerical method to solve the system of equations.
Given data:
specific impulse: = 250 sec
initial mass: = 12700 kg
propellant mass: = 8610 kg
burn time: = 60 sec
maximum body diameter: = 5 feet 4 inches (5.333 ft)
height: = 46 feet
Let’s set up the system of ordinary differential equations for velocity (), altitude (), and mass () that describes this problem:
where we obtained the last equation by recognizing that
for steady burning. In the above, is the reference acceleration due to gravity (9.80065 m/s2), is the atmospheric density, is the cross-sectional area of the body, is the drag coefficient, and is the (varying) acceleration due to gravity.
Density and gravity: To move forward, we need to describe how to evaluate density, gravitational acceleration, and the coefficient of drag at any instant in time. The first two are known functions of altitude:
where = 1.225 kg/m3 is the density at sea level, = 10.4 km is the height scale of the exponential fall, = 6378.388 km is the mean radius of Earth, and = 9.80665 m/s2 is the sea-level acceleration due to gravity.
Coefficient of drag¶
Coefficient of drag is more difficult to handle, since it depends on angle of attack and Mach number: . For vertical flight, the angle of attack is zero (), but the Mach number will be changing both as the vehicle speed changes and the atmospheric conditions change with altitude:
Option 1: As a first approximation, we could simply assume a constant value for coefficient of drag. For example, before and after the transonic regime (where Mach number approaches 1), .
Option 2: If we want to more-accurately capture the increase in coefficient of drag as the vehicle approaches a sonic velocity, and then the relaxation when supersonic, we need to capture the dependence of on Mach number.
First, we already have an expression for , but we also need an expression for pressure. We can use a similar exponential approximation:
where = 101325 Pa is the sea-level standard atmospheric pressure and = 8.4 km. With being a reasonable constant, we can now calculate Mach number based on velocity and altitude.
Now, we need to obtain a functional dependence of ; based on Figure 3, we see that the relationship is fairly complex. The “easiest” way to create a function for is to:
Extract data points from the plot manually, using a tool such as WebPlotDigitizer
Perform a spline interpolation on the data points, using a function such as
splinein Matlab orscipy.interpolate.UnivariateSplinein Python.
Let’s see this in action, using data extracted manually from the source figure:
import numpy as np
from scipy.interpolate import UnivariateSpline
data = np.genfromtxt('coefficient-drag.csv', delimiter=',')
coefficient_drag = UnivariateSpline(data[:,0], data[:,1], s=0, ext='const')
mach = np.linspace(0.2, 6, num=200, endpoint=True)
plt.plot(data[:,0], data[:,1], 'o', mach, coefficient_drag(mach), '-')
plt.xlabel('Mach number')
plt.ylabel('Coefficient of drag')
plt.legend(['Data', 'Cubic spline'])
plt.show()Now that we have a fully defined system of equations, we need a numerical method to integrate.
We can use the solve_ivp function from SciPy, and specify the RK45 method (fourth-order Runge–Kutta, using an embedded fifth-order method to control error and adapt the time step size).
To implement, we need to create a function to evaluate the time derivatives, and then specify initial conditions for velocity, altitude, and mass: , , and .
from scipy.integrate import solve_ivp
def vertical_launch(t, y, spec_impulse, mass_prop, time_burn, diameter, coef_drag):
'''Evaluates system of time derivatives for velocity, altitude, and mass.
'''
radius_earth = 6378.388 * 1e3
gravity_ref = 9.80665
density_ref = 1.225
pressure_ref = 101325
gamma = 1.4
height_den = 10400
height_pres = 8400
v = y[0]
h = y[1]
m = y[2]
gravity = gravity_ref * (radius_earth / (radius_earth + h))**2
density = density_ref * np.exp(-h / height_den)
pressure = pressure_ref * np.exp(-h / height_pres)
mach = v / np.sqrt(gamma * pressure / density)
area = np.pi * diameter**2 / 4
drag = 0.5 * density * v**2 * area * coef_drag(mach)
dmdt = -mass_prop / time_burn
dvdt = (-spec_impulse * gravity_ref / m) * dmdt - drag / m - gravity
dhdt = v
return [dvdt, dhdt, dmdt]
# given constants
spec_impulse = 250
mass_initial = 12700
mass_propellant = 8610
time_burn = 60
diameter = 1.626
sol = solve_ivp(
vertical_launch, [0, time_burn], [0, 0, mass_initial], method='RK45',
args=(spec_impulse, mass_propellant, time_burn, diameter, coefficient_drag),
dense_output=True
)time = np.linspace(0, time_burn, 100)
z = sol.sol(time)
fig, axes = plt.subplots(2, 1)
axes[0].plot(time, z[0, :])
axes[0].set_ylabel('Velocity (m/s)')
axes[0].grid(True)
axes[1].plot(time, z[1, :] / 1000)
axes[1].set_ylabel('Altitude (km)')
axes[1].set_xlabel('Time (s)')
axes[1].grid(True)
plt.show()
print(f'Burnout velocity: {z[0,-1]: 5.2f} m/s')
print(f'Burnout altitude: {z[1,-1]/1000: 5.2f} km')Burnout velocity: 1956.11 m/s
Burnout altitude: 44.54 km
ΔV required for circular orbit¶
Figure 4:Diagram of a satellite in a circular orbit at velocity around the Earth, with average radius of the planet , with the orbital distance , where is the altitude from the surface.
A common mission design will involve a rocket that needs to achieve sufficient to put a satellite into orbit. (In general, stable orbits are elliptic, but that topic is more appropriate for an orbital mechanics course.)
The velocity associated with a circular orbit ensure that centrifugal force counteracts the force of gravity (i.e., weight):
however, we do need to consider the variation in gravity with altitude. At the surface, where , then , but in general we know gravity varies with altitude as expressed by Equation (37), and we can express the radial distance of the orbit as .
Incorporating these, we can find:
Alternatively, we can express via the gravitational parameter , which is the product of the universal gravitational constant with the mass of the Earth :
where
Then, assuming zero initial velocity , the needed to achieve a circular orbit at altitude is
Compare this with the escape velocity, needed to leave the influence of Earth’s gravity:
As an example, if the ISS is orbiting at an average altitude of 415 km, we can calculate its orbital speed:
h_ISS = Q_(415, 'km')
R0 = Q_(6378.388, 'km')
mu = Q_(3.986004e14, 'm^3/s^2')
u_ISS = np.sqrt(mu / (R0 + h_ISS))
print(f'ISS orbital speed: {u_ISS.to('km/s'): .2f}')ISS orbital speed: 7.66 kilometer / second
- Sutton, G. P., & Biblarz, O. (2016). Rocket Propulsion Elements (9th ed.). John Wiley & Sons.
- NOAA, NASA, and USAF. (1976). U.S. Standard Atmosphere, 1976. https://ntrs.nasa.gov/citations/19770009539
- International Organization for Standardization. (1975). ISO 2533:1975 - Standard Atmosphere. https://www.iso.org/standard/7472.html
- Bell, C. (2020). fluids: Fluid dynamics component of Chemical Engineering Design Library (ChEDL). https://github.com/CalebBell/fluids
- Picone, J. M., Hedin, A. E., Drob, D. P., & Aikin, A. C. (2002). NRLMSISE-00 empirical model of the atmosphere: Statistical comparisons and scientific issues. Journal of Geophysical Research: Space Physics, 107(A12), SIA 15-1-SIA 15-16. 10.1029/2002ja009430
- Pavlis, N. K., Holmes, S. A., Kenyon, S. C., & Factor, J. K. (2012). The development and evaluation of the Earth Gravitational Model 2008 (EGM2008). Journal of Geophysical Research: Solid Earth, 117(B4). 10.1029/2011jb008916