blob: 322e41a2362d8111b338015d4bc6296a60093497 [file] [log] [blame]
Austin Schuhb5d302f2019-01-20 20:51:19 -08001#!/usr/bin/python
2
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):
12 def __init__(self,
13 name,
14 motor,
15 G,
16 radius,
17 mass,
18 q_pos,
19 q_vel,
20 kalman_q_pos,
21 kalman_q_vel,
22 kalman_q_voltage,
23 kalman_r_position,
24 dt=0.005):
25 self.name = name
26 self.motor = motor
27 self.G = G
28 self.radius = radius
29 self.mass = mass
30 self.q_pos = q_pos
31 self.q_vel = q_vel
32 self.kalman_q_pos = kalman_q_pos
33 self.kalman_q_vel = kalman_q_vel
34 self.kalman_q_voltage = kalman_q_voltage
35 self.kalman_r_position = kalman_r_position
36 self.dt = dt
37
38
39class LinearSystem(control_loop.ControlLoop):
40 def __init__(self, params, name='LinearSystem'):
41 super(LinearSystem, self).__init__(name)
42 self.params = params
43
44 self.motor = params.motor
45
46 # Gear ratio
47 self.G = params.G
48 self.radius = params.radius
49
Austin Schuh2e554032019-01-21 15:07:27 -080050 # Mass in kg
Austin Schuhb5d302f2019-01-20 20:51:19 -080051 self.mass = params.mass + self.motor.motor_inertia / (
52 (self.G * self.radius)**2.0)
53
54 # Control loop time step
55 self.dt = params.dt
56
57 # State is [position, velocity]
58 # Input is [Voltage]
59 C1 = self.motor.Kt / (self.G * self.G * self.radius * self.radius *
60 self.motor.resistance * self.mass *
61 self.motor.Kv)
62 C2 = self.motor.Kt / (self.G * self.radius * self.motor.resistance *
63 self.mass)
64
65 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
66
67 # Start with the unmodified input
68 self.B_continuous = numpy.matrix([[0], [C2]])
69 glog.debug(repr(self.A_continuous))
70 glog.debug(repr(self.B_continuous))
71
72 self.C = numpy.matrix([[1, 0]])
73 self.D = numpy.matrix([[0]])
74
75 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
76 self.B_continuous, self.dt)
77
78 controllability = controls.ctrb(self.A, self.B)
79 glog.debug('Controllability of %d',
80 numpy.linalg.matrix_rank(controllability))
81 glog.debug('Mass: %f', self.mass)
82 glog.debug('Stall force: %f', self.motor.stall_torque / self.G / self.radius)
83 glog.debug('Stall acceleration: %f', self.motor.stall_torque / self.G /
84 self.radius / self.mass)
85
86 glog.debug('Free speed is %f',
87 -self.B_continuous[1, 0] / self.A_continuous[1, 1] * 12.0)
88
89 self.Q = numpy.matrix([[(1.0 / (self.params.q_pos**2.0)), 0.0],
90 [0.0, (1.0 / (self.params.q_vel**2.0))]])
91
92 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
93 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
94
95 q_pos_ff = 0.005
96 q_vel_ff = 1.0
97 self.Qff = numpy.matrix([[(1.0 / (q_pos_ff**2.0)), 0.0],
98 [0.0, (1.0 / (q_vel_ff**2.0))]])
99
100 self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
101
102 glog.debug('K %s', repr(self.K))
103 glog.debug('Poles are %s',
104 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]))
105
106 self.Q = numpy.matrix([[(self.params.kalman_q_pos**2.0), 0.0],
107 [0.0, (self.params.kalman_q_vel**2.0)]])
108
109 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
110
111 self.KalmanGain, self.Q_steady = controls.kalman(
112 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
113
114 glog.debug('Kal %s', repr(self.KalmanGain))
115
116 # The box formed by U_min and U_max must encompass all possible values,
117 # or else Austin's code gets angry.
118 self.U_max = numpy.matrix([[12.0]])
119 self.U_min = numpy.matrix([[-12.0]])
120
121 self.InitializeState()
122
123
124class IntegralLinearSystem(LinearSystem):
125 def __init__(self, params, name='IntegralLinearSystem'):
126 super(IntegralLinearSystem, self).__init__(params, name=name)
127
Austin Schuhb5d302f2019-01-20 20:51:19 -0800128 self.A_continuous_unaugmented = self.A_continuous
129 self.B_continuous_unaugmented = self.B_continuous
130
131 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
132 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
133 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
134
135 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
136 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
137
138 self.C_unaugmented = self.C
139 self.C = numpy.matrix(numpy.zeros((1, 3)))
140 self.C[0:1, 0:2] = self.C_unaugmented
141
142 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
143 self.B_continuous, self.dt)
144
145 self.Q = numpy.matrix(
146 [[(self.params.kalman_q_pos**2.0), 0.0, 0.0],
147 [0.0, (self.params.kalman_q_vel**2.0), 0.0],
148 [0.0, 0.0, (self.params.kalman_q_voltage**2.0)]])
149
150 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
151
152 self.KalmanGain, self.Q_steady = controls.kalman(
153 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
154
155 self.K_unaugmented = self.K
156 self.K = numpy.matrix(numpy.zeros((1, 3)))
157 self.K[0, 0:2] = self.K_unaugmented
158 self.K[0, 2] = 1
159
160 self.Kff = numpy.concatenate(
161 (self.Kff, numpy.matrix(numpy.zeros((1, 1)))), axis=1)
162
163 self.InitializeState()
164
165
166def RunTest(plant,
167 end_goal,
168 controller,
169 observer=None,
170 duration=1.0,
171 use_profile=True,
172 kick_time=0.5,
173 kick_magnitude=0.0,
Lee Mracek28795ef2019-01-27 05:29:37 -0500174 max_velocity=0.3,
175 max_acceleration=10.0):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800176 """Runs the plant with an initial condition and goal.
177
178 Args:
179 plant: plant object to use.
180 end_goal: end_goal state.
Austin Schuh2e554032019-01-21 15:07:27 -0800181 controller: LinearSystem object to get K from, or None if we should
Austin Schuhb5d302f2019-01-20 20:51:19 -0800182 use plant.
Austin Schuh2e554032019-01-21 15:07:27 -0800183 observer: LinearSystem object to use for the observer, or None if we
184 should use the actual state.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800185 duration: float, time in seconds to run the simulation for.
186 kick_time: float, time in seconds to kick the robot.
187 kick_magnitude: float, disturbance in volts to apply.
188 max_velocity: float, the max speed in m/s to profile.
Lee Mracek28795ef2019-01-27 05:29:37 -0500189 max_acceleration: float, the max acceleration in m/s/s to profile.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800190 """
191 t_plot = []
192 x_plot = []
193 v_plot = []
194 a_plot = []
195 x_goal_plot = []
196 v_goal_plot = []
197 x_hat_plot = []
198 u_plot = []
199 offset_plot = []
200
201 if controller is None:
202 controller = plant
203
204 vbat = 12.0
205
206 goal = numpy.concatenate(
207 (plant.X, numpy.matrix(numpy.zeros((1, 1)))), axis=0)
208
209 profile = TrapezoidProfile(plant.dt)
Lee Mracek28795ef2019-01-27 05:29:37 -0500210 profile.set_maximum_acceleration(max_acceleration)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800211 profile.set_maximum_velocity(max_velocity)
212 profile.SetGoal(goal[0, 0])
213
214 U_last = numpy.matrix(numpy.zeros((1, 1)))
215 iterations = int(duration / plant.dt)
216 for i in xrange(iterations):
217 t = i * plant.dt
218 observer.Y = plant.Y
219 observer.CorrectObserver(U_last)
220
221 offset_plot.append(observer.X_hat[2, 0])
222 x_hat_plot.append(observer.X_hat[0, 0])
223
224 next_goal = numpy.concatenate(
225 (profile.Update(end_goal[0, 0], end_goal[1, 0]),
226 numpy.matrix(numpy.zeros((1, 1)))),
227 axis=0)
228
229 ff_U = controller.Kff * (next_goal - observer.A * goal)
230
231 if use_profile:
232 U_uncapped = controller.K * (goal - observer.X_hat) + ff_U
233 x_goal_plot.append(goal[0, 0])
234 v_goal_plot.append(goal[1, 0])
235 else:
236 U_uncapped = controller.K * (end_goal - observer.X_hat)
237 x_goal_plot.append(end_goal[0, 0])
238 v_goal_plot.append(end_goal[1, 0])
239
240 U = U_uncapped.copy()
241 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
242 x_plot.append(plant.X[0, 0])
243
244 if v_plot:
245 last_v = v_plot[-1]
246 else:
247 last_v = 0
248
249 v_plot.append(plant.X[1, 0])
250 a_plot.append((v_plot[-1] - last_v) / plant.dt)
251
252 u_offset = 0.0
253 if t >= kick_time:
254 u_offset = kick_magnitude
255 plant.Update(U + u_offset)
256
257 observer.PredictObserver(U)
258
259 t_plot.append(t)
260 u_plot.append(U[0, 0])
261
262 ff_U -= U_uncapped - U
263 goal = controller.A * goal + controller.B * ff_U
264
265 if U[0, 0] != U_uncapped[0, 0]:
266 profile.MoveCurrentState(
267 numpy.matrix([[goal[0, 0]], [goal[1, 0]]]))
268
269 glog.debug('Time: %f', t_plot[-1])
270 glog.debug('goal_error %s', repr(end_goal - goal))
271 glog.debug('error %s', repr(observer.X_hat - end_goal))
272
273 pylab.subplot(3, 1, 1)
274 pylab.plot(t_plot, x_plot, label='x')
275 pylab.plot(t_plot, x_hat_plot, label='x_hat')
276 pylab.plot(t_plot, x_goal_plot, label='x_goal')
277 pylab.legend()
278
279 pylab.subplot(3, 1, 2)
280 pylab.plot(t_plot, u_plot, label='u')
281 pylab.plot(t_plot, offset_plot, label='voltage_offset')
282 pylab.legend()
283
284 pylab.subplot(3, 1, 3)
285 pylab.plot(t_plot, a_plot, label='a')
286 pylab.legend()
287
288 pylab.show()
289
290
291def PlotStep(params, R):
292 """Plots a step move to the goal.
293
294 Args:
295 R: numpy.matrix(2, 1), the goal"""
296 plant = LinearSystem(params, params.name)
297 controller = IntegralLinearSystem(params, params.name)
298 observer = IntegralLinearSystem(params, params.name)
299
300 # Test moving the system.
301 initial_X = numpy.matrix([[0.0], [0.0]])
302 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
303 augmented_R[0:2, :] = R
304 RunTest(
305 plant,
306 end_goal=augmented_R,
307 controller=controller,
308 observer=observer,
309 duration=2.0,
310 use_profile=False,
311 kick_time=1.0,
312 kick_magnitude=0.0)
313
314
315def PlotKick(params, R):
316 """Plots a step motion with a kick at 1.0 seconds.
317
318 Args:
319 R: numpy.matrix(2, 1), the goal"""
320 plant = LinearSystem(params, params.name)
321 controller = IntegralLinearSystem(params, params.name)
322 observer = IntegralLinearSystem(params, params.name)
323
324 # Test moving the system.
325 initial_X = numpy.matrix([[0.0], [0.0]])
326 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
327 augmented_R[0:2, :] = R
328 RunTest(
329 plant,
330 end_goal=augmented_R,
331 controller=controller,
332 observer=observer,
333 duration=2.0,
334 use_profile=False,
335 kick_time=1.0,
336 kick_magnitude=2.0)
337
338
Lee Mracek28795ef2019-01-27 05:29:37 -0500339def PlotMotion(params, R, max_velocity=0.3, max_acceleration=10.0):
Austin Schuhb5d302f2019-01-20 20:51:19 -0800340 """Plots a trapezoidal motion.
341
342 Args:
343 R: numpy.matrix(2, 1), the goal,
344 max_velocity: float, The max velocity of the profile.
Lee Mracek28795ef2019-01-27 05:29:37 -0500345 max_acceleration: float, The max acceleration of the profile.
Austin Schuhb5d302f2019-01-20 20:51:19 -0800346 """
347 plant = LinearSystem(params, params.name)
348 controller = IntegralLinearSystem(params, params.name)
349 observer = IntegralLinearSystem(params, params.name)
350
351 # Test moving the system.
352 initial_X = numpy.matrix([[0.0], [0.0]])
353 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
354 augmented_R[0:2, :] = R
355 RunTest(
356 plant,
357 end_goal=augmented_R,
358 controller=controller,
359 observer=observer,
360 duration=2.0,
361 use_profile=True,
Lee Mracek28795ef2019-01-27 05:29:37 -0500362 max_velocity=max_velocity,
363 max_acceleration=max_acceleration)
Austin Schuhb5d302f2019-01-20 20:51:19 -0800364
365
366def WriteLinearSystem(params, plant_files, controller_files, year_namespaces):
367 """Writes out the constants for a linear system to a file.
368
369 Args:
370 params: LinearSystemParams, the parameters defining the system.
371 plant_files: list of strings, the cc and h files for the plant.
372 controller_files: list of strings, the cc and h files for the integral
373 controller.
374 year_namespaces: list of strings, the namespace list to use.
375 """
376 # Write the generated constants out to a file.
377 linear_system = LinearSystem(params, params.name)
378 loop_writer = control_loop.ControlLoopWriter(
379 linear_system.name, [linear_system], namespaces=year_namespaces)
380 loop_writer.AddConstant(
381 control_loop.Constant('kFreeSpeed', '%f', linear_system.motor.
382 free_speed / (2.0 * numpy.pi)))
383 loop_writer.AddConstant(
384 control_loop.Constant('kOutputRatio', '%f', linear_system.G *
385 linear_system.radius))
386 loop_writer.Write(plant_files[0], plant_files[1])
387
388 integral_linear_system = IntegralLinearSystem(params,
389 'Integral' + params.name)
390 integral_loop_writer = control_loop.ControlLoopWriter(
391 integral_linear_system.name, [integral_linear_system],
392 namespaces=year_namespaces)
393 integral_loop_writer.Write(controller_files[0], controller_files[1])