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')
../_images/notebooks_08_thrust_vectoring_analysis_6_0.png
../_images/notebooks_08_thrust_vectoring_analysis_6_1.png