F-16 Fighting Falcon — Nonlinear Longitudinal Dynamics¶
The General Dynamics F-16 Fighting Falcon is an American multirole lightweight 4th-generation fighter. This module provides a nonlinear longitudinal flight-channel model implemented in pure Python/NumPy. The aerodynamic coefficients are interpolated with cubic splines from wind-tunnel tables, providing high-fidelity dynamics across a wide range of angles of attack and control deflections. A Gymnasium environment is included for training control agents.
-
Quick start
Run the environment or the model in just a few lines of code.
-
Model API
Python class documentation for the nonlinear F-16 longitudinal dynamics.
-
Gymnasium environment
Ready-to-use environment for training control agents.
-
Theory
Nonlinear state equations and aerodynamic table structure.
Performance specs (reference)¶
| Parameter | Value |
|---|---|
| Variant | F-16A Block 10 |
| Wingspan, m | 9.45 |
| Aircraft length (with pitot boom), m | 15.03 |
| Aircraft height, m | 5.09 |
| Wing area, m² | 27.87 |
| Sweep angle, ° | 40.0 |
| Normal takeoff weight, kg | 11467 |
Control object structure¶
Unlike the linear F-16 model, which uses constant-coefficient matrices \(A\) and \(B\), the nonlinear model computes aerodynamic forces and moments from tabular data at every time step. The equations of motion are integrated numerically (Euler or RK4).
The state vector and control input:
- \(\alpha\): angle of attack, rad
- \(\omega_z\): pitch angular velocity, rad/s
- \(\delta_{\text{stab}}\): stabilator deflection, rad
- \(\dot{\delta}_{\text{stab}}\): stabilator deflection rate, rad/s
- \(\delta_{\text{stab,act}}\): stabilator command (control input), rad
| Parameter | Symbol | Value |
|---|---|---|
| Aircraft mass | \(m\) | 9295.44 kg |
| Wing area | \(S\) | 27.87 m² |
| Mean aerodynamic chord | \(b_A\) | 3.45 m |
| Pitch inertia | \(J_z\) | 75673.6 kg m² |
| Stabilator time constant | \(T_{\text{stab}}\) | 0.03 s |
| Stabilator damping ratio | \(\xi_{\text{stab}}\) | 0.707 |
| Altitude | \(H\) | 3000 m |
| Airspeed | \(V\) | 150 m/s |
| Gravity | \(g\) | 9.80665 m/s² |
Units
Inside the model, all angles and angular rates are in radians. The Gymnasium environment accepts actions in degrees (range \(\pm 25°\)) for compatibility with existing agents and converts to radians internally.
Mathematical model¶
The model solves the following system of nonlinear ODEs at each time step:
where the aerodynamic force and moment are:
The dynamic pressure \(q\) is computed from the ISA atmosphere model:
Aerodynamic tables¶
The coefficients \(C_y\) and \(m_z\) are not constant — they are multi-dimensional functions interpolated from wind-tunnel data stored as .npz files. The tables are interpolated using cubic splines (csaps).
Actuator model¶
The stabilator is modelled as a second-order system with position and rate limiting:
Actuator limits
The model uses the following default control limits:
- Maximum stabilator deflection: \(\pm 25°\)
- Maximum stabilator rate: \(\pm 60°/\text{s}\)
Integration methods¶
Two numerical integrators are available:
- Euler (default) — first-order forward Euler.
- RK4 — fourth-order Runge-Kutta, provides higher accuracy at the same time step.
Data source¶
- Stevens & Lewis, "Aircraft Control and Simulation".
- Wind-tunnel aerodynamic tables for F-16A, stored as NumPy
.npzarchives.
Quick start¶
import gymnasium as gym
import numpy as np
from tensoraerospace.envs import NonlinearLongitudinalF16
from tensoraerospace.utils import generate_time_period
from tensoraerospace.signals.standard import sinusoid
dt = 0.01
tp = generate_time_period(tn=20, dt=dt)
number_time_steps = len(tp)
reference_signal = sinusoid(
degree=3, tp=tp, frequency=0.1, output_rad=True
).reshape(1, -1)
env = gym.make(
'NonlinearLongitudinalF16-v0',
number_time_steps=number_time_steps,
initial_state=np.array([0.0, 0.0]),
reference_signal=reference_signal,
dt=dt,
integrator="euler",
)
state, info = env.reset()
for _ in range(number_time_steps - 1):
action = np.array([0.0]) # degrees
state, reward, terminated, truncated, info = env.step(action)
if terminated or truncated:
break
import numpy as np
from tensoraerospace.aerospacemodel.f16.nonlinear.longitudinal import LongitudinalF16
dt = 0.01
number_time_steps = 200
# State: [alpha, wz, stab, dstab] (rad)
x0 = np.array([0.0, 0.0, 0.0, 0.0])
model = LongitudinalF16(
x0=x0,
selected_state_output=["alpha", "wz"],
dt=dt,
integrator="rk4",
)
for t in range(number_time_steps - 1):
u = np.array([np.radians(-2.0)]) # stabilator command (rad)
state_next = model.run_step(u)
Python API¶
LongitudinalF16(x0, selected_state_output=None, t0=0, dt=0.01, integrator='euler')
¶
Bases: ModelBase
F-16 in isolated longitudinal channel.
Action: stab_act (elevator command, rad). State: alpha, wz, stab, dstab (rad / rad·s⁻¹).
current_state
property
¶
Most recent state as a flat 1-D ndarray (alpha, wz, stab, dstab).
NonlinearLongitudinalF16(initial_state, reference_signal, number_time_steps, tracking_states=None, state_space=None, control_space=None, output_space=None, reward_func=None, use_reward=True, dt=0.01, integrator='euler', control_bias=0.0, feedforward_fn=None, airspeed=200.0, render_mode=None, chart_states=('alpha', 'wz', 'stab'), trail_length=None, initial_pitch=0.0, damage_profile=None, damage_observable=False, damage_event_callback=None)
¶
Bases: Env
Gymnasium env over the pure-numpy nonlinear F-16 longitudinal model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
initial_state
|
ndarray
|
Initial state vector. Either the full 4-element model
state |
required |
reference_signal
|
ndarray
|
Reference trajectory of shape |
required |
number_time_steps
|
int
|
Episode length. |
required |
tracking_states
|
list[str] | None
|
Names of tracked states. Defaults to |
None
|
state_space
|
list[str] | None
|
Subset of model states to expose as the observation.
Observations are returned in radians. Defaults to |
None
|
control_space
|
list[str] | None
|
Names of control channels. Defaults to |
None
|
output_space
|
list[str] | None
|
Compatibility alias of |
None
|
reward_func
|
Callable[[ndarray, ndarray, int], ndarray | float] | None
|
Custom reward callable |
None
|
use_reward
|
bool
|
If False, reward is fixed at 1.0 each step. |
True
|
dt
|
float
|
Discretisation step (s). Defaults to 0.01. |
0.01
|
integrator
|
Literal['euler', 'rk4']
|
|
'euler'
|
control_bias
|
float
|
Constant offset (degrees) added to every action before clipping and conversion to radians. Use this to operate the agent in "delta around trim" space when the trim control is non-zero (e.g., on the nonlinear F-16, trim elevator is ~-4.45°). Default 0. |
0.0
|
feedforward_fn
|
Callable[[int, ndarray], float] | None
|
Optional callable |
None
|
Action units: by convention IHDP and most existing tensoraerospace agents
were tuned with the linear F-16 env, whose elevator action is interpreted
in degrees with magnitude limit 25 and rate limit 60. This env keeps
the same convention so those agent settings transfer: action is in
degrees, range [-25, 25], and is converted to radians internally
before being handed to the underlying numpy model.
default_reward(state, ref_signal, ts)
staticmethod
¶
Tracking-error reward with a small angular-rate penalty.
