Note
This page was generated from a Jupyter notebook.
Thrust Vectoring Analysis
Based on a NASA report - Optimal Pitch Thrust-Vector Angle and Benefits for all Flight Regimes
Use JSBSim to compare how varying the thrust vector angle can minimize fuel burn for a given flight condition and compare the results to the NASA report.
Tests performed for a cruise condition and for a climb condition.
[1]:
import sys
if 'google.colab' in sys.modules:
print('Running on Google Colab – installing jsbsim …')
!pip install jsbsim
PATH_TO_JSBSIM_FILES = None
Initialize FDM
[2]:
import jsbsim
# --- JSBSim Initialization ---
# These lines initialize the flight dynamics model.
# Create a flight dynamics model (FDM) instance.
fdm = jsbsim.FGFDMExec(PATH_TO_JSBSIM_FILES)
fdm.set_debug_level(0) # Suppress verbose JSBSim console output
if fdm is not None:
print("FDM created successfully")
fdm.set_debug_level(0) # Suppress verbose JSBSim console output
else:
print("Failed to create FDM")
JSBSim Flight Dynamics Model v1.3.0 Apr 9 2026 10:00:08
[JSBSim-ML v2.0]
JSBSim startup beginning ...
FDM created successfully
Tweak aircraft XML file: remove <input/> nodes from the officially released files
[3]:
import os
import xml.etree.ElementTree as ET
AIRCRAFT_NAME="737"
ac_xml_file_path = os.path.join(fdm.get_root_dir(), f'aircraft/{AIRCRAFT_NAME}/{AIRCRAFT_NAME}.xml')
print(f"Aircraft original XML file: {ac_xml_file_path}")
print("Parsing XML ...")
ac_xml_tree = ET.parse(ac_xml_file_path)
ac_xml_root = ac_xml_tree.getroot()
# Save a copy of the original XML file for backup
backup_xml_file_path = ac_xml_file_path.replace('.xml', '_backup.xml')
print(f"Saving backup XML file: {backup_xml_file_path}")
ac_xml_tree.write(backup_xml_file_path)
# Traverse the XML tree and remove <input ... /> occurrences with a 'port' attribute
for x in ac_xml_root.findall('input'):
has_port = x.get('port') is not None
if has_port:
print(f"\tRemoving <input ... /> with port: {x.get('port')}")
ac_xml_root.remove(x)
print(f"Saving modified XML: {ac_xml_file_path}")
ac_xml_tree.write(ac_xml_file_path)
#check that the input occurrences are removed
ac_xml_tree2 = ET.parse(ac_xml_file_path)
ac_xml_root2 = ac_xml_tree2.getroot()
inputs = ac_xml_root2.findall('input')
if not inputs:
print("All <input ... /> occurrences successfully removed.")
else:
print(f"Warning: Found {len(inputs)} <input/> occurrences remaining.")
Aircraft original XML file: /home/vscode/.local/lib/python3.11/site-packages/jsbsim/aircraft/737/737.xml
Parsing XML ...
Saving backup XML file: /home/vscode/.local/lib/python3.11/site-packages/jsbsim/aircraft/737/737_backup.xml
Saving modified XML: /home/vscode/.local/lib/python3.11/site-packages/jsbsim/aircraft/737/737.xml
All <input ... /> occurrences successfully removed.
[4]:
import math
import numpy as np
import matplotlib.pyplot as plt
# --- Configuration Section ---
# Global variables that must be modified to match your particular need
# The aircraft name
# Note - It should match the exact spelling of the model file
AIRCRAFT_NAME="737"
# --- JSBSim Initialization ---
# These lines initialize the flight dynamics model.
# Avoid flooding the console with log messages
jsbsim.FGJSBBase().debug_lvl = 0
# Create a flight dynamics model (FDM) instance.
fdm = jsbsim.FGFDMExec(PATH_TO_JSBSIM_FILES)
# Load the aircraft model
fdm.load_model(AIRCRAFT_NAME)
# Set engines running
fdm['propulsion/set-running'] = -1
def thrust_vector_range_test(altitude, speed, flight_path_angle, title):
# altitude: altitude above sea level (ft)
# speed: mach number of speed (<1)
# calibrated airspeed (kts) (>=1)
# flight_path_angle: flight path angle (deg)
# title: title for plot
# Thrust vectoring angles in pitch (deg) to test
tv_angles = np.linspace(0, 10, 100)
# Thrust and AoA trim results storage
thrusts = []
alphas = []
# Initialize the minimum thrust and thrust vectoring angles to a very large number
# to track/record the minimum thrust and the angle at which the minimum occurs.
min_thrust = 1000000 # thrust (lbf)
min_angle = 100 # Thrust Vector Angle in pitch (deg)
# Iterate each thrust vector angles in pitch.
for tv_angle in tv_angles:
# --- Simulation Initialization ---
# This line initializes the flight dynamics model.
# Initial conditions
fdm['ic/h-sl-ft'] = altitude # altitude above sea level (ft)
# Check the speed and set the value according to if the speed is mach or kts
if speed < 1.0:
fdm['ic/mach'] = speed # mach number of speed
else:
fdm['ic/vc-kts'] = speed # calibrated airspeed (kts)
fdm['ic/gamma-deg'] = flight_path_angle # flight path angle (deg)
# Initialize the aircraft with initial conditions
fdm.run_ic()
# --- Simulation running ---
# These lines run the simulation.
# Trim the aircraft.
try:
# Set thrust vector angle in pitch (deg) for both engines
fdm["propulsion/engine[0]/pitch-angle-rad"] = math.radians(tv_angle)
fdm["propulsion/engine[1]/pitch-angle-rad"] = math.radians(tv_angle)
# Trim the aircraft.
# 1 means straight flight by using all changeable control variables.
fdm['simulation/do_simple_trim'] = 1
# Record the simulation data.
# Append the angle of attack to the result storage.
alphas.append(fdm["aero/alpha-deg"])
# Append the thrust to the result storage.
thrust = fdm["propulsion/engine[0]/thrust-lbs"]*2 # because there are two engines
thrusts.append(thrust)
# Update the minimum thrust and thrust vectoring angles.
if thrust < min_thrust:
min_thrust = thrust
min_angle = tv_angle
except jsbsim.TrimFailureError:
print("Trim failed....")
pass # Ignore trim failure
# --- Plot Results ---
# This section plots the simulation results.
plt.rcParams["figure.figsize"] = (12, 8) # Set the figure size for matplotlib plots.
fig, ax1 = plt.subplots()
plt.title(title)
# Plot the thrust values against the thrust vector angles.
ax1.plot(tv_angles, thrusts, label='Thrust')
# Plot the minimum thrust as a red scatter point.
ax1.scatter(min_angle, min_thrust, color='red', label=f'Minimum Thrust at {min_angle:.2f} deg')
ax1.set_xlabel('Thrust Vector Angle (deg)')
ax1.set_ylabel('Thrust (lbf)')
# Create the second y-axis for AoA
ax2 = ax1.twinx()
ax2.set_ylabel('Alpha (deg)')
# Plot the alpha values against the thrust vector angles.
ax2.plot(tv_angles, alphas, color='green', label='Alpha')
ax1.legend(loc='upper center')
ax2.legend(loc='center right')
# Save the figure as an SVG file.
plt.savefig(f"{title}.svg", format="svg")
# Show the plot.
plt.show()
# Cruise conditions - 30,000ft Mach 0.8
thrust_vector_range_test(30000, 0.8, 0, 'Cruise - 30,000ft Mach 0.8')
# Climb conditions - 15,000ft 300KIAS flight path angle of 3 degrees
thrust_vector_range_test(15000, 300, 3, 'Climb - 15,000ft 300KIAS FPA 3 deg')