brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | """ |
| 4 | Control loop pole placement library. |
| 5 | |
| 6 | This library will grow to support many different pole placement methods. |
| 7 | Currently it only supports direct pole placement. |
| 8 | """ |
| 9 | |
| 10 | __author__ = 'Austin Schuh (austin.linux@gmail.com)' |
| 11 | |
| 12 | import numpy |
| 13 | import slycot |
Austin Schuh | c976f49 | 2015-02-22 21:28:18 -0800 | [diff] [blame] | 14 | import scipy.signal.cont2discrete |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 15 | import glog |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 16 | |
| 17 | class Error (Exception): |
| 18 | """Base class for all control loop exceptions.""" |
| 19 | |
| 20 | |
| 21 | class 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. |
| 28 | def 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 Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 86 | |
| 87 | |
| 88 | def c2d(A, B, dt): |
| 89 | """Converts from continuous time state space representation to discrete time. |
Austin Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 90 | Returns (A, B). C and D are unchanged.""" |
Austin Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 91 | |
Austin Schuh | c976f49 | 2015-02-22 21:28:18 -0800 | [diff] [blame] | 92 | ans_a, ans_b, _, _, _ = scipy.signal.cont2discrete((A, B, None, None), dt) |
| 93 | return numpy.matrix(ans_a), numpy.matrix(ans_b) |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 94 | |
| 95 | def ctrb(A, B): |
Brian Silverman | e18cf50 | 2015-11-28 23:04:43 +0000 | [diff] [blame] | 96 | """Returns the controllability matrix. |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 97 | |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 98 | This matrix must have full rank for all the states to be controllable. |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 99 | """ |
| 100 | n = A.shape[0] |
| 101 | output = B |
| 102 | intermediate = B |
| 103 | for i in xrange(0, n): |
| 104 | intermediate = A * intermediate |
| 105 | output = numpy.concatenate((output, intermediate), axis=1) |
| 106 | |
| 107 | return output |
| 108 | |
| 109 | def dlqr(A, B, Q, R): |
| 110 | """Solves for the optimal lqr controller. |
| 111 | |
| 112 | x(n+1) = A * x(n) + B * u(n) |
| 113 | J = sum(0, inf, x.T * Q * x + u.T * R * u) |
| 114 | """ |
| 115 | |
| 116 | # P = (A.T * P * A) - (A.T * P * B * numpy.linalg.inv(R + B.T * P *B) * (A.T * P.T * B).T + Q |
| 117 | |
Austin Schuh | 1a38796 | 2015-01-31 16:36:20 -0800 | [diff] [blame] | 118 | P, rcond, w, S, T = slycot.sb02od( |
| 119 | n=A.shape[0], m=B.shape[1], A=A, B=B, Q=Q, R=R, dico='D') |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 120 | |
| 121 | F = numpy.linalg.inv(R + B.T * P *B) * B.T * P * A |
| 122 | return F |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 123 | |
| 124 | def kalman(A, B, C, Q, R): |
| 125 | """Solves for the steady state kalman gain and covariance matricies. |
| 126 | |
| 127 | Args: |
| 128 | A, B, C: SS matricies. |
| 129 | Q: The model uncertantity |
| 130 | R: The measurement uncertainty |
| 131 | |
| 132 | Returns: |
| 133 | KalmanGain, Covariance. |
| 134 | """ |
Austin Schuh | 572ff40 | 2015-11-08 12:17:50 -0800 | [diff] [blame] | 135 | I = numpy.matrix(numpy.eye(Q.shape[0])) |
| 136 | Z = numpy.matrix(numpy.zeros(Q.shape[0])) |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 137 | n = A.shape[0] |
| 138 | m = C.shape[0] |
| 139 | |
| 140 | controllability_rank = numpy.linalg.matrix_rank(ctrb(A.T, C.T)) |
Brian Silverman | e18cf50 | 2015-11-28 23:04:43 +0000 | [diff] [blame] | 141 | if controllability_rank != n: |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 142 | glog.warning('Observability of %d != %d, unobservable state', |
Brian Silverman | e18cf50 | 2015-11-28 23:04:43 +0000 | [diff] [blame] | 143 | controllability_rank, n) |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 144 | |
Austin Schuh | 572ff40 | 2015-11-08 12:17:50 -0800 | [diff] [blame] | 145 | # Compute the steady state covariance matrix. |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 146 | 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 Schuh | 572ff40 | 2015-11-08 12:17:50 -0800 | [diff] [blame] | 147 | S = C * P_prior * C.T + R |
| 148 | K = numpy.linalg.lstsq(S.T, (P_prior * C.T).T)[0].T |
| 149 | P = (I - K * C) * P_prior |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 150 | |
| 151 | return K, P |
Austin Schuh | 86093ad | 2016-02-06 14:29:34 -0800 | [diff] [blame] | 152 | |
| 153 | def TwoStateFeedForwards(B, Q): |
| 154 | """Computes the feed forwards constant for a 2 state controller. |
| 155 | |
| 156 | This will take the form U = Kff * (R(n + 1) - A * R(n)), where Kff is the |
| 157 | feed-forwards constant. It is important that Kff is *only* computed off |
| 158 | the goal and not the feed back terms. |
| 159 | |
| 160 | Args: |
| 161 | B: numpy.Matrix[num_states, num_inputs] The B matrix. |
| 162 | Q: numpy.Matrix[num_states, num_states] The Q (cost) matrix. |
| 163 | |
| 164 | Returns: |
| 165 | numpy.Matrix[num_inputs, num_states] |
| 166 | """ |
| 167 | |
| 168 | # We want to find the optimal U such that we minimize the tracking cost. |
| 169 | # This means that we want to minimize |
| 170 | # (B * U - (R(n+1) - A R(n)))^T * Q * (B * U - (R(n+1) - A R(n))) |
| 171 | # TODO(austin): This doesn't take into account the cost of U |
| 172 | |
| 173 | return numpy.linalg.inv(B.T * Q * B) * B.T * Q.T |