blob: 9e9adf77be14c57e84c853178b0c4438d9e3e41f [file] [log] [blame]
Austin Schuh82162452022-02-07 22:01:45 -08001#!/usr/bin/python3
2
3from frc971.control_loops.python import control_loop
4from frc971.control_loops.python import controls
5import numpy
Austin Schuh2e28d872022-02-19 18:25:57 -08006import osqp
Austin Schuh82162452022-02-07 22:01:45 -08007import math
Austin Schuhc3e04282022-02-12 20:00:53 -08008import scipy.optimize
Austin Schuh82162452022-02-07 22:01:45 -08009import sys
10import math
11from y2022.control_loops.python import catapult_lib
12from matplotlib import pylab
13
14import gflags
15import glog
16
17FLAGS = gflags.FLAGS
18
Austin Schuhc3e04282022-02-12 20:00:53 -080019gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
Austin Schuh82162452022-02-07 22:01:45 -080020
21ball_mass = 0.25
22ball_diameter = 9.5 * 0.0254
23lever = 17.5 * 0.0254
24
25G = (14.0 / 72.0) * (12.0 / 33.0)
26
27
28def AddResistance(motor, resistance):
29 motor.resistance += resistance
30 return motor
31
Austin Schuhc3e04282022-02-12 20:00:53 -080032
Austin Schuh82162452022-02-07 22:01:45 -080033J_ball = 1.5 * ball_mass * lever * lever
34# Assuming carbon fiber, calculate the mass of the bar.
35M_bar = (1750 * lever * 0.0254 * 0.0254 * (1.0 - (1 - 0.07)**2.0))
36# And the moment of inertia.
37J_bar = 1.0 / 3.0 * M_bar * lever**2.0
38
39# Do the same for a theoretical cup. Assume a 40 thou thick carbon cup.
40M_cup = (1750 * 0.0254 * 0.04 * 2 * math.pi * (ball_diameter / 2.)**2.0)
41J_cup = M_cup * lever**2.0 + M_cup * (ball_diameter / 2.)**2.0
42
Austin Schuh82162452022-02-07 22:01:45 -080043
Austin Schuhb5af5912022-03-27 13:31:20 -070044J = (0.0 * J_ball + J_bar + J_cup * 0.0)
milind-u37dc40b2022-03-08 19:51:27 -080045JEmpty = (J_bar + J_cup * 0.0)
Austin Schuh82162452022-02-07 22:01:45 -080046
Austin Schuhac61c882022-02-23 17:22:22 -080047kCatapultWithBall = catapult_lib.CatapultParams(
Austin Schuh2e28d872022-02-19 18:25:57 -080048 name='Catapult',
Austin Schuhdfeb83c2022-03-12 13:17:49 -080049 motor=AddResistance(control_loop.NMotor(control_loop.Falcon(), 2), 0.01),
Austin Schuh82162452022-02-07 22:01:45 -080050 G=G,
51 J=J,
Austin Schuhac61c882022-02-23 17:22:22 -080052 radius=lever,
53 q_pos=2.8,
54 q_vel=20.0,
milind-u37dc40b2022-03-08 19:51:27 -080055 kalman_q_pos=0.01,
Austin Schuhac61c882022-02-23 17:22:22 -080056 kalman_q_vel=1.0,
57 kalman_q_voltage=1.5,
milind-u37dc40b2022-03-08 19:51:27 -080058 kalman_r_position=0.001)
Austin Schuhac61c882022-02-23 17:22:22 -080059
60kCatapultEmpty = catapult_lib.CatapultParams(
61 name='Catapult',
milind-u37dc40b2022-03-08 19:51:27 -080062 motor=AddResistance(control_loop.NMotor(control_loop.Falcon(), 2), 0.02),
Austin Schuhac61c882022-02-23 17:22:22 -080063 G=G,
64 J=JEmpty,
65 radius=lever,
66 q_pos=2.8,
67 q_vel=20.0,
68 kalman_q_pos=0.12,
69 kalman_q_vel=1.0,
70 kalman_q_voltage=1.5,
71 kalman_r_position=0.05)
Austin Schuhc3e04282022-02-12 20:00:53 -080072
Austin Schuhc3e04282022-02-12 20:00:53 -080073# Ideas for adjusting the cost function:
74#
75# Penalize battery current?
76# Penalize accel/rotor current?
77# Penalize velocity error off destination?
78# Penalize max u
79#
80# Ramp up U cost over time?
81# Once moving, open up saturation bounds
82#
83# We really want our cost function to be robust so that we can tolerate the
84# battery not delivering like we want at the end.
85
Austin Schuh2e28d872022-02-19 18:25:57 -080086def quadratic_cost(catapult, X_initial, X_final, horizon):
87 Q_final = numpy.matrix([[10000.0, 0.0], [0.0, 10000.0]])
Austin Schuhc3e04282022-02-12 20:00:53 -080088
Austin Schuh2e28d872022-02-19 18:25:57 -080089 As = numpy.vstack([catapult.A**(n + 1) for n in range(0, horizon)])
90 Af = catapult.A**horizon
91
92 Bs = numpy.matrix(numpy.zeros((2 * horizon, horizon)))
93 for n in range(0, horizon):
94 for m in range(0, n + 1):
95 Bs[n * 2:(n * 2) + 2, m] = catapult.A**(n - m) * catapult.B
96
97 Bf = Bs[horizon * 2 - 2:, :]
98
99 P_final = 2.0 * Bf.transpose() * Q_final * Bf
100 q_final = (2.0 * (Af * X_initial - X_final).transpose() * Q_final *
101 Bf).transpose()
102
103 constant_final = (Af * X_initial - X_final).transpose() * Q_final * (
104 Af * X_initial - X_final)
105
106 m = numpy.matrix([[catapult.A[1, 1] ** (n + 1)] for n in range(horizon)])
107 M = Bs[1:horizon * 2:2, :]
108
109 W = numpy.matrix(numpy.identity(horizon) - numpy.eye(horizon, horizon, -1)) / catapult.dt
110 w = -numpy.matrix(numpy.eye(horizon, 1, 0)) / catapult.dt
111
112
113 Pi = numpy.diag([
114 (0.01 ** 2.0) + (0.02 * max(0.0, 20 - (horizon - n)) / 20.0) ** 2.0 for n in range(horizon)
115 ])
116
117 P_accel = 2.0 * M.transpose() * W.transpose() * Pi * W * M
118 q_accel = 2.0 * (((W * m + w) * X_initial[1, 0]).transpose() * Pi * W * M).transpose()
119 constant_accel = ((W * m + w) * X_initial[1, 0]).transpose() * Pi * (
120 (W * m + w) * X_initial[1, 0])
121
122 return ((P_accel + P_final), (q_accel + q_final), (constant_accel + constant_final))
123
124
125def new_cost(catapult, X_initial, X_final, u):
126 u_matrix = numpy.matrix(u).transpose()
127 Q_final = numpy.matrix([[10000.0, 0.0], [0.0, 10000.0]])
128
129 As = numpy.vstack([catapult.A**(n + 1) for n in range(0, len(u))])
130 Af = catapult.A**len(u)
131
132 Bs = numpy.matrix(numpy.zeros((2 * len(u), len(u))))
133 for n in range(0, len(u)):
134 for m in range(0, n + 1):
135 Bs[n * 2:(n * 2) + 2, m] = catapult.A**(n - m) * catapult.B
136
137 Bf = Bs[len(u) * 2 - 2:, :]
138
139 P_final = 2.0 * Bf.transpose() * Q_final * Bf
140 q_final = (2.0 * (Af * X_initial - X_final).transpose() * Q_final *
141 Bf).transpose()
142
143 constant_final = (Af * X_initial - X_final).transpose() * Q_final * (
144 Af * X_initial - X_final)
145
146 m = numpy.matrix([[catapult.A[1, 1] ** (n + 1)] for n in range(len(u))])
147 M = Bs[1:len(u) * 2:2, :]
148
149 W = numpy.matrix(numpy.identity(len(u)) - numpy.eye(len(u), len(u), -1)) / catapult.dt
150 w = -numpy.matrix(numpy.eye(len(u), 1, 0)) * X_initial[1, 0] / catapult.dt
151
152 accel = W * (M * u_matrix + m * X_initial[1, 0]) + w
153
154 Pi = numpy.diag([
155 (0.01 ** 2.0) + (0.02 * max(0.0, 20 - (len(u) - n)) / 20.0) ** 2.0 for n in range(len(u))
156 ])
157
158 P_accel = 2.0 * M.transpose() * W.transpose() * Pi * W * M
159 q_accel = 2.0 * ((W * m * X_initial[1, 0] + w).transpose() * Pi * W * M).transpose()
160 constant_accel = (W * m * X_initial[1, 0] +
161 w).transpose() * Pi * (W * m * X_initial[1, 0] + w)
162
163
164def mpc_cost(catapult, X_initial, X_final, u_matrix):
165
Austin Schuhc3e04282022-02-12 20:00:53 -0800166 X = X_initial.copy()
167 cost = 0.0
168 last_u = u_matrix[0]
169 max_u = 0.0
170 for count, u in enumerate(u_matrix):
171 v_prior = X[1, 0]
172 X = catapult.A * X + catapult.B * numpy.matrix([[u]])
173 v = X[1, 0]
174
175 # Smoothness cost on voltage change and voltage.
176 #cost += (u - last_u) ** 2.0
177 #cost += (u - 6.0) ** 2.0
178
179 measured_a = (v - v_prior) / catapult.dt
180 expected_a = 0.0
181
182 # Our good cost!
183 cost_scalar = 0.02 * max(0.0, (20 - (len(u_matrix) - count)) / 20.)
184 cost += ((measured_a - expected_a) * cost_scalar)**2.0
185 cost += (measured_a * 0.010)**2.0
186
187 # Quadratic cost. This delays as long as possible, but approximates a
188 # LQR until saturation.
189 #cost += (u - 0.0) ** 2.0
190 #cost += (0.1 * (X_final[0, 0] - X[0, 0])) ** 2.0
191 #cost += (0.5 * (X_final[1, 0] - X[1, 0])) ** 2.0
192
193 max_u = max(u, max_u)
194 last_u = u
195
196 # Penalize max power usage. This is hard to solve.
197 #cost += max_u * 10
198
199 terminal_cost = (X - X_final).transpose() * numpy.matrix(
200 [[10000.0, 0.0], [0.0, 10000.0]]) * (X - X_final)
201 cost += terminal_cost[0, 0]
202
203 return cost
204
205
Austin Schuh2e28d872022-02-19 18:25:57 -0800206def SolveCatapult(catapult, X_initial, X_final, u):
Austin Schuhc3e04282022-02-12 20:00:53 -0800207 """ Solves for the optimal action given a seed, state, and target """
208 def vbat_constraint(z, i):
209 return 12.0 - z[i]
210
211 def forward(z, i):
212 return z[i]
213
Austin Schuh2e28d872022-02-19 18:25:57 -0800214 P, q, c = quadratic_cost(catapult, X_initial, X_final, len(u))
Austin Schuhc3e04282022-02-12 20:00:53 -0800215
Austin Schuh2e28d872022-02-19 18:25:57 -0800216
217 def mpc_cost2(u_solver):
218 u_matrix = numpy.matrix(u_solver).transpose()
219 cost = mpc_cost(catapult, X_initial, X_final, u_solver)
220 return cost
221
222
223 def mpc_cost3(u_solver):
224 u_matrix = numpy.matrix(u_solver).transpose()
225 return (0.5 * u_matrix.transpose() * P * u_matrix +
226 q.transpose() * u_matrix + c)[0, 0]
227
228 # If we provide scipy with the analytical jacobian and hessian, it solves
229 # more accurately and a *lot* faster.
230 def jacobian(u):
231 u_matrix = numpy.matrix(u).transpose()
232 return numpy.array(P * u_matrix + q)
233
234 def hessian(u):
235 return numpy.array(P)
236
237 constraints = []
238 constraints += [{
Austin Schuhc3e04282022-02-12 20:00:53 -0800239 'type': 'ineq',
240 'fun': vbat_constraint,
241 'args': (i, )
242 } for i in numpy.arange(len(u))]
243
244 constraints += [{
245 'type': 'ineq',
246 'fun': forward,
247 'args': (i, )
248 } for i in numpy.arange(len(u))]
249
Austin Schuh2e28d872022-02-19 18:25:57 -0800250 result = scipy.optimize.minimize(mpc_cost3,
Austin Schuhc3e04282022-02-12 20:00:53 -0800251 u,
Austin Schuh2e28d872022-02-19 18:25:57 -0800252 jac=jacobian,
253 hess=hessian,
Austin Schuhc3e04282022-02-12 20:00:53 -0800254 method='SLSQP',
Austin Schuh2e28d872022-02-19 18:25:57 -0800255 tol=1e-12,
Austin Schuhc3e04282022-02-12 20:00:53 -0800256 constraints=constraints)
257 print(result)
258
259 return result.x
260
261
262def CatapultProblem():
Austin Schuhac61c882022-02-23 17:22:22 -0800263 c = catapult_lib.Catapult(kCatapultWithBall)
Austin Schuhc3e04282022-02-12 20:00:53 -0800264
265 kHorizon = 40
266
267 u = [0.0] * kHorizon
268 X_initial = numpy.matrix([[0.0], [0.0]])
269 X_final = numpy.matrix([[2.0], [25.0]])
270
271
272 X_initial = numpy.matrix([[0.0], [0.0]])
273 X = X_initial.copy()
274
275 t_samples = [0.0]
276 x_samples = [0.0]
277 v_samples = [0.0]
278 a_samples = [0.0]
279
280 # Iteratively solve our MPC and simulate it.
281 u_samples = []
282 for i in range(kHorizon):
Austin Schuh2e28d872022-02-19 18:25:57 -0800283 u_horizon = SolveCatapult(c, X, X_final, u)
284
Austin Schuhc3e04282022-02-12 20:00:53 -0800285 u_samples.append(u_horizon[0])
286 v_prior = X[1, 0]
287 X = c.A * X + c.B * numpy.matrix([[u_horizon[0]]])
288 v = X[1, 0]
289 t_samples.append(t_samples[-1] + c.dt)
290 x_samples.append(X[0, 0])
291 v_samples.append(X[1, 0])
292 a_samples.append((v - v_prior) / c.dt)
293
294 u = u_horizon[1:]
295
296 print('Final state', X.transpose())
297 print('Final velocity', X[1, 0] * lever)
298 pylab.subplot(2, 1, 1)
299 pylab.plot(t_samples, x_samples, label="x")
300 pylab.plot(t_samples, v_samples, label="v")
301 pylab.plot(t_samples[1:], u_samples, label="u")
302 pylab.legend()
303 pylab.subplot(2, 1, 2)
304 pylab.plot(t_samples, a_samples, label="a")
305 pylab.legend()
306
307 pylab.show()
Austin Schuh82162452022-02-07 22:01:45 -0800308
309
310def main(argv):
Austin Schuh82162452022-02-07 22:01:45 -0800311 if FLAGS.plot:
Austin Schuhac61c882022-02-23 17:22:22 -0800312 # Do all our math with a lower voltage so we have headroom.
313 U = numpy.matrix([[9.0]])
314
315 prob = osqp.OSQP()
316
317 kHorizon = 40
318 catapult = catapult_lib.Catapult(kCatapultWithBall)
319 X_initial = numpy.matrix([[0.0], [0.0]])
320 X_final = numpy.matrix([[2.0], [25.0]])
321 P, q, c = quadratic_cost(catapult, X_initial, X_final, kHorizon)
322 A = numpy.identity(kHorizon)
323 l = numpy.zeros((kHorizon, 1))
324 u = numpy.ones((kHorizon, 1)) * 12.0
325
326 prob.setup(scipy.sparse.csr_matrix(P),
327 q,
328 scipy.sparse.csr_matrix(A),
329 l,
330 u,
331 warm_start=True)
332
333 result = prob.solve()
334 # Check solver status
335 if result.info.status != 'solved':
336 raise ValueError('OSQP did not solve the problem!')
337
338 # Apply first control input to the plant
339 print(result.x)
340
341 glog.debug("J ball", ball_mass * lever * lever)
342 glog.debug("J bar", J_bar)
343 glog.debug("bar mass", M_bar)
344 glog.debug("J cup", J_cup)
345 glog.debug("cup mass", M_cup)
346 glog.debug("J", J)
347 glog.debug("J Empty", JEmpty)
348
349 glog.debug(
Austin Schuh2e28d872022-02-19 18:25:57 -0800350 "For G:", G, " max speed ",
Austin Schuhac61c882022-02-23 17:22:22 -0800351 catapult_lib.MaxSpeed(params=kCatapultWithBall,
Austin Schuh2e28d872022-02-19 18:25:57 -0800352 U=U,
353 final_position=math.pi / 2.0))
354
355 CatapultProblem()
Austin Schuhac61c882022-02-23 17:22:22 -0800356
357 catapult_lib.PlotStep(params=kCatapultWithBall,
358 R=numpy.matrix([[1.0], [0.0]]))
359 catapult_lib.PlotKick(params=kCatapultWithBall,
360 R=numpy.matrix([[1.0], [0.0]]))
Austin Schuh2e28d872022-02-19 18:25:57 -0800361 return 0
362
Austin Schuhac61c882022-02-23 17:22:22 -0800363 catapult_lib.PlotShot(kCatapultWithBall,
364 U,
365 final_position=math.pi / 4.0)
Austin Schuh82162452022-02-07 22:01:45 -0800366
367 gs = []
368 speed = []
369 for i in numpy.linspace(0.01, 0.15, 150):
Austin Schuhac61c882022-02-23 17:22:22 -0800370 kCatapultWithBall.G = i
371 gs.append(kCatapultWithBall.G)
Austin Schuhc3e04282022-02-12 20:00:53 -0800372 speed.append(
Austin Schuhac61c882022-02-23 17:22:22 -0800373 catapult_lib.MaxSpeed(params=kCatapultWithBall,
Austin Schuhc3e04282022-02-12 20:00:53 -0800374 U=U,
375 final_position=math.pi / 2.0))
376 pylab.plot(gs, speed, label="max_speed")
Austin Schuh82162452022-02-07 22:01:45 -0800377 pylab.show()
Austin Schuh2e28d872022-02-19 18:25:57 -0800378
379 if len(argv) != 5:
380 glog.fatal(
381 'Expected .h file name and .cc file name for the catapult and integral catapult.'
382 )
383 else:
384 namespaces = ['y2022', 'control_loops', 'superstructure', 'catapult']
Austin Schuhac61c882022-02-23 17:22:22 -0800385 catapult_lib.WriteCatapult([kCatapultWithBall, kCatapultEmpty], argv[1:3], argv[3:5], namespaces)
Austin Schuh2e28d872022-02-19 18:25:57 -0800386 return 0
Austin Schuh82162452022-02-07 22:01:45 -0800387
388
389if __name__ == '__main__':
390 argv = FLAGS(sys.argv)
Austin Schuhac61c882022-02-23 17:22:22 -0800391 glog.init()
Austin Schuh82162452022-02-07 22:01:45 -0800392 sys.exit(main(argv))