blob: 3bda4e1570b8090df73e2bd371f7f726766df076 [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
Austin Schuhb5d302f2019-01-20 20:51:19 -08002
3from aos.util.trapezoid_profile import TrapezoidProfile
4from frc971.control_loops.python import control_loop
5from frc971.control_loops.python import controls
6import numpy
Austin Schuhb5d302f2019-01-20 20:51:19 -08007from matplotlib import pylab
8import glog
9
10
11class LinearSystemParams(object):
Ravago Jones5127ccc2022-07-31 16:32:45 -070012
Austin Schuhb5d302f2019-01-20 20:51:19 -080013 def __init__(self,
14 name,
15 motor,
16 G,
17 radius,
18 mass,
19 q_pos,
20 q_vel,
21 kalman_q_pos,
22 kalman_q_vel,
23 kalman_q_voltage,
24 kalman_r_position,
Austin Schuh63d095d2019-02-23 11:57:12 -080025 dt=0.00505):
Austin Schuhb5d302f2019-01-20 20:51:19 -080026 self.name = name
27 self.motor = motor
28 self.G = G
29 self.radius = radius
30 self.mass = mass
31 self.q_pos = q_pos
32 self.q_vel = q_vel
33 self.kalman_q_pos = kalman_q_pos
34 self.kalman_q_vel = kalman_q_vel
35 self.kalman_q_voltage = kalman_q_voltage
36 self.kalman_r_position = kalman_r_position
37 self.dt = dt
38
39
40class LinearSystem(control_loop.ControlLoop):
Ravago Jones5127ccc2022-07-31 16:32:45 -070041
Austin Schuhb5d302f2019-01-20 20:51:19 -080042 def __init__(self, params, name='LinearSystem'):
43 super(LinearSystem, self).__init__(name)
44 self.params = params
45
46 self.motor = params.motor
47
48 # Gear ratio
49 self.G = params.G
50 self.radius = params.radius
51
Austin Schuh2e554032019-01-21 15:07:27 -080052 # Mass in kg
Austin Schuhb5d302f2019-01-20 20:51:19 -080053 self.mass = params.mass + self.motor.motor_inertia / (
54 (self.G * self.radius)**2.0)
55
56 # Control loop time step
57 self.dt = params.dt
58
59 # State is [position, velocity]
60 # Input is [Voltage]
Ravago Jones5127ccc2022-07-31 16:32:45 -070061 C1 = self.motor.Kt / (self.G * self.G * self.radius * self.radius *
62 self.motor.resistance * self.mass *
63 self.motor.Kv)
64 C2 = self.motor.Kt / (self.G * self.radius * self.motor.resistance *
65 self.mass)
Austin Schuhb5d302f2019-01-20 20:51:19 -080066
67 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
68
69 # Start with the unmodified input
70 self.B_continuous = numpy.matrix([[0], [C2]])
71 glog.debug(repr(self.A_continuous))
72 glog.debug(repr(self.B_continuous))
73
74 self.C = numpy.matrix([[1, 0]])
75 self.D = numpy.matrix([[0]])
76
77 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
78 self.B_continuous, self.dt)
79
80 controllability = controls.ctrb(self.A, self.B)
81 glog.debug('Controllability of %d',
82 numpy.linalg.matrix_rank(controllability))
83 glog.debug('Mass: %f', self.mass)
Tyler Chatow6738c362019-02-16 14:12:30 -080084 glog.debug('Stall force: %f',
85 self.motor.stall_torque / self.G / self.radius)
86 glog.debug('Stall acceleration: %f',
87 self.motor.stall_torque / self.G / self.radius / self.mass)
Austin Schuhb5d302f2019-01-20 20:51:19 -080088
89 glog.debug('Free speed is %f',
90 -self.B_continuous[1, 0] / self.A_continuous[1, 1] * 12.0)
91
92 self.Q = numpy.matrix([[(1.0 / (self.params.q_pos**2.0)), 0.0],
93 [0.0, (1.0 / (self.params.q_vel**2.0))]])
94
95 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
96 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
97
98 q_pos_ff = 0.005
99 q_vel_ff = 1.0
100 self.Qff = numpy.matrix([[(1.0 / (q_pos_ff**2.0)), 0.0],
101 [0.0, (1.0 / (q_vel_ff**2.0))]])
102
103 self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
104
105 glog.debug('K %s', repr(self.K))
106 glog.debug('Poles are %s',
107 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]))
108
109 self.Q = numpy.matrix([[(self.params.kalman_q_pos**2.0), 0.0],
110 [0.0, (self.params.kalman_q_vel**2.0)]])
111
112 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
113
Ravago Jones5127ccc2022-07-31 16:32:45 -0700114 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
115 B=self.B,
116 C=self.C,
117 Q=self.Q,
118 R=self.R)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800119
120 glog.debug('Kal %s', repr(self.KalmanGain))
121
122 # The box formed by U_min and U_max must encompass all possible values,
123 # or else Austin's code gets angry.
124 self.U_max = numpy.matrix([[12.0]])
125 self.U_min = numpy.matrix([[-12.0]])
126
127 self.InitializeState()
128
129
130class IntegralLinearSystem(LinearSystem):
Ravago Jones5127ccc2022-07-31 16:32:45 -0700131
Austin Schuhb5d302f2019-01-20 20:51:19 -0800132 def __init__(self, params, name='IntegralLinearSystem'):
133 super(IntegralLinearSystem, self).__init__(params, name=name)
134
Austin Schuhb5d302f2019-01-20 20:51:19 -0800135 self.A_continuous_unaugmented = self.A_continuous
136 self.B_continuous_unaugmented = self.B_continuous
137
138 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
139 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
140 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
141
142 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
143 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
144
145 self.C_unaugmented = self.C
146 self.C = numpy.matrix(numpy.zeros((1, 3)))
147 self.C[0:1, 0:2] = self.C_unaugmented
148
149 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
150 self.B_continuous, self.dt)
151
Tyler Chatow6738c362019-02-16 14:12:30 -0800152 self.Q = numpy.matrix([[(self.params.kalman_q_pos**2.0), 0.0, 0.0],
153 [0.0, (self.params.kalman_q_vel**2.0), 0.0],
Ravago Jones5127ccc2022-07-31 16:32:45 -0700154 [0.0, 0.0,
155 (self.params.kalman_q_voltage**2.0)]])
Austin Schuhb5d302f2019-01-20 20:51:19 -0800156
157 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
158
Ravago Jones5127ccc2022-07-31 16:32:45 -0700159 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
160 B=self.B,
161 C=self.C,
162 Q=self.Q,
163 R=self.R)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800164
165 self.K_unaugmented = self.K
166 self.K = numpy.matrix(numpy.zeros((1, 3)))
167 self.K[0, 0:2] = self.K_unaugmented
168 self.K[0, 2] = 1
169
170 self.Kff = numpy.concatenate(
171 (self.Kff, numpy.matrix(numpy.zeros((1, 1)))), axis=1)
172
173 self.InitializeState()
174
175
176def RunTest(plant,
177 end_goal,
178 controller,
179 observer=None,
180 duration=1.0,
181 use_profile=True,
182 kick_time=0.5,
183 kick_magnitude=0.0,
Lee Mracek28795ef2019-01-27 05:29:37 -0500184 max_velocity=0.3,
185 max_acceleration=10.0):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800186 """Runs the plant with an initial condition and goal.
187
188 Args:
189 plant: plant object to use.
190 end_goal: end_goal state.
Austin Schuh2e554032019-01-21 15:07:27 -0800191 controller: LinearSystem object to get K from, or None if we should
Austin Schuhb5d302f2019-01-20 20:51:19 -0800192 use plant.
Austin Schuh2e554032019-01-21 15:07:27 -0800193 observer: LinearSystem object to use for the observer, or None if we
194 should use the actual state.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800195 duration: float, time in seconds to run the simulation for.
196 kick_time: float, time in seconds to kick the robot.
197 kick_magnitude: float, disturbance in volts to apply.
198 max_velocity: float, the max speed in m/s to profile.
Lee Mracek28795ef2019-01-27 05:29:37 -0500199 max_acceleration: float, the max acceleration in m/s/s to profile.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800200 """
201 t_plot = []
202 x_plot = []
203 v_plot = []
204 a_plot = []
205 x_goal_plot = []
206 v_goal_plot = []
207 x_hat_plot = []
208 u_plot = []
209 offset_plot = []
210
211 if controller is None:
212 controller = plant
213
214 vbat = 12.0
215
Tyler Chatow6738c362019-02-16 14:12:30 -0800216 goal = numpy.concatenate((plant.X, numpy.matrix(numpy.zeros((1, 1)))),
217 axis=0)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800218
219 profile = TrapezoidProfile(plant.dt)
Lee Mracek28795ef2019-01-27 05:29:37 -0500220 profile.set_maximum_acceleration(max_acceleration)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800221 profile.set_maximum_velocity(max_velocity)
222 profile.SetGoal(goal[0, 0])
223
224 U_last = numpy.matrix(numpy.zeros((1, 1)))
225 iterations = int(duration / plant.dt)
Austin Schuh5ea48472021-02-02 20:46:41 -0800226 for i in range(iterations):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800227 t = i * plant.dt
228 observer.Y = plant.Y
229 observer.CorrectObserver(U_last)
230
231 offset_plot.append(observer.X_hat[2, 0])
232 x_hat_plot.append(observer.X_hat[0, 0])
233
Ravago Jones5127ccc2022-07-31 16:32:45 -0700234 next_goal = numpy.concatenate((profile.Update(
235 end_goal[0, 0], end_goal[1, 0]), numpy.matrix(numpy.zeros(
236 (1, 1)))),
237 axis=0)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800238
239 ff_U = controller.Kff * (next_goal - observer.A * goal)
240
241 if use_profile:
242 U_uncapped = controller.K * (goal - observer.X_hat) + ff_U
243 x_goal_plot.append(goal[0, 0])
244 v_goal_plot.append(goal[1, 0])
245 else:
246 U_uncapped = controller.K * (end_goal - observer.X_hat)
247 x_goal_plot.append(end_goal[0, 0])
248 v_goal_plot.append(end_goal[1, 0])
249
250 U = U_uncapped.copy()
251 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
252 x_plot.append(plant.X[0, 0])
253
254 if v_plot:
255 last_v = v_plot[-1]
256 else:
257 last_v = 0
258
259 v_plot.append(plant.X[1, 0])
260 a_plot.append((v_plot[-1] - last_v) / plant.dt)
261
262 u_offset = 0.0
263 if t >= kick_time:
264 u_offset = kick_magnitude
265 plant.Update(U + u_offset)
266
267 observer.PredictObserver(U)
268
269 t_plot.append(t)
270 u_plot.append(U[0, 0])
271
272 ff_U -= U_uncapped - U
273 goal = controller.A * goal + controller.B * ff_U
274
275 if U[0, 0] != U_uncapped[0, 0]:
Ravago Jones5127ccc2022-07-31 16:32:45 -0700276 profile.MoveCurrentState(numpy.matrix([[goal[0, 0]], [goal[1,
277 0]]]))
Austin Schuhb5d302f2019-01-20 20:51:19 -0800278
279 glog.debug('Time: %f', t_plot[-1])
280 glog.debug('goal_error %s', repr(end_goal - goal))
281 glog.debug('error %s', repr(observer.X_hat - end_goal))
282
283 pylab.subplot(3, 1, 1)
284 pylab.plot(t_plot, x_plot, label='x')
285 pylab.plot(t_plot, x_hat_plot, label='x_hat')
286 pylab.plot(t_plot, x_goal_plot, label='x_goal')
287 pylab.legend()
288
289 pylab.subplot(3, 1, 2)
290 pylab.plot(t_plot, u_plot, label='u')
291 pylab.plot(t_plot, offset_plot, label='voltage_offset')
292 pylab.legend()
293
294 pylab.subplot(3, 1, 3)
295 pylab.plot(t_plot, a_plot, label='a')
296 pylab.legend()
297
298 pylab.show()
299
300
Austin Schuh9d9d3742019-02-15 23:00:13 -0800301def PlotStep(params, R, plant_params=None):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800302 """Plots a step move to the goal.
303
304 Args:
Austin Schuh9d9d3742019-02-15 23:00:13 -0800305 params: LinearSystemParams for the controller and observer
306 plant_params: LinearSystemParams for the plant. Defaults to params if
307 plant_params is None.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800308 R: numpy.matrix(2, 1), the goal"""
Austin Schuh9d9d3742019-02-15 23:00:13 -0800309 plant = LinearSystem(plant_params or params, params.name)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800310 controller = IntegralLinearSystem(params, params.name)
311 observer = IntegralLinearSystem(params, params.name)
312
313 # Test moving the system.
314 initial_X = numpy.matrix([[0.0], [0.0]])
315 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
316 augmented_R[0:2, :] = R
Ravago Jones5127ccc2022-07-31 16:32:45 -0700317 RunTest(plant,
318 end_goal=augmented_R,
319 controller=controller,
320 observer=observer,
321 duration=2.0,
322 use_profile=False,
323 kick_time=1.0,
324 kick_magnitude=0.0)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800325
326
Austin Schuh9d9d3742019-02-15 23:00:13 -0800327def PlotKick(params, R, plant_params=None):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800328 """Plots a step motion with a kick at 1.0 seconds.
329
330 Args:
Austin Schuh9d9d3742019-02-15 23:00:13 -0800331 params: LinearSystemParams for the controller and observer
332 plant_params: LinearSystemParams for the plant. Defaults to params if
333 plant_params is None.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800334 R: numpy.matrix(2, 1), the goal"""
Austin Schuh9d9d3742019-02-15 23:00:13 -0800335 plant = LinearSystem(plant_params or params, params.name)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800336 controller = IntegralLinearSystem(params, params.name)
337 observer = IntegralLinearSystem(params, params.name)
338
339 # Test moving the system.
340 initial_X = numpy.matrix([[0.0], [0.0]])
341 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
342 augmented_R[0:2, :] = R
Ravago Jones5127ccc2022-07-31 16:32:45 -0700343 RunTest(plant,
344 end_goal=augmented_R,
345 controller=controller,
346 observer=observer,
347 duration=2.0,
348 use_profile=False,
349 kick_time=1.0,
350 kick_magnitude=2.0)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800351
352
Austin Schuh9d9d3742019-02-15 23:00:13 -0800353def PlotMotion(params,
354 R,
355 max_velocity=0.3,
356 max_acceleration=10.0,
357 plant_params=None):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800358 """Plots a trapezoidal motion.
359
360 Args:
Austin Schuh9d9d3742019-02-15 23:00:13 -0800361 params: LinearSystemParams for the controller and observer
362 plant_params: LinearSystemParams for the plant. Defaults to params if
363 plant_params is None.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800364 R: numpy.matrix(2, 1), the goal,
365 max_velocity: float, The max velocity of the profile.
Lee Mracek28795ef2019-01-27 05:29:37 -0500366 max_acceleration: float, The max acceleration of the profile.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800367 """
Austin Schuh9d9d3742019-02-15 23:00:13 -0800368 plant = LinearSystem(plant_params or params, params.name)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800369 controller = IntegralLinearSystem(params, params.name)
370 observer = IntegralLinearSystem(params, params.name)
371
372 # Test moving the system.
373 initial_X = numpy.matrix([[0.0], [0.0]])
374 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
375 augmented_R[0:2, :] = R
Ravago Jones5127ccc2022-07-31 16:32:45 -0700376 RunTest(plant,
377 end_goal=augmented_R,
378 controller=controller,
379 observer=observer,
380 duration=2.0,
381 use_profile=True,
382 max_velocity=max_velocity,
383 max_acceleration=max_acceleration)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800384
385
386def WriteLinearSystem(params, plant_files, controller_files, year_namespaces):
387 """Writes out the constants for a linear system to a file.
388
389 Args:
Tyler Chatowd3afdef2019-04-06 22:15:26 -0700390 params: list of LinearSystemParams or LinearSystemParams, the
391 parameters defining the system.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800392 plant_files: list of strings, the cc and h files for the plant.
393 controller_files: list of strings, the cc and h files for the integral
394 controller.
395 year_namespaces: list of strings, the namespace list to use.
396 """
397 # Write the generated constants out to a file.
Tyler Chatowd3afdef2019-04-06 22:15:26 -0700398 linear_systems = []
399 integral_linear_systems = []
400
401 if type(params) is list:
402 name = params[0].name
403 for index, param in enumerate(params):
Ravago Jones26f7ad02021-02-05 15:45:59 -0800404 linear_systems.append(LinearSystem(param, param.name + str(index)))
Tyler Chatowd3afdef2019-04-06 22:15:26 -0700405 integral_linear_systems.append(
Ravago Jones26f7ad02021-02-05 15:45:59 -0800406 IntegralLinearSystem(param,
407 'Integral' + param.name + str(index)))
Tyler Chatowd3afdef2019-04-06 22:15:26 -0700408 else:
409 name = params.name
410 linear_systems.append(LinearSystem(params, params.name))
411 integral_linear_systems.append(
412 IntegralLinearSystem(params, 'Integral' + params.name))
413
Ravago Jones5127ccc2022-07-31 16:32:45 -0700414 loop_writer = control_loop.ControlLoopWriter(name,
415 linear_systems,
416 namespaces=year_namespaces)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800417 loop_writer.AddConstant(
Ravago Jones26f7ad02021-02-05 15:45:59 -0800418 control_loop.Constant('kFreeSpeed', '%f',
419 linear_systems[0].motor.free_speed))
Austin Schuhb5d302f2019-01-20 20:51:19 -0800420 loop_writer.AddConstant(
Ravago Jones26f7ad02021-02-05 15:45:59 -0800421 control_loop.Constant('kOutputRatio', '%f',
422 linear_systems[0].G * linear_systems[0].radius))
Alex Perry5fb5ff22019-02-09 21:53:17 -0800423 loop_writer.AddConstant(
Tyler Chatowd3afdef2019-04-06 22:15:26 -0700424 control_loop.Constant('kRadius', '%f', linear_systems[0].radius))
Austin Schuhb5d302f2019-01-20 20:51:19 -0800425 loop_writer.Write(plant_files[0], plant_files[1])
426
Austin Schuhb5d302f2019-01-20 20:51:19 -0800427 integral_loop_writer = control_loop.ControlLoopWriter(
Ravago Jones26f7ad02021-02-05 15:45:59 -0800428 'Integral' + name, integral_linear_systems, namespaces=year_namespaces)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800429 integral_loop_writer.Write(controller_files[0], controller_files[1])