Flight dynamics, interactively!

It’s possible to match two interesting things: flight dynamics and interactive programming with Python. The magic tool we’re going to use is called Jupyter Notebook. Basically, it’s Python programming done with our favourite web browser.

As we seen in the previous post about flight dynamics, the coupling of rigid body motions and aerodynamics leads to a peculiar aircraft behaviour. Using a Jupyter notebook, we’re going to integrate the equations of motion as function of time. We’ll produce some plots (like the one below!) showing the short and long term response of the airplane.

The rigid rotation is shown in this plot. The short term oscillations disappear after few seconds since the short period mode is strongly damped. However, the long term response is dominated by the phugoid mode which has low frequency and little damping.

I’ve included the Jupyter notebook I used to produce this plot here below. The implementation is quite technical but I tried to include some comments to make it easier to read. However, if you are interested, please feel free to contact me!

Fligth dynamics, interactively!

Let’s import some Python modules. We’re going to use some modules from standard library (time, math) but also NumPy and SciPy

import time
import math

# Numpy and Scipy
import scipy as sc
import numpy as np

import scipy.integrate as si

import scipy.linalg as sl
import numpy.linalg as nl

from numpy import pi as pi
from scipy.optimize import fsolve as fsolve

# Matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
import matplotlib.pylab as pylab

pylab.rcParams['figure.figsize'] = (8, 5)
pylab.rcParams['figure.dpi'] = 96
pylab.rcParams['savefig.dpi'] = 96

Parameters

The test case we’re going to implement is taken from literature, specifically from the book “Flight Dynamics Principles” – Cook, M.V., 2nd edition at page 106.

# Units: Imperial

#-------------------------------- General parameters
#- Parameters at sea level: air density and thrust
TSL = 1000
rho0 = 1.10

#- Initial conditions: speed, angle of attack,
#  Euler angle and air density at given altitude
V0 = 305
alpha0 = 0.
theta0 = 0.
zE0  = 0.
rho = 0.00238

#- Mass and inertia properties
mass = 746
Iy = 65000
gravity = 32.2

#-------------------------------- Stability derivatives
# These derivatives represent how the aerodynamic forces
# change due to the flight dynamics unknowns. For example,
# Zw represents the change in the vertical force due to the
# vertical velocity w

#- Derivatives - Z direction
Zu = -159.64
Zu_dot = 0.

Zw = -328.24
Zw_dot = 0

Zq = 0
Zq_dot = 0

Zdelta = -16502.0

#- Derivatives - X direction
Xu = -26.26
Xu_dot = 0.

Xw = 79.82
Xw_dot = 0

Xq = 0
Xq_dot = 0

Xdelta = 0

#- Derivatives - Rotation
Mu = 0
Mu_dot = 0

Mw = -1014.0
Mw_dot = -36.4

Mq = -18135.0
Mq_dot = 0

Mdelta = -303575.0

#-------------------------------- Simulation options
# We are going to simulate the first 60 seconds. The solution to
# the differential equations is calculated each 0.1 seconds

simTime = [0, 60]
simStep = 0.1

# Horizontal and vertical velocity at equilibrium condition
Ue = V0*np.cos(alpha0)
We = V0*np.sin(alpha0)

# Initial conditions. We're going to simulate an initial disturbance
# in the angular velocity q
state_vector0 = [0, 0, 0.1, 0]
elevator0 = 0
throttle0 = 0

Equations of motion

These differential equations express the evolution of the system with respect to time. This is the set of equations that we’ll integrate to get the flight dynamics response of the system.

$$\begin{align}
\left[ \begin{array}{cccc}
m-X_{\dot u} & -X_{\dot w} & -X_{\dot q} & 0\\
-Z_{\dot u} & m-Z_{\dot w} & -Z_{\dot q} & 0 \\
-M_{\dot u} & -M_{\dot w} & I_y-M_{\dot q} & 0\\
0 & 0 & 0 & 1
\end{array} \right]
\left[ \begin{array}{c}
{\dot u} \\
{\dot w} \\
{\dot q} \\
{\dot \theta} \\
\end{array} \right] = &
\left[ \begin{array}{cccc}
X_{u} & X_{w} & X_{q} & 0 \\
Z_{u} & Z_{w} & Z_{q} & 0 \\
M_{u} & M_{w} &M_{q} & 0 \\
0 & 0 & 0 & 0
\end{array} \right]
\left[ \begin{array}{c}
{ u } \\
{ w } \\
{ q } \\
{\theta} \\
\end{array} \right] + \\ &
\left[ \begin{array}{cccc}
0 & 0 & – m W_e & – m g \cos \theta_0 \\
0 & 0 & m U_e & – m g \sin \theta_0 \\
0 & 0 & 0 & 0 \\
0 & 0 & 1 & 0
\end{array} \right]
\left[ \begin{array}{c}
{ u} \\
{ w} \\
{ q} \\
{\theta} \\
\end{array} \right] + \\ &
\left[ \begin{array}{c}
X_\delta \\
Z_\delta \\
M_\delta \\
0
\end{array} \right]
\delta +
\left[ \begin{array}{c}
T_{SL} \frac{\rho}{\rho_0} \\
0 \\
0 \\
0
\end{array} \right] \eta
\end{align}
$$

Matrices and eigenvalues

def getMatrices(Ue, We):
    # This function return the Jacobian matrix of the system
    # and the other vectors related to control surfaces and thrust
    # that we need for the integration

    # ---------
    I1 = np.array([[mass-Xu_dot, -Xw_dot, -Xq_dot, 0.],
                   [-Zu_dot, mass-Zw_dot, -Zq_dot, 0.],
                   [-Mu_dot, -Mw_dot, Iy-Mq_dot, 0.],
                   [      0,       0,        0, 1]])

    In = nl.inv(I1)


    # ---------
    An = np.array([[Xu, Xw, Xq, 0],
                   [Zu, Zw, Zq, 0],
                   [Mu, Mw, Mq, 0],
                   [0,0,0,0]])

    Kn = np.array([[0, 0, - mass * We, - mass * gravity * np.cos(theta0)],
                   [0, 0, + mass * Ue, - mass * gravity * np.sin(theta0)],
                   [0, 0, 0, 0],
                   [0, 0, 1, 0]])

    A = An + Kn

    # ---------
    D = np.array([Xdelta, Zdelta, Mdelta, 0])
    T = np.array([TSL * rho / rho0, 0, 0, 0])

    return np.dot(In, A), np.dot(In, D), np.dot(In, T)

Using the function we have written, we can calculate the eigenvalues of the system. We’re expecting two eigenvalues corresponding to short period and phugoid!

Jac, Jac_D, Jac_T = getMatrices(V0, 0.)

print "Jacobian matrix = "
print Jac
print

print "Eigenvalue analysis: "
evals = nl.eig(Jac)[0]
for ev in evals:
    print "\t", ev
print
Jacobian matrix =
[[ -3.52010724e-02   1.06997319e-01   0.00000000e+00  -3.22000000e+01]
 [ -2.13994638e-01  -4.40000000e-01   3.05000000e+02   0.00000000e+00]
 [  1.19836997e-04  -1.53536000e-02  -4.49800000e-01   0.00000000e+00]
 [  0.00000000e+00   0.00000000e+00   1.00000000e+00   0.00000000e+00]]

Eigenvalue analysis:
	(-0.44586984133+2.16437192886j)
	(-0.44586984133-2.16437192886j)
	(-0.0166306948626+0.147431081456j)
	(-0.0166306948626-0.147431081456j)

Eigenvalues are complex conjugate since they represent harmonic motions. The imaginary part of the eigenvalue is related to oscillation frequency. The eigenvalue with higher frequency is the short period whereas the low-frequency mode is the phugoid.

Literature comparison

The solution to the eigenvalue problem can be derived analitically. The final result is copied here from literature as a comparison with the evaluation we made. In oarticular, transfer function is given as
$$
TF(s) = \frac{-4.664 (s+0.135) (s+0.267)}{(s^2+0.033 s + 0.022) (s^2 + 0.893 s + 4.884)}
$$

with stability modes given by the solution of the denominator
$$
s_1 = -0.0165 \pm 0.1474 \mathrm{i}
$$

and

$$
s_2 = -0.4465 \pm 2.164 \mathrm{i}
$$

As you can see, the numeric evaluation gives very similar results.

Integration

At this point, it is time to integrate the equations and have some nice plots of the flight dynamics response.

def integrate(y0, tSpan, dt, update=None, callback=None, maxInnerSteps=10000):
    '''
    Integrate a set of first order differential equation. This function acts
    similarly to MATLAB's ode45 function.

    t, y, timing = integrate(y0, tSpan, dt, update, callback, maxInnerSteps)

    INPUT
      y0 = initial conditions (y at tBeg)
      tSpan = [tBeg, tEnd]
      dt = step size

      update = fn(t, y)
      callback = fn(t, y) called at every succeful integration

      maxInnerSteps = max number of inner iterations (default: 10000)

    OUTPUT
      t = list of timesteps
      y = solutions at timestep (one line per timestep)
      timing = elapsed time for the integration of the timestep
    '''

    tBegin = min(tSpan)
    tEnd = max(tSpan)

    history_t = []
    history_y = []

    integr = sc.integrate.ode(update)
    integr.set_integrator('dopri5', nsteps=maxInnerSteps)
    integr.set_initial_value(y0, tBegin)

    t_elapsed = []
    tic = time.time()

    history_t.append(tBegin)
    history_y.append(np.array(y0))

    while integr.t < tEnd:
        values = integr.integrate(integr.t + dt)

        if not integr.successful():
            raise Exception("Integration not successful")

        history_t.append(integr.t)
        history_y.append(values)

        if callback is not None:
            try:
                callback(integr.t, values)
            except:
                print "Error in the callback function"

        toc = time.time() - tic
        t_elapsed.append(toc)

    return np.array(history_t), np.array(history_y), np.array(t_elapsed)

def eqsOfMotion(time, state_vector, elevator, throttle, Ue, We):
    """
    This function return the first order derivative of the state vector at each timestep.
    The derivative is calculated using the Jacobian matrix evaluated earlier.
    """

    if np.any(np.isnan(state_vector)):
        raise ValueError("NaN!")

    Jac, Jac_D, Jac_T = getMatrices(Ue, We)

    state_vector_dot = np.dot(Jac, state_vector) \
        + Jac_D * elevator \
        + Jac_T * throttle

    return state_vector_dot

It’s good to output the initial conditions and the initial external disturbance

print "Initial state vector:"
print state_vector0
print

print "Elevator = ", elevator0 * (180./np.pi)
print "Throttle = ", throttle0

fn_integrate = lambda t, x: eqsOfMotion(t, x, elevator0, throttle0, Ue, We)
t_time, t_values, t_elapsed = integrate(np.array(state_vector0), simTime, simStep, fn_integrate, None)

print "Integration performed!"

Results

In this section the integration is performed and four plots are produced as result of the integration. Short period and phugoid are clearly visible in the response of the system.

names = ["u", "w", "q", "theta"]
titles = ["Horizontal speed", "Vertical speed", "Angular speed", "Rotation"]
units = ["[ft/s]", "[ft/s]", "[deg/s]", "[deg]"]
convertion = [1, 1, (180./np.pi), (180./np.pi)]

plt.figure()

for index in range(4):

    plt.figure(index +1)
    plt.plot(t_time, t_values[:,index]*convertion[index])
    plt.ylabel("{} {}".format(names[index], units[index]))
    plt.xlabel("Time [s]")
    plt.title(titles[index])
    plt.grid('on')

plt.show()

Calculating flight path

The flight path can be calculated as post-processing step.

# The derivatives of the horizontal and vertical position in the inertial frame are calculated
xE_dot = +(Ue + t_values[:,0]) * np.cos(t_values[:,3]) + (We + t_values[:,1]) * np.sin(t_values[:,3])
zE_dot = +(Ue + t_values[:,0]) * np.sin(t_values[:,3]) - (We + t_values[:,1]) * np.cos(t_values[:,3])

# When these derivatives are needed at time steps not sampled, we have to use interpolation.
# This is needed because the integration routine can choose time steps for which no evaluation of
# xE_dot or zE_dot is available
def int_path(t, x):
    xE_dot_temp = np.interp(t, t_time, xE_dot)
    zE_dot_temp = np.interp(t, t_time, zE_dot)
    return np.array([xE_dot_temp, zE_dot_temp])

path_time, path_values, _ = integrate(np.array([0,0]), simTime, simStep, int_path, None)

plt.figure()
plt.plot(path_values[:,0], path_values[:,1])
plt.xlabel('Horizontal translation [ft]')
plt.ylabel('Vertical translation [ft]')
plt.title('Flight path')
plt.grid()
plt.show()

Leave a Reply

avatar
  Subscribe  
Notify of

Pin It on Pinterest