blob: 7c7c310d2a2b444b54c934160a34a1b4086b1ca1 [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
brians343bc112013-02-10 01:53:46 +00002"""
3Control loop pole placement library.
4
5This library will grow to support many different pole placement methods.
6Currently it only supports direct pole placement.
7"""
8
9__author__ = 'Austin Schuh (austin.linux@gmail.com)'
10
11import numpy
Parker Schuh1cbabbf2017-04-12 19:51:11 -070012import scipy.linalg
Austin Schuh085eab92020-11-26 13:54:51 -080013import scipy.signal
Austin Schuhc9177b52015-11-28 13:18:31 -080014import glog
brians343bc112013-02-10 01:53:46 +000015
Ravago Jones26f7ad02021-02-05 15:45:59 -080016
17class Error(Exception):
18 """Base class for all control loop exceptions."""
brians343bc112013-02-10 01:53:46 +000019
20
brians343bc112013-02-10 01:53:46 +000021# 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 Schuh085eab92020-11-26 13:54:51 -080024def dplace(A, B, poles):
Ravago Jones26f7ad02021-02-05 15:45:59 -080025 """Set the poles of (A - BF) to poles.
brians343bc112013-02-10 01:53:46 +000026
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
brians343bc112013-02-10 01:53:46 +000033 Returns:
34 numpy.matrix(m x n), K
35 """
Ravago Jones5127ccc2022-07-31 16:32:45 -070036 return scipy.signal.place_poles(A=A, B=B,
37 poles=numpy.array(poles)).gain_matrix
Ravago Jones26f7ad02021-02-05 15:45:59 -080038
Austin Schuhc8ca2442013-02-23 12:29:33 -080039
Austin Schuhc8ca2442013-02-23 12:29:33 -080040def c2d(A, B, dt):
Ravago Jones26f7ad02021-02-05 15:45:59 -080041 """Converts from continuous time state space representation to discrete time.
Parker Schuh1cbabbf2017-04-12 19:51:11 -070042 Returns (A, B). C and D are unchanged.
43 This code is copied from: scipy.signal.cont2discrete method zoh
44 """
Austin Schuhc8ca2442013-02-23 12:29:33 -080045
Ravago Jones26f7ad02021-02-05 15:45:59 -080046 a, b = numpy.array(A), numpy.array(B)
47 # Build an exponential matrix
48 em_upper = numpy.hstack((a, b))
Parker Schuh1cbabbf2017-04-12 19:51:11 -070049
Ravago Jones26f7ad02021-02-05 15:45:59 -080050 # Need to stack zeros under the a and b matrices
Ravago Jones5127ccc2022-07-31 16:32:45 -070051 em_lower = numpy.hstack((numpy.zeros(
52 (b.shape[1], a.shape[0])), numpy.zeros((b.shape[1], b.shape[1]))))
Parker Schuh1cbabbf2017-04-12 19:51:11 -070053
Ravago Jones26f7ad02021-02-05 15:45:59 -080054 em = numpy.vstack((em_upper, em_lower))
55 ms = scipy.linalg.expm(dt * em)
Parker Schuh1cbabbf2017-04-12 19:51:11 -070056
Ravago Jones26f7ad02021-02-05 15:45:59 -080057 # Dispose of the lower rows
58 ms = ms[:a.shape[0], :]
Parker Schuh1cbabbf2017-04-12 19:51:11 -070059
Ravago Jones26f7ad02021-02-05 15:45:59 -080060 ad = ms[:, 0:a.shape[1]]
61 bd = ms[:, a.shape[1]:]
Parker Schuh1cbabbf2017-04-12 19:51:11 -070062
Ravago Jones26f7ad02021-02-05 15:45:59 -080063 return numpy.matrix(ad), numpy.matrix(bd)
64
Austin Schuh7ec34fd2014-02-15 22:27:46 -080065
66def ctrb(A, B):
Ravago Jones26f7ad02021-02-05 15:45:59 -080067 """Returns the controllability matrix.
Austin Schuh7ec34fd2014-02-15 22:27:46 -080068
Austin Schuhc9177b52015-11-28 13:18:31 -080069 This matrix must have full rank for all the states to be controllable.
Austin Schuh7ec34fd2014-02-15 22:27:46 -080070 """
Ravago Jones26f7ad02021-02-05 15:45:59 -080071 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 Schuh7ec34fd2014-02-15 22:27:46 -080077
Ravago Jones26f7ad02021-02-05 15:45:59 -080078 return output
79
Austin Schuh7ec34fd2014-02-15 22:27:46 -080080
Austin Schuh434c8372018-01-21 16:30:06 -080081def dlqr(A, B, Q, R, optimal_cost_function=False):
Ravago Jones26f7ad02021-02-05 15:45:59 -080082 """Solves for the optimal lqr controller.
Austin Schuh7ec34fd2014-02-15 22:27:46 -080083
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
Austin Schuh7227a8b2024-09-21 16:22:42 -070088 # 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 # X.T * P * X -> optimal cost to infinity
Austin Schuh7ec34fd2014-02-15 22:27:46 -080090
Ravago Jones26f7ad02021-02-05 15:45:59 -080091 P = scipy.linalg.solve_discrete_are(a=A, b=B, q=Q, r=R)
Austin Schuh7227a8b2024-09-21 16:22:42 -070092 F = numpy.linalg.inv(R + B.T @ P @ B) @ B.T @ P @ A
Ravago Jones26f7ad02021-02-05 15:45:59 -080093 if optimal_cost_function:
94 return F, P
95 else:
96 return F
97
Austin Schuhe4a14f22015-03-01 00:12:29 -080098
99def kalman(A, B, C, Q, R):
Ravago Jones26f7ad02021-02-05 15:45:59 -0800100 """Solves for the steady state kalman gain and covariance matricies.
Austin Schuhe4a14f22015-03-01 00:12:29 -0800101
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 Jones26f7ad02021-02-05 15:45:59 -0800110 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 Schuhc9177b52015-11-28 13:18:31 -0800114
Ravago Jones26f7ad02021-02-05 15:45:59 -0800115 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 Schuhe4a14f22015-03-01 00:12:29 -0800119
Ravago Jones26f7ad02021-02-05 15:45:59 -0800120 # 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 Schuhe4a14f22015-03-01 00:12:29 -0800125
Ravago Jones26f7ad02021-02-05 15:45:59 -0800126 return K, P
Austin Schuh86093ad2016-02-06 14:29:34 -0800127
Austin Schuh3ad5ed82017-02-25 21:36:19 -0800128
129def kalmd(A_continuous, B_continuous, Q_continuous, R_continuous, dt):
Ravago Jones26f7ad02021-02-05 15:45:59 -0800130 """Converts a continuous time kalman filter to discrete time.
Austin Schuh3ad5ed82017-02-25 21:36:19 -0800131
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 Jones26f7ad02021-02-05 15:45:59 -0800145 # 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 Jones5127ccc2022-07-31 16:32:45 -0700167 phi22 = phi[number_of_states:2 * number_of_states,
168 number_of_states:2 * number_of_states]
Ravago Jones26f7ad02021-02-05 15:45:59 -0800169 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 Schuh3ad5ed82017-02-25 21:36:19 -0800173
174
Austin Schuh86093ad2016-02-06 14:29:34 -0800175def TwoStateFeedForwards(B, Q):
Ravago Jones26f7ad02021-02-05 15:45:59 -0800176 """Computes the feed forwards constant for a 2 state controller.
Austin Schuh86093ad2016-02-06 14:29:34 -0800177
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 Jones26f7ad02021-02-05 15:45:59 -0800190 # 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 Schuh86093ad2016-02-06 14:29:34 -0800194
Ravago Jones26f7ad02021-02-05 15:45:59 -0800195 return numpy.linalg.inv(B.T * Q * B) * B.T * Q.T