LQR control design for vertical takeoff and landing aircraft

This script demonstrates the use of the python-control package for analysis and design of a controller for a vectored thrust aircraft model that is used as a running example through the text Feedback Systems by Astrom and Murray. This example makes use of MATLAB compatible commands.

Code

  1# pvtol_lqr.m - LQR design for vectored thrust aircraft
  2# RMM, 14 Jan 03
  3#
  4# This file works through an LQR based design problem, using the
  5# planar vertical takeoff and landing (PVTOL) aircraft example from
  6# Astrom and Murray, Chapter 5.  It is intended to demonstrate the
  7# basic functionality of the python-control package.
  8#
  9
 10import os
 11import numpy as np
 12import matplotlib.pyplot as plt  # MATLAB-like plotting functions
 13import control as ct
 14
 15#
 16# System dynamics
 17#
 18# These are the dynamics for the PVTOL system, written in state space
 19# form.
 20#
 21
 22# System parameters
 23m = 4       # mass of aircraft
 24J = 0.0475  # inertia around pitch axis
 25r = 0.25    # distance to center of force
 26g = 9.8     # gravitational constant
 27c = 0.05    # damping factor (estimated)
 28
 29# State space dynamics
 30xe = [0, 0, 0, 0, 0, 0]  # equilibrium point of interest
 31ue = [0, m * g]  # (note these are lists, not matrices)
 32
 33# TODO: The following objects need converting from np.matrix to np.array
 34# This will involve re-working the subsequent equations as the shapes
 35# See below.
 36
 37A = np.array(
 38    [[0, 0, 0, 1, 0, 0],
 39     [0, 0, 0, 0, 1, 0],
 40     [0, 0, 0, 0, 0, 1],
 41     [0, 0, (-ue[0]*np.sin(xe[2]) - ue[1]*np.cos(xe[2]))/m, -c/m, 0, 0],
 42     [0, 0, (ue[0]*np.cos(xe[2]) - ue[1]*np.sin(xe[2]))/m, 0, -c/m, 0],
 43     [0, 0, 0, 0, 0, 0]]
 44)
 45
 46# Input matrix
 47B = np.array(
 48    [[0, 0], [0, 0], [0, 0],
 49     [np.cos(xe[2])/m, -np.sin(xe[2])/m],
 50     [np.sin(xe[2])/m, np.cos(xe[2])/m],
 51     [r/J, 0]]
 52)
 53
 54# Output matrix 
 55C = np.array([[1, 0, 0, 0, 0, 0], [0, 1, 0, 0, 0, 0]])
 56D = np.array([[0, 0], [0, 0]])
 57
 58#
 59# Construct inputs and outputs corresponding to steps in xy position
 60#
 61# The vectors xd and yd correspond to the states that are the desired
 62# equilibrium states for the system.  The matrices Cx and Cy are the 
 63# corresponding outputs.
 64#
 65# The way these vectors are used is to compute the closed loop system
 66# dynamics as
 67#
 68#   xdot = Ax + B u  =>  xdot = (A-BK)x + K xd
 69#      u = -K(x - xd)       y = Cx
 70#
 71# The closed loop dynamics can be simulated using the "step" command, 
 72# with K*xd as the input vector (assumes that the "input" is unit size,
 73# so that xd corresponds to the desired steady state.
 74#
 75
 76xd = np.array([[1], [0], [0], [0], [0], [0]])
 77yd = np.array([[0], [1], [0], [0], [0], [0]])
 78
 79#
 80# Extract the relevant dynamics for use with SISO library
 81#
 82# The current python-control library only supports SISO transfer
 83# functions, so we have to modify some parts of the original MATLAB
 84# code to extract out SISO systems.  To do this, we define the 'lat' and
 85# 'alt' index vectors to consist of the states that are are relevant
 86# to the lateral (x) and vertical (y) dynamics.
 87#
 88
 89# Indices for the parts of the state that we want
 90lat = (0, 2, 3, 5)
 91alt = (1, 4)
 92
 93# Decoupled dynamics
 94Ax = A[np.ix_(lat, lat)]
 95Bx = B[np.ix_(lat, [0])]
 96Cx = C[np.ix_([0], lat)]
 97Dx = D[np.ix_([0], [0])]
 98
 99Ay = A[np.ix_(alt, alt)]
100By = B[np.ix_(alt, [1])]
101Cy = C[np.ix_([1], alt)]
102Dy = D[np.ix_([1], [1])]
103
104# Label the plot
105plt.clf()
106plt.suptitle("LQR controllers for vectored thrust aircraft (pvtol-lqr)")
107
108#
109# LQR design
110#
111
112# Start with a diagonal weighting
113Qx1 = np.diag([1, 1, 1, 1, 1, 1])
114Qu1a = np.diag([1, 1])
115K1a, X, E = ct.lqr(A, B, Qx1, Qu1a)
116
117# Close the loop: xdot = Ax - B K (x-xd)
118#
119# Note: python-control requires we do this 1 input at a time
120# H1a = ss(A-B*K1a, B*K1a*concatenate((xd, yd), axis=1), C, D);
121# (T, Y) = step_response(H1a, T=np.linspace(0,10,100));
122#
123
124# Step response for the first input
125H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)],
126             Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
127Tx, Yx = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
128
129# Step response for the second input
130H1ay = ct.ss(Ay - By @ K1a[np.ix_([1], alt)],
131             By @ K1a[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
132Ty, Yy = ct.step_response(H1ay, T=np.linspace(0, 10, 100))
133
134plt.subplot(221)
135plt.title("Identity weights")
136# plt.plot(T, Y[:,1, 1], '-', T, Y[:,2, 2], '--')
137plt.plot(Tx.T, Yx.T, '-', Ty.T, Yy.T, '--')
138plt.plot([0, 10], [1, 1], 'k-')
139
140plt.axis([0, 10, -0.1, 1.4])
141plt.ylabel('position')
142plt.legend(('x', 'y'), loc='lower right')
143
144# Look at different input weightings
145Qu1a = np.diag([1, 1])
146K1a, X, E = ct.lqr(A, B, Qx1, Qu1a)
147H1ax = ct.ss(Ax - Bx @ K1a[np.ix_([0], lat)],
148             Bx @ K1a[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
149
150Qu1b = (40 ** 2)*np.diag([1, 1])
151K1b, X, E = ct.lqr(A, B, Qx1, Qu1b)
152H1bx = ct.ss(Ax - Bx @ K1b[np.ix_([0], lat)],
153             Bx @ K1b[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
154
155Qu1c = (200 ** 2)*np.diag([1, 1])
156K1c, X, E = ct.lqr(A, B, Qx1, Qu1c)
157H1cx = ct.ss(Ax - Bx @ K1c[np.ix_([0], lat)],
158             Bx @ K1c[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
159
160T1, Y1 = ct.step_response(H1ax, T=np.linspace(0, 10, 100))
161T2, Y2 = ct.step_response(H1bx, T=np.linspace(0, 10, 100))
162T3, Y3 = ct.step_response(H1cx, T=np.linspace(0, 10, 100))
163
164plt.subplot(222)
165plt.title("Effect of input weights")
166plt.plot(T1.T, Y1.T, 'b-')
167plt.plot(T2.T, Y2.T, 'b-')
168plt.plot(T3.T, Y3.T, 'b-')
169plt.plot([0, 10], [1, 1], 'k-')
170
171plt.axis([0, 10, -0.1, 1.4])
172
173# arcarrow([1.3, 0.8], [5, 0.45], -6)
174plt.text(5.3, 0.4, r'$\rho$')
175
176# Output weighting - change Qx to use outputs
177Qx2 = C.T @ C
178Qu2 = 0.1 * np.diag([1, 1])
179K2, X, E = ct.lqr(A, B, Qx2, Qu2)
180
181H2x = ct.ss(Ax - Bx @ K2[np.ix_([0], lat)],
182            Bx @ K2[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
183H2y = ct.ss(Ay - By @ K2[np.ix_([1], alt)],
184            By @ K2[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
185
186plt.subplot(223)
187plt.title("Output weighting")
188T2x, Y2x = ct.step_response(H2x, T=np.linspace(0, 10, 100))
189T2y, Y2y = ct.step_response(H2y, T=np.linspace(0, 10, 100))
190plt.plot(T2x.T, Y2x.T, T2y.T, Y2y.T)
191plt.ylabel('position')
192plt.xlabel('time')
193plt.ylabel('position')
194plt.legend(('x', 'y'), loc='lower right')
195
196#
197# Physically motivated weighting
198#
199# Shoot for 1 cm error in x, 10 cm error in y.  Try to keep the angle
200# less than 5 degrees in making the adjustments.  Penalize side forces
201# due to loss in efficiency.
202#
203
204Qx3 = np.diag([100, 10, 2*np.pi/5, 0, 0, 0])
205Qu3 = 0.1*np.diag([1, 10])
206K3, X, E = ct.lqr(A, B, Qx3, Qu3)
207
208H3x = ct.ss(Ax - Bx @ K3[np.ix_([0], lat)],
209            Bx @ K3[np.ix_([0], lat)] @ xd[lat, :], Cx, Dx)
210H3y = ct.ss(Ay - By @ K3[np.ix_([1], alt)],
211            By @ K3[np.ix_([1], alt)] @ yd[alt, :], Cy, Dy)
212plt.subplot(224)
213# step_response(H3x, H3y, 10)
214T3x, Y3x = ct.step_response(H3x, T=np.linspace(0, 10, 100))
215T3y, Y3y = ct.step_response(H3y, T=np.linspace(0, 10, 100))
216plt.plot(T3x.T, Y3x.T, T3y.T, Y3y.T)
217plt.title("Physically motivated weights")
218plt.xlabel('time')
219plt.legend(('x', 'y'), loc='lower right')
220plt.tight_layout()
221
222if 'PYCONTROL_TEST_EXAMPLES' not in os.environ:
223    plt.show()

Notes

1. The environment variable PYCONTROL_TEST_EXAMPLES is used for testing to turn off plotting of the outputs.