Note

This page was generated from a Jupyter notebook.

JSBSim Flight Simulation

This notebook demonstrates a trimmed flight simulation with the Cessna 172P using the JSBSim Python API. We record a set of key flight parameters over a 60-second time window and visualise them as time-history plots.

Topics covered

  • Running a longer simulation loop with data collection.

  • Working with JSBSim’s property tree (positions, velocities, attitudes, forces).

  • Unit conversions (feet → metres, knots → m/s, radians → degrees).

  • Producing publication-quality plots with matplotlib.

1. Setup

[1]:
# If running on Google Colab, install the required packages.

import sys

if 'google.colab' in sys.modules:
    print('Running on Google Colab \u2013 installing jsbsim \u2026')
    !pip install jsbsim
[2]:
import math
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import jsbsim

matplotlib.rcParams.update({
    'figure.dpi': 120,
    'axes.grid': True,
    'grid.alpha': 0.4,
})

print(f"JSBSim version : {jsbsim.__version__}")
print(f"NumPy version  : {np.__version__}")
print(f"Matplotlib ver : {matplotlib.__version__}")
JSBSim version : 1.3.0
NumPy version  : 2.4.4
Matplotlib ver : 3.10.8

2. Initialise JSBSim and trim

[3]:
# ---------- configuration ----------
AIRCRAFT      = 'c172p'
ALT_FT        = 5000.0   # Initial altitude [ft MSL]
AIRSPEED_KTS  = 90.0     # Calibrated airspeed [kts]
HEADING_DEG   = 90.0     # True heading [deg]
SIM_DURATION  = 60.0     # Simulation duration [s]
RECORD_HZ     = 10       # Data recording frequency [Hz]
# -----------------------------------

fdm = jsbsim.FGFDMExec(None)
fdm.set_debug_level(0)
fdm.load_model(AIRCRAFT)

fdm['ic/h-sl-ft']      = ALT_FT
fdm['ic/vc-kts']       = AIRSPEED_KTS
fdm['ic/gamma-deg']    = 0.0
fdm['ic/psi-true-deg'] = HEADING_DEG
fdm.run_ic()

fdm['propulsion/set-running'] = -1
fdm['simulation/do_simple_trim'] = 1

dt          = fdm.get_delta_t()
record_step = max(1, int(1.0 / (RECORD_HZ * dt)))

print(f"Aircraft       : {AIRCRAFT}")
print(f"Alt (MSL)      : {ALT_FT:.0f} ft")
print(f"Airspeed       : {AIRSPEED_KTS:.0f} kts KCAS")
print(f"Time step Δt   : {dt:.6f} s  ({1/dt:.0f} Hz)")
print(f"Record step    : every {record_step} steps ({RECORD_HZ} Hz)")
print(f"Trim AoA       : {fdm['aero/alpha-deg']:.2f} deg")
print(f"Trim throttle  : {fdm['fcs/throttle-cmd-norm[0]']:.3f}")


     JSBSim Flight Dynamics Model v1.3.0 Apr  9 2026 10:00:08
            [JSBSim-ML v2.0]

JSBSim startup beginning ...

Aircraft       : c172p
Alt (MSL)      : 5000 ft
Airspeed       : 90 kts KCAS
Time step Δt   : 0.008333 s  (120 Hz)
Record step    : every 12 steps (10 Hz)
Trim AoA       : 1.28 deg
Trim throttle  : 0.694

3. Run the simulation and record data

[4]:
# Containers for recorded data
rec = {
    't_s':        [],
    'alt_ft':     [],
    'alt_m':      [],
    'vt_fps':     [],
    'vc_kts':     [],
    'alpha_deg':  [],
    'theta_deg':  [],
    'phi_deg':    [],
    'psi_deg':    [],
    'nx':         [],   # load factor (longitudinal)
    'nz':         [],   # load factor (normal)
    'lat_deg':    [],
    'lon_deg':    [],
}

total_steps = int(SIM_DURATION / dt)
step = 0

while step < total_steps:
    fdm.run()
    step += 1

    if step % record_step == 0:
        rec['t_s'].append(fdm['simulation/sim-time-sec'])
        rec['alt_ft'].append(fdm['position/h-sl-ft'])
        rec['alt_m'].append(fdm['position/h-sl-ft'] * 0.3048)
        rec['vt_fps'].append(fdm['velocities/vt-fps'])
        rec['vc_kts'].append(fdm['velocities/vc-kts'])
        rec['alpha_deg'].append(fdm['aero/alpha-deg'])
        rec['theta_deg'].append(fdm['attitude/theta-deg'])
        rec['phi_deg'].append(fdm['attitude/phi-deg'])
        rec['psi_deg'].append(fdm['attitude/psi-deg'])
        rec['nx'].append(fdm['accelerations/Nx'])
        rec['nz'].append(fdm['accelerations/Nz'])
        rec['lat_deg'].append(math.degrees(fdm['position/lat-geod-rad']))
        rec['lon_deg'].append(math.degrees(fdm['position/long-gc-rad']))

# Convert to NumPy arrays for convenient indexing
for key in rec:
    rec[key] = np.array(rec[key])

print(f"Recorded {len(rec['t_s'])} samples over {SIM_DURATION:.0f} s")
print(f"Final altitude : {rec['alt_ft'][-1]:.1f} ft")
print(f"Final airspeed : {rec['vc_kts'][-1]:.1f} kts KCAS")
Recorded 600 samples over 60 s
Final altitude : 5000.2 ft
Final airspeed : 90.0 kts KCAS

4. Visualise – altitude and airspeed

[5]:
fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)

axes[0].plot(rec['t_s'], rec['alt_ft'], color='steelblue', linewidth=1.5)
axes[0].set_ylabel('Altitude (ft MSL)')
axes[0].set_title(f'{AIRCRAFT.upper()} – Trimmed Level Flight  '
                  f'({ALT_FT:.0f} ft, {AIRSPEED_KTS:.0f} kts)')

axes[1].plot(rec['t_s'], rec['vc_kts'], color='darkorange', linewidth=1.5)
axes[1].set_ylabel('Calibrated Airspeed (kts)')
axes[1].set_xlabel('Time (s)')

plt.tight_layout()
plt.savefig('altitude_airspeed.png', bbox_inches='tight')
plt.show()
print("Figure saved as altitude_airspeed.png")
../_images/notebooks_02_jsbsim_flight_simulation_9_0.png
Figure saved as altitude_airspeed.png

5. Visualise – attitude angles

[6]:
fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)

axes[0].plot(rec['t_s'], rec['theta_deg'], color='mediumseagreen', linewidth=1.5)
axes[0].set_ylabel('Pitch θ (deg)')
axes[0].set_title('Attitude Time History')

axes[1].plot(rec['t_s'], rec['phi_deg'], color='crimson', linewidth=1.5)
axes[1].set_ylabel('Roll φ (deg)')

axes[2].plot(rec['t_s'], rec['psi_deg'], color='mediumpurple', linewidth=1.5)
axes[2].set_ylabel('Heading ψ (deg)')
axes[2].set_xlabel('Time (s)')

plt.tight_layout()
plt.savefig('attitude.png', bbox_inches='tight')
plt.show()
../_images/notebooks_02_jsbsim_flight_simulation_11_0.png

6. Visualise – ground track

[7]:
fig, ax = plt.subplots(figsize=(7, 7))

sc = ax.scatter(
    rec['lon_deg'], rec['lat_deg'],
    c=rec['t_s'], cmap='plasma', s=6, zorder=3
)
cbar = fig.colorbar(sc, ax=ax, label='Time (s)')

ax.plot(rec['lon_deg'][0], rec['lat_deg'][0],
        'go', ms=10, label='Start', zorder=4)
ax.plot(rec['lon_deg'][-1], rec['lat_deg'][-1],
        'rs', ms=10, label='End', zorder=4)

ax.set_xlabel('Longitude (deg)')
ax.set_ylabel('Latitude (deg)')
ax.set_title('Ground Track')
ax.legend()

x_min, x_max = ax.get_xlim()
y_min, y_max = ax.get_ylim()
span = max(x_max - x_min, y_max - y_min)
x_center = 0.5 * (x_min + x_max)
y_center = 0.5 * (y_min + y_max)
ax.set_xlim(x_center - 0.5 * span, x_center + 0.5 * span)
ax.set_ylim(y_center - 0.5 * span, y_center + 0.5 * span)
ax.set_aspect('equal', adjustable='box')

plt.tight_layout()
plt.savefig('ground_track.png', bbox_inches='tight')
plt.show()
../_images/notebooks_02_jsbsim_flight_simulation_13_0.png

7. Reset and re-run: 10-degree turn to the left

JSBSim can be reset and re-trimmed to explore different manoeuvres. Here we apply a step aileron input to initiate a gentle left turn.

[8]:
# Reset
fdm.reset_to_initial_conditions(0)
fdm['propulsion/set-running'] = -1
fdm['simulation/do_simple_trim'] = 1

turn_rec = {'t_s': [], 'phi_deg': [], 'psi_deg': [], 'alt_ft': []}

total_steps = int(30.0 / dt)
for step in range(total_steps):
    # Apply a gentle left aileron deflection for the first 5 s
    if fdm['simulation/sim-time-sec'] < 5.0:
        fdm['fcs/aileron-cmd-norm'] = -0.15
    else:
        fdm['fcs/aileron-cmd-norm'] = 0.0

    fdm.run()

    if step % record_step == 0:
        turn_rec['t_s'].append(fdm['simulation/sim-time-sec'])
        turn_rec['phi_deg'].append(fdm['attitude/phi-deg'])
        turn_rec['psi_deg'].append(fdm['attitude/psi-deg'])
        turn_rec['alt_ft'].append(fdm['position/h-sl-ft'])

for key in turn_rec:
    turn_rec[key] = np.array(turn_rec[key])

fig, axes = plt.subplots(2, 1, figsize=(10, 6), sharex=True)

axes[0].plot(turn_rec['t_s'], turn_rec['phi_deg'], color='crimson', linewidth=1.5)
axes[0].axvline(5.0, linestyle='--', color='grey', alpha=0.7, label='Aileron released')
axes[0].set_ylabel('Roll φ (deg)')
axes[0].set_title('Aileron Step Input – Roll and Heading Response')
axes[0].legend()

axes[1].plot(turn_rec['t_s'], turn_rec['psi_deg'], color='mediumpurple', linewidth=1.5)
axes[1].set_ylabel('Heading ψ (deg)')
axes[1].set_xlabel('Time (s)')

plt.tight_layout()
plt.savefig('turn_manoeuvre.png', bbox_inches='tight')
plt.show()
print("Turn manoeuvre complete")
../_images/notebooks_02_jsbsim_flight_simulation_15_0.png
Turn manoeuvre complete

Summary

In this notebook you:

  • Ran a 60-second trimmed level-flight simulation.

  • Recorded and plotted altitude, airspeed, attitude, and ground track.

  • Demonstrated how to apply control inputs (aileron step) and observe the aircraft’s response.

Next: 03_pathsim_intro.ipynb – introduction to PathSim.