Skip to content

Aircraft dynamics

In this example, an IMU is simulated from aircraft dynamics.

The NASA Enginering and Safety Center NESC publishes "check-case" data to compare and validate aircraft dynamics simulators. Each check-case is a standardized scenario designed to test simulator performance. In this example we use check-case 13.4 (lateral offset maneuver of a subsonic aircraft) which is described in the NESC report, Reference [31]. Paraphrasing from the report:

This scenario [13.4] utilizes an F-16 model at 10,000 ft above First Flight airport in Kitty Hawk, NC on a heading of 45 degree true at 400 KTAS relative to the still atmosphere. The F16 autopilot was engaged. At t = +20.0 sec, a 2,000-ft lateral offset was commanded to the right, and the response of the vehicle to that command change was recorded.

Load and visualize the input

First we load and visualize the NESC data. We see, as described, a right "dogleg" turn on a trajectory heading north-east. The aircraft rolls and yaws to achieve the trajectory change. Minor deviations in pitch and altitude are also observed due to the coupled dynamics.

Altitude definitions

The NESC report defines altitude as Geometric altitude above reference ellipsoid, sphere, or plane but elsewhere refers to altitude relative to mean sea level (MSL) or geoid. The GlobalPose class requires altitude relative to the reference ellipsoid. For the purposes of this example the difference is negligible but users should take care to use consistent definitions.

# -----------------------------------------------------------------------------
# Copyright (c) 2023-2025, Inertial Simulation LLC.
# All rights reserved.
# Do not use or redistribute without permission.
# Email: info@inertialsim.com
# -----------------------------------------------------------------------------
%matplotlib widget

import numpy as np

from inertialsim import plot
from inertialsim.examples import nesc

path = "nesc_data"
flight_data = nesc.load(path, "13p4", "02")

lla = plot.MapOverlay("Trajectory", map_type="hybrid", zoom=12)
lla.line(flight_data.position[:, 0], flight_data.position[:, 1])
# Uncomment for real-time interactive visualization
# lla.show()
Bokeh Plot
att = plot.TimeSeries(title="Simulated Attitude", ylabel="Euler Angles (deg)")
att.line(flight_data.time, np.rad2deg(flight_data.attitude))
att.legend(["Roll", "Pitch", "Yaw"])

alt = plot.TimeSeries(title="Simulated Altitude", ylabel="Altitude (m)")
alt.line(flight_data.time, flight_data.position[:, 2], color="#2196F3")
alt.figure.set_layout_engine("tight")
Figure
Figure

Create the input data

Because the aircraft is flying at 10,000 feet (~3050 meters) altitude and moving over long distances, we need to use geodetic coordinates and accurate gravity models. A local Cartesian frame and/or the assumption that gravity is fixed and vertical will result in inaccuracies.

We create a GlobalPose object and populate it with the flight data. Note, we supply both the geodetic position (latitude, longitude, altitude) and velocity (local-level NED components). If velocity data is available, it is highly recommended to use it. When calculating accelerometer outputs, a single numerical difference is required from velocity inputs. Two numerical differences are required if only position is input.

from inertialsim.geodesy import GlobalPose
from inertialsim.geometry import Rotation

attitude = Rotation.from_euler(flight_data.attitude, "XYZ", flight_data.time)
pose = GlobalPose.from_geodetic(
    attitude=attitude,
    position=flight_data.position,
    velocity=flight_data.velocity,
    time=flight_data.time,
)

Simulate an aircraft grade IMU measuring the input

Finally we simulate a Northrop Grumman LN-200 IMU. It has a default data rate of 100Hz. The NESC flight data is sampled at 10Hz so the input is resampled in the simulate call. Linear interpolation on the underlying pose manifold is used.

from inertialsim.devices import northrop_grumman_ln200
from inertialsim.sensors.imu import IMU, IMUModel

# Create the IMU and simulate the result.  Using the default IMUModel() ensures
# that the output is resampled to match the LN-200 sample rate.
imu = IMU(IMUModel(), northrop_grumman_ln200, rng=1)
result = imu.simulate(global_pose=pose)

ar = plot.TimeSeries(title="Gyro Measurements", ylabel="Angular Rate (rad/s)")
ar.line(result.angular_rate.time, result.angular_rate.data)
ar.zoom_inset([0.6, 0.6, 0.35, 0.35], [23, 25], [-0.02, 0.05])
ar.legend(["X", "Y", "Z"], loc="lower left")

sf = plot.TimeSeries(
    title="Accelerometer Measurements", ylabel="Specific Force (m/s/s)"
)
sf.line(result.specific_force.time, result.specific_force.data)
sf.zoom_inset([0.6, 0.4, 0.35, 0.35], [38, 42], [-10.2, -9.8])
sf.legend(["X", "Y", "Z"])
Figure
Figure

Analyzing the result

Reference angular rate data is available in the NESC data set and it matches the simulated gyro data closely. The simulated accelerometer data shows the gravity vector primarily in the z-axis of the sensor frame as expected. The "dogleg" maneuver induces an approximately 0.5g acceleration. There is a small nearly-constant component of gravity measured in the x-axis of the sensor frame due to the non-zero pitch angle, which is correlated with the angle of attack. This example demonstrates InertialSim's ability to handle geodetic inputs that accurately account for Earth's shape, rotation, and gravity.