Examples of Satellite Orbits

Low Earth Orbit (LEO)

  • Altitude ~160-2000 km
  • One example of a LEO satellite is the Starlink satellite, STARLINK-33612 (NORAD CAT ID 63325), which is part of SpaceX’s Starlink constellation.
  • The approximate orbital parameters for this satellite at some time are: (From space-track.org)

\(a = 6718.827 \, \text{km}; \, e = 0.00019440; \, i = 43.0000^\circ; \, \text{RAAN} = 156.5261^\circ; \, \omega = 261.3203^\circ; \, \theta = 30.0^\circ.\)

The orbit of this satellite plotted using 2 body (Keplerian) dynamics is shown below:

LEO satellite: Starlink 33612 orbit

Figure 7.1: LEO satellite: Starlink 33612 orbit

Medium Earth Orbit (MEO)

  • Altitude ~2000 km to ~35,786 km
  • One example of a MEO satellite is the GPS satellite, NAVSTAR 76 (USA 266) (NORAD CAT ID 41328), which is part of the NAVSTAR GPS constellation.
  • The approximate orbital parameters for this satellite at some time are: (From space-track.org)

\(a = 26559.490 \, \text{km}; \, e = 0.00874290; \, i = 55.2608^\circ; \, \text{RAAN} = 106.9860^\circ; \, \omega = 242.0236^\circ; \, \theta = 30.0^\circ.\)

The orbit of this satellite plotted using 2 body (Keplerian) dynamics is shown below:

MEO satellite: NAVSTAR 76 orbit

Figure 7.2: MEO satellite: NAVSTAR 76 orbit

Geosynchronous Orbit

  • These orbits have a period equal to the Earth’s rotation period (approximately 24 hours) but are not necessarily circular or in the equatorial plane.
  • One possible exapmple of state of a satellite in geosynchronous orbit is given below:

\(a = 42164.000 \, \text{km}; \, e = 0.100000; \, i = 15.0000^\circ; \, \text{RAAN} = 40.0000^\circ; \, \omega = 0.0000^\circ; \, \theta = 30.0^\circ.\)

The orbit of this satellite plotted using 2 body (Keplerian) dynamics is shown below:

Geosynchronous satellite orbit

Figure 7.3: Geosynchronous satellite orbit

Geostationary Orbit (GEO)

  • Altitude ~35,786 km
  • A geostationary orbit is a special case of geosynchronous orbit where the orbit is circular and lies exactly in the Earth’s equatorial plane, so the satellite appears fixed in the sky.
  • One possible exapmple of state of a satellite in geostationary orbit is given below:

\(a = 42164.000 \, \text{km}; \, e = 0.000000; \, i = 0.0000^\circ; \, \text{RAAN} = 0.0000^\circ; \, \omega = 0.0000^\circ; \, \theta = 30.0^\circ.\)

The orbit of this satellite plotted using 2 body (Keplerian) dynamics is shown below:

Geostationary satellite orbit

Figure 7.4: Geostationary satellite orbit

Highly Elliptical Orbit (HEO) – Molniya Orbit

  • Molniya (“lightning”) was a military communications satellite system used by the Soviet Union. The satellites used highly eccentric elliptical orbits of approximately 63.4 degrees inclination and orbital period of about 12 hours (called Molniya orbits), which allowed them to be visible to near-polar regions for long periods.
  • One example of a Molniya satellite is, Molniya 3-50 (NORAD CAT ID 25847).
  • The approximate orbital parameters for this satellite at some time are: (From space-track.org)

\(a = 26553.584 \, \text{km}; \, e = 0.68462900; \, i = 63.5126^\circ; \, \text{RAAN} = 201.1207^\circ; \, \omega = 280.6099^\circ; \, \theta = 30.0000^\circ.\)

The orbit of this satellite plotted using 2 body (Keplerian) dynamics is shown below:

HEO Satellite: Molniya 3-50 orbit

Figure 7.5: HEO Satellite: Molniya 3-50 orbit

Codes used

The Python code used for plotting the orbits is given below: (With example of LEO satellite)

Code
# By Suyog Soman (MTech, AFM 2023-2025) using GPT-4o

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D  # necessary for 3d plotting

def class2cart(a, e, i, RAAN, omega, theta):
    """
    Converts classical orbital elements to Cartesian coordinates (ECI frame).

    Parameters:
        a     : float
                Semi-major axis [km]
        e     : float
                Eccentricity
        i     : float
                Inclination [rad]
        RAAN  : float
                Right ascension of ascending node [rad]
        omega : float
                Argument of periapsis [rad]
        theta : float
                True anomaly [rad]

    Returns:
        r : numpy array (3,)
            Position vector in km
        v : numpy array (3,)
            Velocity vector in km/s
    """
    mu = 398600  # Earth's gravitational parameter [km^3/s^2]

    # Distance from the central body in the perifocal frame
    r_pqw = (a * (1 - e**2)) / (1 + e * np.cos(theta)) * np.array([np.cos(theta), np.sin(theta), 0])
    
    # Velocity in perifocal frame
    v_pqw = np.sqrt(mu / (a * (1 - e**2))) * np.array([-np.sin(theta), e + np.cos(theta), 0])
    
    # Rotation matrices from perifocal to ECI frame:
    R3_W = np.array([
        [np.cos(RAAN), -np.sin(RAAN), 0],
        [np.sin(RAAN),  np.cos(RAAN), 0],
        [0,             0,            1]
    ])
    R1_i = np.array([
        [1, 0,           0],
        [0, np.cos(i), -np.sin(i)],
        [0, np.sin(i),  np.cos(i)]
    ])
    R3_w = np.array([
        [np.cos(omega), -np.sin(omega), 0],
        [np.sin(omega),  np.cos(omega), 0],
        [0,              0,             1]
    ])
    
    # Complete rotation matrix from perifocal to ECI frame
    R = R3_W @ R1_i @ R3_w
    
    # Transform position and velocity vectors to the ECI frame
    r = R @ r_pqw
    v = R @ v_pqw

    return r, v

def propagate_orbit(a, e, i, RAAN, omega, theta0, dt):
    """
    Propagates the orbit using Kepler's classical method of analytical orbit propagation.

    Parameters:
        a     : float
                Semi-major axis [km]
        e     : float
                Eccentricity
        i     : float
                Inclination [rad]
        RAAN  : float
                Right Ascension of the Ascending Node [rad]
        omega : float
                Argument of periapsis [rad]
        theta0: float
                Initial true anomaly [rad]
        dt    : float
                Propagation time [s]
    
    Returns:
        a, e, i, RAAN, omega, theta_final : tuple of floats
            Updated orbital elements (only theta changes for two-body propagation).
    """
    mu = 398600.0  # Earth's gravitational parameter [km^3/s^2]
    
    # Mean motion (n) [rad/s]
    n = np.sqrt(mu/(a**3))
    
    # Compute the initial eccentric anomaly (E0) from theta0
    E0 = 2 * np.arctan(np.sqrt((1 - e)/(1 + e)) * np.tan(theta0/2))
    if E0 < 0:
        E0 += 2*np.pi
    
    # Compute the initial mean anomaly (M0)
    M0 = E0 - e*np.sin(E0)
    
    # Compute the final mean anomaly (Mf)
    Mf = M0 + n*dt
    
    # Solve Kepler's equation: Ef - e*sin(Ef) = Mf using Newton-Raphson iteration
    Ef = Mf  # initial guess
    tol = 1e-8
    error = 1.0
    while np.abs(error) > tol:
        error = (Ef - e*np.sin(Ef) - Mf) / (1 - e*np.cos(Ef))
        Ef -= error
    
    # Compute the final true anomaly from Ef
    theta_final = 2 * np.arctan(np.sqrt((1+e)/(1-e)) * np.tan(Ef/2))
    if theta_final < 0:
        theta_final += 2*np.pi
    
    # In a two-body problem, other orbital elements remain constant.
    return a, e, i, RAAN, omega, theta_final

if __name__ == "__main__":
    # Define the initial classical orbital elements
    a = 6718.827              # Semi-major axis in km
    e = 0.00019440            # Eccentricity
    i = np.deg2rad(43.0000)     # Inclination in radians
    RAAN = np.deg2rad(156.5261)  # RAAN in radians
    omega = np.deg2rad(261.3203) # Argument of periapsis in radians
    theta0 = np.deg2rad(30.0)   # Initial true anomaly in radians

    mu = 398600.0  # Earth's gravitational parameter [km^3/s^2]
    # Calculate orbital period: T = 2*pi * sqrt(a^3/mu)
    T = 2 * np.pi * np.sqrt(a**3 / mu)
    print(f"Orbital period (s): {T:.2f}")

    # Define propagation parameters: propagate over one orbital period
    dt = 60              # time step in seconds (1 minute)
    num_steps = int(np.ceil(T / dt))

    # Lists to store trajectory for plotting
    rx_list = []
    ry_list = []
    rz_list = []

    # Set the initial time and orbital state
    current_theta = theta0
    
    # Get initial state
    r, _ = class2cart(a, e, i, RAAN, omega, current_theta)
    rx_list.append(r[0])
    ry_list.append(r[1])
    rz_list.append(r[2])
    
    # Propagate the orbit step by step
    for step in range(1, num_steps + 1):
        # Propagate for dt seconds
        a, e, i, RAAN, omega, new_theta = propagate_orbit(a, e, i, RAAN, omega, current_theta, dt)
        current_theta = new_theta  # update true anomaly for next step
        
        # Convert the updated orbital elements to Cartesian coordinates
        r, _ = class2cart(a, e, i, RAAN, omega, current_theta)
        
        # Save the state for plotting
        rx_list.append(r[0])
        ry_list.append(r[1])
        rz_list.append(r[2])

    # Plotting the 3D orbit with Earth at the centre
    fig = plt.figure(figsize=(10, 8))
    ax = fig.add_subplot(111, projection='3d')
    
    # Plot the orbit trajectory
    ax.plot(rx_list, ry_list, rz_list, label='Orbit Trajectory', color='b')
    
    # Plot Earth as a sphere
    earth_radius = 6378  # Earth's radius in km
    u, v_grid = np.mgrid[0:2*np.pi:50j, 0:np.pi:25j]
    x_sphere = earth_radius * np.cos(u) * np.sin(v_grid)
    y_sphere = earth_radius * np.sin(u) * np.sin(v_grid)
    z_sphere = earth_radius * np.cos(v_grid)
    ax.plot_surface(x_sphere, y_sphere, z_sphere, color='c', alpha=0.5, linewidth=0)
    
    # Setting labels and title
    ax.set_xlabel('X (km)')
    ax.set_ylabel('Y (km)')
    ax.set_zlabel('Z (km)')
    ax.set_title('Orbit Propagation over One Orbital Period')
    ax.legend()
    ax.grid(True)
    
    # Set equal aspect ratio for all axes
    max_val = np.max(np.abs([rx_list, ry_list, rz_list]))
    ax.set_xlim([-max_val, max_val])
    ax.set_ylim([-max_val, max_val])
    ax.set_zlim([-max_val, max_val])
    
    plt.show()
Orbital period (s): 5480.89

Plotting all in one fig.

Code
# By Suyog Soman (MTech, AFM 2023-2025) using GPT-4o
import numpy as np
import plotly.graph_objects as go

def class2cart(a, e, i, RAAN, omega, theta):
    """
    Converts classical orbital elements to Cartesian coordinates (ECI frame).
    """
    mu = 398600  # Earth's gravitational parameter [km^3/s^2]
    # Position in perifocal frame
    r_pqw = (a * (1 - e**2)) / (1 + e * np.cos(theta)) * np.array([np.cos(theta), np.sin(theta), 0])
    # Velocity in perifocal frame
    v_pqw = np.sqrt(mu / (a * (1 - e**2))) * np.array([-np.sin(theta), e + np.cos(theta), 0])
    
    # Rotation matrices for perifocal to ECI conversion:
    R3_W = np.array([
        [np.cos(RAAN), -np.sin(RAAN), 0],
        [np.sin(RAAN),  np.cos(RAAN), 0],
        [0,             0,            1]
    ])
    R1_i = np.array([
        [1, 0,           0],
        [0, np.cos(i), -np.sin(i)],
        [0, np.sin(i),  np.cos(i)]
    ])
    R3_w = np.array([
        [np.cos(omega), -np.sin(omega), 0],
        [np.sin(omega),  np.cos(omega), 0],
        [0,              0,             1]
    ])
    R = R3_W @ R1_i @ R3_w  # overall rotation matrix
    
    r = R @ r_pqw
    v = R @ v_pqw
    return r, v

def propagate_orbit(a, e, i, RAAN, omega, theta0, dt):
    """
    Propagates the orbit by dt seconds using Kepler's method.
    Only the true anomaly changes for a two-body orbit.
    """
    mu = 398600.0  # Earth's gravitational parameter [km^3/s^2]
    n = np.sqrt(mu / (a**3))
    
    # Convert initial true anomaly to eccentric anomaly
    E0 = 2 * np.arctan(np.sqrt((1 - e)/(1 + e)) * np.tan(theta0/2))
    if E0 < 0:
        E0 += 2*np.pi
    M0 = E0 - e*np.sin(E0)
    Mf = M0 + n*dt
    
    # Solve Kepler's Equation using Newton-Raphson iteration
    Ef = Mf
    tol = 1e-8
    error = 1.0
    while np.abs(error) > tol:
        error = (Ef - e*np.sin(Ef) - Mf) / (1 - e*np.cos(Ef))
        Ef -= error
    
    theta_final = 2 * np.arctan(np.sqrt((1+e)/(1-e)) * np.tan(Ef/2))
    if theta_final < 0:
        theta_final += 2*np.pi
        
    return a, e, i, RAAN, omega, theta_final

# Define satellite orbits using a list of dictionaries.
# Angles are given in degrees and will be converted to radians.
satellites = [
    {"name": "LEO: STARLINK-33612", 
     "a": 6718.827, 
     "e": 0.00019440, 
     "i": np.deg2rad(43.0), 
     "RAAN": np.deg2rad(156.5261), 
     "omega": np.deg2rad(261.3203), 
     "theta": np.deg2rad(30.0)},
    
    {"name": "MEO: GPS NAVSTAR 76", 
     "a": 26559.490, 
     "e": 0.00874290, 
     "i": np.deg2rad(55.2608), 
     "RAAN": np.deg2rad(106.9860), 
     "omega": np.deg2rad(242.0236), 
     "theta": np.deg2rad(30.0)},
    
    {"name": "Geosynchronous", 
     "a": 42164.000, 
     "e": 0.100000, 
     "i": np.deg2rad(15.0), 
     "RAAN": np.deg2rad(40.0), 
     "omega": np.deg2rad(0.0), 
     "theta": np.deg2rad(30.0)},
    
    {"name": "Geostationary", 
     "a": 42164.000, 
     "e": 0.000000, 
     "i": np.deg2rad(0.0), 
     "RAAN": np.deg2rad(0.0), 
     "omega": np.deg2rad(0.0), 
     "theta": np.deg2rad(30.0)},
    
    {"name": "HEO: Molniya 3-50", 
     "a": 26553.584, 
     "e": 0.68462900, 
     "i": np.deg2rad(63.5126), 
     "RAAN": np.deg2rad(201.1207), 
     "omega": np.deg2rad(280.6099), 
     "theta": np.deg2rad(30.0)}
]

mu = 398600.0  # km^3/s^2

# Prepare a list to hold Plotly traces
traces = []
colors = ['blue', 'green', 'red', 'magenta', 'black']

for idx, sat in enumerate(satellites):
    a = sat["a"]
    e = sat["e"]
    i_val = sat["i"]
    RAAN = sat["RAAN"]
    omega = sat["omega"]
    theta0 = sat["theta"]
    
    # Calculate the orbital period for the satellite
    T = 2 * np.pi * np.sqrt(a**3 / mu)
    dt = 60  # time step in seconds
    num_steps = int(np.ceil(T / dt))
    
    rx_list, ry_list, rz_list = [], [], []
    current_theta = theta0
    
    # Save initial position
    r, _ = class2cart(a, e, i_val, RAAN, omega, current_theta)
    rx_list.append(r[0])
    ry_list.append(r[1])
    rz_list.append(r[2])
    
    # Propagate orbit over one period
    for step in range(1, num_steps + 1):
        a, e, i_val, RAAN, omega, new_theta = propagate_orbit(a, e, i_val, RAAN, omega, current_theta, dt)
        current_theta = new_theta
        r, _ = class2cart(a, e, i_val, RAAN, omega, current_theta)
        rx_list.append(r[0])
        ry_list.append(r[1])
        rz_list.append(r[2])
    
    # Create a trace for the satellite orbit
    trace = go.Scatter3d(
        x=rx_list,
        y=ry_list,
        z=rz_list,
        mode='lines',
        line=dict(color=colors[idx % len(colors)], width=2),
        name=sat["name"]
    )
    traces.append(trace)

# Create an Earth sphere
earth_radius = 6378  # km
u = np.linspace(0, 2*np.pi, 50)
v = np.linspace(0, np.pi, 25)
u, v = np.meshgrid(u, v)
x_sphere = earth_radius * np.cos(u) * np.sin(v)
y_sphere = earth_radius * np.sin(u) * np.sin(v)
z_sphere = earth_radius * np.cos(v)

earth_trace = go.Surface(
    x=x_sphere, 
    y=y_sphere, 
    z=z_sphere, 
    colorscale='Blues',
    opacity=0.5,
    showscale=False,
    name='Earth'
)

# Create the figure with all traces
fig = go.Figure(data=traces + [earth_trace])
fig.update_layout(
    title="Orbits of Satellites",
    scene=dict(
        xaxis_title='X (km)',
        yaxis_title='Y (km)',
        zaxis_title='Z (km)',
        aspectmode='data'
    ),
    legend=dict(x=0.02, y=0.98)
)
fig.show()