blob: 55c4267c3c1eec1441cbfc417944a0995950538f [file] [log] [blame]
brians343bc112013-02-10 01:53:46 +00001#!/usr/bin/python
2
3"""
4Control loop pole placement library.
5
6This library will grow to support many different pole placement methods.
7Currently it only supports direct pole placement.
8"""
9
10__author__ = 'Austin Schuh (austin.linux@gmail.com)'
11
12import numpy
13import slycot
Parker Schuh1cbabbf2017-04-12 19:51:11 -070014import scipy.linalg
Austin Schuhc9177b52015-11-28 13:18:31 -080015import glog
brians343bc112013-02-10 01:53:46 +000016
17class Error (Exception):
18 """Base class for all control loop exceptions."""
19
20
21class PolePlacementError(Error):
22 """Exception raised when pole placement fails."""
23
24
25# TODO(aschuh): dplace should take a control system object.
26# There should also exist a function to manipulate laplace expressions, and
27# something to plot bode plots and all that.
28def dplace(A, B, poles, alpha=1e-6):
29 """Set the poles of (A - BF) to poles.
30
31 Args:
32 A: numpy.matrix(n x n), The A matrix.
33 B: numpy.matrix(n x m), The B matrix.
34 poles: array(imaginary numbers), The poles to use. Complex conjugates poles
35 must be in pairs.
36
37 Raises:
38 ValueError: Arguments were the wrong shape or there were too many poles.
39 PolePlacementError: Pole placement failed.
40
41 Returns:
42 numpy.matrix(m x n), K
43 """
44 # See http://www.icm.tu-bs.de/NICONET/doc/SB01BD.html for a description of the
45 # fortran code that this is cleaning up the interface to.
46 n = A.shape[0]
47 if A.shape[1] != n:
48 raise ValueError("A must be square")
49 if B.shape[0] != n:
50 raise ValueError("B must have the same number of states as A.")
51 m = B.shape[1]
52
53 num_poles = len(poles)
54 if num_poles > n:
55 raise ValueError("Trying to place more poles than states.")
56
57 out = slycot.sb01bd(n=n,
58 m=m,
59 np=num_poles,
60 alpha=alpha,
61 A=A,
62 B=B,
63 w=numpy.array(poles),
64 dico='D')
65
66 A_z = numpy.matrix(out[0])
67 num_too_small_eigenvalues = out[2]
68 num_assigned_eigenvalues = out[3]
69 num_uncontrollable_eigenvalues = out[4]
70 K = numpy.matrix(-out[5])
71 Z = numpy.matrix(out[6])
72
73 if num_too_small_eigenvalues != 0:
74 raise PolePlacementError("Number of eigenvalues that are too small "
75 "and are therefore unmodified is %d." %
76 num_too_small_eigenvalues)
77 if num_assigned_eigenvalues != num_poles:
78 raise PolePlacementError("Did not place all the eigenvalues that were "
79 "requested. Only placed %d eigenvalues." %
80 num_assigned_eigenvalues)
81 if num_uncontrollable_eigenvalues != 0:
82 raise PolePlacementError("Found %d uncontrollable eigenvlaues." %
83 num_uncontrollable_eigenvalues)
84
85 return K
Austin Schuhc8ca2442013-02-23 12:29:33 -080086
Austin Schuhc8ca2442013-02-23 12:29:33 -080087def c2d(A, B, dt):
88 """Converts from continuous time state space representation to discrete time.
Parker Schuh1cbabbf2017-04-12 19:51:11 -070089 Returns (A, B). C and D are unchanged.
90 This code is copied from: scipy.signal.cont2discrete method zoh
91 """
Austin Schuhc8ca2442013-02-23 12:29:33 -080092
Parker Schuh1cbabbf2017-04-12 19:51:11 -070093 a, b = numpy.array(A), numpy.array(B)
94 # Build an exponential matrix
95 em_upper = numpy.hstack((a, b))
96
97 # Need to stack zeros under the a and b matrices
98 em_lower = numpy.hstack((numpy.zeros((b.shape[1], a.shape[0])),
99 numpy.zeros((b.shape[1], b.shape[1]))))
100
101 em = numpy.vstack((em_upper, em_lower))
102 ms = scipy.linalg.expm(dt * em)
103
104 # Dispose of the lower rows
105 ms = ms[:a.shape[0], :]
106
107 ad = ms[:, 0:a.shape[1]]
108 bd = ms[:, a.shape[1]:]
109
110 return numpy.matrix(ad), numpy.matrix(bd)
Austin Schuh7ec34fd2014-02-15 22:27:46 -0800111
112def ctrb(A, B):
Brian Silvermane18cf502015-11-28 23:04:43 +0000113 """Returns the controllability matrix.
Austin Schuh7ec34fd2014-02-15 22:27:46 -0800114
Austin Schuhc9177b52015-11-28 13:18:31 -0800115 This matrix must have full rank for all the states to be controllable.
Austin Schuh7ec34fd2014-02-15 22:27:46 -0800116 """
117 n = A.shape[0]
118 output = B
119 intermediate = B
120 for i in xrange(0, n):
121 intermediate = A * intermediate
122 output = numpy.concatenate((output, intermediate), axis=1)
123
124 return output
125
126def dlqr(A, B, Q, R):
127 """Solves for the optimal lqr controller.
128
129 x(n+1) = A * x(n) + B * u(n)
130 J = sum(0, inf, x.T * Q * x + u.T * R * u)
131 """
132
133 # P = (A.T * P * A) - (A.T * P * B * numpy.linalg.inv(R + B.T * P *B) * (A.T * P.T * B).T + Q
134
Austin Schuh1a387962015-01-31 16:36:20 -0800135 P, rcond, w, S, T = slycot.sb02od(
136 n=A.shape[0], m=B.shape[1], A=A, B=B, Q=Q, R=R, dico='D')
Austin Schuh7ec34fd2014-02-15 22:27:46 -0800137
138 F = numpy.linalg.inv(R + B.T * P *B) * B.T * P * A
139 return F
Austin Schuhe4a14f22015-03-01 00:12:29 -0800140
141def kalman(A, B, C, Q, R):
142 """Solves for the steady state kalman gain and covariance matricies.
143
144 Args:
145 A, B, C: SS matricies.
146 Q: The model uncertantity
147 R: The measurement uncertainty
148
149 Returns:
150 KalmanGain, Covariance.
151 """
Austin Schuh572ff402015-11-08 12:17:50 -0800152 I = numpy.matrix(numpy.eye(Q.shape[0]))
153 Z = numpy.matrix(numpy.zeros(Q.shape[0]))
Austin Schuhc9177b52015-11-28 13:18:31 -0800154 n = A.shape[0]
155 m = C.shape[0]
156
157 controllability_rank = numpy.linalg.matrix_rank(ctrb(A.T, C.T))
Brian Silvermane18cf502015-11-28 23:04:43 +0000158 if controllability_rank != n:
Austin Schuhc9177b52015-11-28 13:18:31 -0800159 glog.warning('Observability of %d != %d, unobservable state',
Brian Silvermane18cf502015-11-28 23:04:43 +0000160 controllability_rank, n)
Austin Schuhe4a14f22015-03-01 00:12:29 -0800161
Austin Schuh572ff402015-11-08 12:17:50 -0800162 # Compute the steady state covariance matrix.
Austin Schuhc9177b52015-11-28 13:18:31 -0800163 P_prior, rcond, w, S, T = slycot.sb02od(n=n, m=m, A=A.T, B=C.T, Q=Q, R=R, dico='D')
Austin Schuh572ff402015-11-08 12:17:50 -0800164 S = C * P_prior * C.T + R
165 K = numpy.linalg.lstsq(S.T, (P_prior * C.T).T)[0].T
166 P = (I - K * C) * P_prior
Austin Schuhe4a14f22015-03-01 00:12:29 -0800167
168 return K, P
Austin Schuh86093ad2016-02-06 14:29:34 -0800169
Austin Schuh3ad5ed82017-02-25 21:36:19 -0800170
171def kalmd(A_continuous, B_continuous, Q_continuous, R_continuous, dt):
172 """Converts a continuous time kalman filter to discrete time.
173
174 Args:
175 A_continuous: The A continuous matrix
176 B_continuous: the B continuous matrix
177 Q_continuous: The continuous cost matrix
178 R_continuous: The R continuous matrix
179 dt: Timestep
180
181 The math for this is from:
182 https://www.mathworks.com/help/control/ref/kalmd.html
183
184 Returns:
185 The discrete matrices of A, B, Q, and R.
186 """
187 # TODO(austin): Verify that the dimensions make sense.
188 number_of_states = A_continuous.shape[0]
189 number_of_inputs = B_continuous.shape[1]
190 M = numpy.zeros((len(A_continuous) + number_of_inputs,
191 len(A_continuous) + number_of_inputs))
192 M[0:number_of_states, 0:number_of_states] = A_continuous
193 M[0:number_of_states, number_of_states:] = B_continuous
194 M_exp = scipy.linalg.expm(M * dt)
195 A_discrete = M_exp[0:number_of_states, 0:number_of_states]
196 B_discrete = numpy.matrix(M_exp[0:number_of_states, number_of_states:])
197 Q_continuous = (Q_continuous + Q_continuous.T) / 2.0
198 R_continuous = (R_continuous + R_continuous.T) / 2.0
199 M = numpy.concatenate((-A_continuous, Q_continuous), axis=1)
200 M = numpy.concatenate(
201 (M, numpy.concatenate((numpy.matrix(
202 numpy.zeros((number_of_states, number_of_states))),
203 numpy.transpose(A_continuous)), axis = 1)), axis = 0)
204 phi = numpy.matrix(scipy.linalg.expm(M*dt))
205 phi12 = phi[0:number_of_states, number_of_states:(2*number_of_states)]
206 phi22 = phi[number_of_states:2*number_of_states, number_of_states:2*number_of_states]
207 Q_discrete = phi22.T * phi12
208 Q_discrete = (Q_discrete + Q_discrete.T) / 2.0
209 R_discrete = R_continuous / dt
210 return (A_discrete, B_discrete, Q_discrete, R_discrete)
211
212
Austin Schuh86093ad2016-02-06 14:29:34 -0800213def TwoStateFeedForwards(B, Q):
214 """Computes the feed forwards constant for a 2 state controller.
215
216 This will take the form U = Kff * (R(n + 1) - A * R(n)), where Kff is the
217 feed-forwards constant. It is important that Kff is *only* computed off
218 the goal and not the feed back terms.
219
220 Args:
221 B: numpy.Matrix[num_states, num_inputs] The B matrix.
222 Q: numpy.Matrix[num_states, num_states] The Q (cost) matrix.
223
224 Returns:
225 numpy.Matrix[num_inputs, num_states]
226 """
227
228 # We want to find the optimal U such that we minimize the tracking cost.
229 # This means that we want to minimize
230 # (B * U - (R(n+1) - A R(n)))^T * Q * (B * U - (R(n+1) - A R(n)))
231 # TODO(austin): This doesn't take into account the cost of U
232
233 return numpy.linalg.inv(B.T * Q * B) * B.T * Q.T