Austin Schuh | 085eab9 | 2020-11-26 13:54:51 -0800 | [diff] [blame] | 1 | #!/usr/bin/python3 |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 2 | """ |
| 3 | Control loop pole placement library. |
| 4 | |
| 5 | This library will grow to support many different pole placement methods. |
| 6 | Currently it only supports direct pole placement. |
| 7 | """ |
| 8 | |
| 9 | __author__ = 'Austin Schuh (austin.linux@gmail.com)' |
| 10 | |
| 11 | import numpy |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 12 | import scipy.linalg |
Austin Schuh | 085eab9 | 2020-11-26 13:54:51 -0800 | [diff] [blame] | 13 | import scipy.signal |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 14 | import glog |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 15 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 16 | |
| 17 | class Error(Exception): |
| 18 | """Base class for all control loop exceptions.""" |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 19 | |
| 20 | |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 21 | # TODO(aschuh): dplace should take a control system object. |
| 22 | # There should also exist a function to manipulate laplace expressions, and |
| 23 | # something to plot bode plots and all that. |
Austin Schuh | 085eab9 | 2020-11-26 13:54:51 -0800 | [diff] [blame] | 24 | def dplace(A, B, poles): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 25 | """Set the poles of (A - BF) to poles. |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 26 | |
| 27 | Args: |
| 28 | A: numpy.matrix(n x n), The A matrix. |
| 29 | B: numpy.matrix(n x m), The B matrix. |
| 30 | poles: array(imaginary numbers), The poles to use. Complex conjugates poles |
| 31 | must be in pairs. |
| 32 | |
brians | 343bc11 | 2013-02-10 01:53:46 +0000 | [diff] [blame] | 33 | Returns: |
| 34 | numpy.matrix(m x n), K |
| 35 | """ |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame^] | 36 | return scipy.signal.place_poles(A=A, B=B, |
| 37 | poles=numpy.array(poles)).gain_matrix |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 38 | |
Austin Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 39 | |
Austin Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 40 | def c2d(A, B, dt): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 41 | """Converts from continuous time state space representation to discrete time. |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 42 | Returns (A, B). C and D are unchanged. |
| 43 | This code is copied from: scipy.signal.cont2discrete method zoh |
| 44 | """ |
Austin Schuh | c8ca244 | 2013-02-23 12:29:33 -0800 | [diff] [blame] | 45 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 46 | a, b = numpy.array(A), numpy.array(B) |
| 47 | # Build an exponential matrix |
| 48 | em_upper = numpy.hstack((a, b)) |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 49 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 50 | # Need to stack zeros under the a and b matrices |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame^] | 51 | em_lower = numpy.hstack((numpy.zeros( |
| 52 | (b.shape[1], a.shape[0])), numpy.zeros((b.shape[1], b.shape[1])))) |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 53 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 54 | em = numpy.vstack((em_upper, em_lower)) |
| 55 | ms = scipy.linalg.expm(dt * em) |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 56 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 57 | # Dispose of the lower rows |
| 58 | ms = ms[:a.shape[0], :] |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 59 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 60 | ad = ms[:, 0:a.shape[1]] |
| 61 | bd = ms[:, a.shape[1]:] |
Parker Schuh | 1cbabbf | 2017-04-12 19:51:11 -0700 | [diff] [blame] | 62 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 63 | return numpy.matrix(ad), numpy.matrix(bd) |
| 64 | |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 65 | |
| 66 | def ctrb(A, B): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 67 | """Returns the controllability matrix. |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 68 | |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 69 | 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] | 70 | """ |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 71 | n = A.shape[0] |
| 72 | output = B |
| 73 | intermediate = B |
| 74 | for i in range(0, n): |
| 75 | intermediate = A * intermediate |
| 76 | output = numpy.concatenate((output, intermediate), axis=1) |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 77 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 78 | return output |
| 79 | |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 80 | |
Austin Schuh | 434c837 | 2018-01-21 16:30:06 -0800 | [diff] [blame] | 81 | def dlqr(A, B, Q, R, optimal_cost_function=False): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 82 | """Solves for the optimal lqr controller. |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 83 | |
| 84 | x(n+1) = A * x(n) + B * u(n) |
| 85 | J = sum(0, inf, x.T * Q * x + u.T * R * u) |
| 86 | """ |
| 87 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 88 | # P = (A.T * P * A) - (A.T * P * B * numpy.linalg.inv(R + B.T * P *B) * (A.T * P.T * B).T + Q |
| 89 | # 0.5 * X.T * P * X -> optimal cost to infinity |
Austin Schuh | 7ec34fd | 2014-02-15 22:27:46 -0800 | [diff] [blame] | 90 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 91 | P = scipy.linalg.solve_discrete_are(a=A, b=B, q=Q, r=R) |
| 92 | F = numpy.linalg.inv(R + B.T * P * B) * B.T * P * A |
| 93 | if optimal_cost_function: |
| 94 | return F, P |
| 95 | else: |
| 96 | return F |
| 97 | |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 98 | |
| 99 | def kalman(A, B, C, Q, R): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 100 | """Solves for the steady state kalman gain and covariance matricies. |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 101 | |
| 102 | Args: |
| 103 | A, B, C: SS matricies. |
| 104 | Q: The model uncertantity |
| 105 | R: The measurement uncertainty |
| 106 | |
| 107 | Returns: |
| 108 | KalmanGain, Covariance. |
| 109 | """ |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 110 | I = numpy.matrix(numpy.eye(Q.shape[0])) |
| 111 | Z = numpy.matrix(numpy.zeros(Q.shape[0])) |
| 112 | n = A.shape[0] |
| 113 | m = C.shape[0] |
Austin Schuh | c9177b5 | 2015-11-28 13:18:31 -0800 | [diff] [blame] | 114 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 115 | controllability_rank = numpy.linalg.matrix_rank(ctrb(A.T, C.T)) |
| 116 | if controllability_rank != n: |
| 117 | glog.warning('Observability of %d != %d, unobservable state', |
| 118 | controllability_rank, n) |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 119 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 120 | # Compute the steady state covariance matrix. |
| 121 | P_prior = scipy.linalg.solve_discrete_are(a=A.T, b=C.T, q=Q, r=R) |
| 122 | S = C * P_prior * C.T + R |
| 123 | K = numpy.linalg.lstsq(S.T, (P_prior * C.T).T, rcond=None)[0].T |
| 124 | P = (I - K * C) * P_prior |
Austin Schuh | e4a14f2 | 2015-03-01 00:12:29 -0800 | [diff] [blame] | 125 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 126 | return K, P |
Austin Schuh | 86093ad | 2016-02-06 14:29:34 -0800 | [diff] [blame] | 127 | |
Austin Schuh | 3ad5ed8 | 2017-02-25 21:36:19 -0800 | [diff] [blame] | 128 | |
| 129 | def kalmd(A_continuous, B_continuous, Q_continuous, R_continuous, dt): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 130 | """Converts a continuous time kalman filter to discrete time. |
Austin Schuh | 3ad5ed8 | 2017-02-25 21:36:19 -0800 | [diff] [blame] | 131 | |
| 132 | Args: |
| 133 | A_continuous: The A continuous matrix |
| 134 | B_continuous: the B continuous matrix |
| 135 | Q_continuous: The continuous cost matrix |
| 136 | R_continuous: The R continuous matrix |
| 137 | dt: Timestep |
| 138 | |
| 139 | The math for this is from: |
| 140 | https://www.mathworks.com/help/control/ref/kalmd.html |
| 141 | |
| 142 | Returns: |
| 143 | The discrete matrices of A, B, Q, and R. |
| 144 | """ |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 145 | # TODO(austin): Verify that the dimensions make sense. |
| 146 | number_of_states = A_continuous.shape[0] |
| 147 | number_of_inputs = B_continuous.shape[1] |
| 148 | M = numpy.zeros((len(A_continuous) + number_of_inputs, |
| 149 | len(A_continuous) + number_of_inputs)) |
| 150 | M[0:number_of_states, 0:number_of_states] = A_continuous |
| 151 | M[0:number_of_states, number_of_states:] = B_continuous |
| 152 | M_exp = scipy.linalg.expm(M * dt) |
| 153 | A_discrete = M_exp[0:number_of_states, 0:number_of_states] |
| 154 | B_discrete = numpy.matrix(M_exp[0:number_of_states, number_of_states:]) |
| 155 | Q_continuous = (Q_continuous + Q_continuous.T) / 2.0 |
| 156 | R_continuous = (R_continuous + R_continuous.T) / 2.0 |
| 157 | M = numpy.concatenate((-A_continuous, Q_continuous), axis=1) |
| 158 | M = numpy.concatenate( |
| 159 | (M, |
| 160 | numpy.concatenate( |
| 161 | (numpy.matrix(numpy.zeros((number_of_states, number_of_states))), |
| 162 | numpy.transpose(A_continuous)), |
| 163 | axis=1)), |
| 164 | axis=0) |
| 165 | phi = numpy.matrix(scipy.linalg.expm(M * dt)) |
| 166 | phi12 = phi[0:number_of_states, number_of_states:(2 * number_of_states)] |
Ravago Jones | 5127ccc | 2022-07-31 16:32:45 -0700 | [diff] [blame^] | 167 | phi22 = phi[number_of_states:2 * number_of_states, |
| 168 | number_of_states:2 * number_of_states] |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 169 | Q_discrete = phi22.T * phi12 |
| 170 | Q_discrete = (Q_discrete + Q_discrete.T) / 2.0 |
| 171 | R_discrete = R_continuous / dt |
| 172 | return (A_discrete, B_discrete, Q_discrete, R_discrete) |
Austin Schuh | 3ad5ed8 | 2017-02-25 21:36:19 -0800 | [diff] [blame] | 173 | |
| 174 | |
Austin Schuh | 86093ad | 2016-02-06 14:29:34 -0800 | [diff] [blame] | 175 | def TwoStateFeedForwards(B, Q): |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 176 | """Computes the feed forwards constant for a 2 state controller. |
Austin Schuh | 86093ad | 2016-02-06 14:29:34 -0800 | [diff] [blame] | 177 | |
| 178 | This will take the form U = Kff * (R(n + 1) - A * R(n)), where Kff is the |
| 179 | feed-forwards constant. It is important that Kff is *only* computed off |
| 180 | the goal and not the feed back terms. |
| 181 | |
| 182 | Args: |
| 183 | B: numpy.Matrix[num_states, num_inputs] The B matrix. |
| 184 | Q: numpy.Matrix[num_states, num_states] The Q (cost) matrix. |
| 185 | |
| 186 | Returns: |
| 187 | numpy.Matrix[num_inputs, num_states] |
| 188 | """ |
| 189 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 190 | # We want to find the optimal U such that we minimize the tracking cost. |
| 191 | # This means that we want to minimize |
| 192 | # (B * U - (R(n+1) - A R(n)))^T * Q * (B * U - (R(n+1) - A R(n))) |
| 193 | # TODO(austin): This doesn't take into account the cost of U |
Austin Schuh | 86093ad | 2016-02-06 14:29:34 -0800 | [diff] [blame] | 194 | |
Ravago Jones | 26f7ad0 | 2021-02-05 15:45:59 -0800 | [diff] [blame] | 195 | return numpy.linalg.inv(B.T * Q * B) * B.T * Q.T |