blob: d3d77c5b10915b12bcd3d09d7b380b735e4c5a83 [file] [log] [blame]
Austin Schuh1a387962015-01-31 16:36:20 -08001#!/usr/bin/python
2
Austin Schuh6c20f202017-02-18 22:31:44 -08003from frc971.control_loops.python import control_loop
4from frc971.control_loops.python import controls
Austin Schuh1a387962015-01-31 16:36:20 -08005import numpy
Austin Schuhdbd6bfa2015-02-14 21:25:16 -08006import math
Austin Schuh1a387962015-01-31 16:36:20 -08007import sys
Austin Schuh1a387962015-01-31 16:36:20 -08008from matplotlib import pylab
9
Austin Schuh6c20f202017-02-18 22:31:44 -080010import gflags
11import glog
12
13FLAGS = gflags.FLAGS
14
15gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
16
Austin Schuh8de10c72015-02-27 23:33:40 -080017
Austin Schuh1a387962015-01-31 16:36:20 -080018class Arm(control_loop.ControlLoop):
Austin Schuh6c20f202017-02-18 22:31:44 -080019 def __init__(self, name='Arm', mass=None):
Austin Schuh1a387962015-01-31 16:36:20 -080020 super(Arm, self).__init__(name)
21 # Stall Torque in N m
22 self.stall_torque = 0.476
23 # Stall Current in Amps
24 self.stall_current = 80.730
25 # Free Speed in RPM
26 self.free_speed = 13906.0
27 # Free Current in Amps
28 self.free_current = 5.820
29 # Mass of the arm
30 if mass is None:
31 self.mass = 13.0
32 else:
33 self.mass = mass
34
35 # Resistance of the motor
36 self.R = 12.0 / self.stall_current
37 # Motor velocity constant
38 self.Kv = ((self.free_speed / 60.0 * 2.0 * numpy.pi) /
39 (12.0 - self.R * self.free_current))
40 # Torque constant
41 self.Kt = self.stall_torque / self.stall_current
42 # Gear ratio
43 self.G = (44.0 / 12.0) * (54.0 / 14.0) * (54.0 / 14.0) * (44.0 / 20.0) * (72.0 / 16.0)
44 # Fridge arm length
45 self.r = 32 * 0.0254
46 # Control loop time step
47 self.dt = 0.005
48
49 # Arm moment of inertia
50 self.J = self.r * self.mass
51
52 # Arm left/right spring constant (N*m / radian)
Austin Schuh0b477d02015-02-20 22:09:13 -080053 self.spring = 100.0
Austin Schuh1a387962015-01-31 16:36:20 -080054
55 # State is [average position, average velocity,
56 # position difference/2, velocity difference/2]
57 # Position difference is 1 - 2
58 # Input is [Voltage 1, Voltage 2]
59
Austin Schuh8de10c72015-02-27 23:33:40 -080060 self.C1 = self.spring / (self.J * 0.5)
61 self.C2 = self.Kt * self.G / (self.J * 0.5 * self.R)
62 self.C3 = self.G * self.G * self.Kt / (self.R * self.J * 0.5 * self.Kv)
Austin Schuh1a387962015-01-31 16:36:20 -080063
64 self.A_continuous = numpy.matrix(
65 [[0, 1, 0, 0],
Austin Schuh8de10c72015-02-27 23:33:40 -080066 [0, -self.C3, 0, 0],
Austin Schuh1a387962015-01-31 16:36:20 -080067 [0, 0, 0, 1],
Austin Schuh8de10c72015-02-27 23:33:40 -080068 [0, 0, -self.C1 * 2.0, -self.C3]])
Austin Schuh1a387962015-01-31 16:36:20 -080069
Austin Schuh6c20f202017-02-18 22:31:44 -080070 glog.debug('Full speed is %f', self.C2 / self.C3 * 12.0)
Austin Schuh1a387962015-01-31 16:36:20 -080071
Austin Schuh6c20f202017-02-18 22:31:44 -080072 glog.debug('Stall arm difference is %f', 12.0 * self.C2 / self.C1)
73 glog.debug('Stall arm difference first principles is %f', self.stall_torque * self.G / self.spring)
Austin Schuhdbd6bfa2015-02-14 21:25:16 -080074
Austin Schuh6c20f202017-02-18 22:31:44 -080075 glog.debug('5 degrees of arm error is %f', self.spring / self.r * (math.pi * 5.0 / 180.0))
Austin Schuhdbd6bfa2015-02-14 21:25:16 -080076
Austin Schuh1a387962015-01-31 16:36:20 -080077 # Start with the unmodified input
78 self.B_continuous = numpy.matrix(
79 [[0, 0],
Austin Schuh8de10c72015-02-27 23:33:40 -080080 [self.C2 / 2.0, self.C2 / 2.0],
Austin Schuh1a387962015-01-31 16:36:20 -080081 [0, 0],
Austin Schuh8de10c72015-02-27 23:33:40 -080082 [self.C2 / 2.0, -self.C2 / 2.0]])
Austin Schuh1a387962015-01-31 16:36:20 -080083
84 self.C = numpy.matrix([[1, 0, 1, 0],
85 [1, 0, -1, 0]])
86 self.D = numpy.matrix([[0, 0],
87 [0, 0]])
88
89 self.A, self.B = self.ContinuousToDiscrete(
90 self.A_continuous, self.B_continuous, self.dt)
91
Brian Silvermane18cf502015-11-28 23:04:43 +000092 controllability = controls.ctrb(self.A, self.B)
Austin Schuh6c20f202017-02-18 22:31:44 -080093 glog.debug('Rank of augmented controllability matrix. %d', numpy.linalg.matrix_rank(
94 controllability))
Austin Schuh1a387962015-01-31 16:36:20 -080095
96 q_pos = 0.02
97 q_vel = 0.300
Austin Schuhbfb8b242015-02-16 15:45:22 -080098 q_pos_diff = 0.005
99 q_vel_diff = 0.13
Austin Schuh1a387962015-01-31 16:36:20 -0800100 self.Q = numpy.matrix([[(1.0 / (q_pos ** 2.0)), 0.0, 0.0, 0.0],
101 [0.0, (1.0 / (q_vel ** 2.0)), 0.0, 0.0],
102 [0.0, 0.0, (1.0 / (q_pos_diff ** 2.0)), 0.0],
103 [0.0, 0.0, 0.0, (1.0 / (q_vel_diff ** 2.0))]])
104
105 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0)), 0.0],
106 [0.0, 1.0 / (12.0 ** 2.0)]])
107 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
Austin Schuh6c20f202017-02-18 22:31:44 -0800108 glog.debug('Controller\n %s', repr(self.K))
Austin Schuh1a387962015-01-31 16:36:20 -0800109
Austin Schuh6c20f202017-02-18 22:31:44 -0800110 glog.debug('Controller Poles\n %s',
111 numpy.linalg.eig(self.A - self.B * self.K)[0])
Austin Schuh1a387962015-01-31 16:36:20 -0800112
113 self.rpl = 0.20
114 self.ipl = 0.05
115 self.PlaceObserverPoles([self.rpl + 1j * self.ipl,
116 self.rpl + 1j * self.ipl,
117 self.rpl - 1j * self.ipl,
118 self.rpl - 1j * self.ipl])
119
120 # The box formed by U_min and U_max must encompass all possible values,
121 # or else Austin's code gets angry.
122 self.U_max = numpy.matrix([[12.0], [12.0]])
123 self.U_min = numpy.matrix([[-12.0], [-12.0]])
124
Austin Schuh6c20f202017-02-18 22:31:44 -0800125 glog.debug('Observer (Converted to a KF):\n%s',
126 repr(numpy.linalg.inv(self.A) * self.L))
Austin Schuh8de10c72015-02-27 23:33:40 -0800127
128 self.InitializeState()
129
130
131class IntegralArm(Arm):
Austin Schuh6c20f202017-02-18 22:31:44 -0800132 def __init__(self, name='IntegralArm', mass=None):
Austin Schuh8de10c72015-02-27 23:33:40 -0800133 super(IntegralArm, self).__init__(name=name, mass=mass)
134
135 self.A_continuous_unaugmented = self.A_continuous
136 self.A_continuous = numpy.matrix(numpy.zeros((5, 5)))
137 self.A_continuous[0:4, 0:4] = self.A_continuous_unaugmented
138 self.A_continuous[1, 4] = self.C2
139
140 # Start with the unmodified input
141 self.B_continuous_unaugmented = self.B_continuous
142 self.B_continuous = numpy.matrix(numpy.zeros((5, 2)))
143 self.B_continuous[0:4, 0:2] = self.B_continuous_unaugmented
144
145 self.C_unaugmented = self.C
146 self.C = numpy.matrix(numpy.zeros((2, 5)))
147 self.C[0:2, 0:4] = self.C_unaugmented
148
149 self.A, self.B = self.ContinuousToDiscrete(
150 self.A_continuous, self.B_continuous, self.dt)
Austin Schuh6c20f202017-02-18 22:31:44 -0800151 glog.debug('A cont: %s', repr(self.A_continuous))
152 glog.debug('B cont %s', repr(self.B_continuous))
153 glog.debug('A discrete %s', repr(self.A))
Austin Schuh8de10c72015-02-27 23:33:40 -0800154
155 q_pos = 0.08
156 q_vel = 0.40
157
158 q_pos_diff = 0.08
159 q_vel_diff = 0.40
160 q_voltage = 6.0
161 self.Q = numpy.matrix([[(q_pos ** 2.0), 0.0, 0.0, 0.0, 0.0],
162 [0.0, (q_vel ** 2.0), 0.0, 0.0, 0.0],
163 [0.0, 0.0, (q_pos_diff ** 2.0), 0.0, 0.0],
164 [0.0, 0.0, 0.0, (q_vel_diff ** 2.0), 0.0],
165 [0.0, 0.0, 0.0, 0.0, (q_voltage ** 2.0)]])
166
167 r_volts = 0.05
168 self.R = numpy.matrix([[(r_volts ** 2.0), 0.0],
169 [0.0, (r_volts ** 2.0)]])
170
171 self.KalmanGain, self.Q_steady = controls.kalman(
172 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
173
174 self.U_max = numpy.matrix([[12.0], [12.0]])
175 self.U_min = numpy.matrix([[-12.0], [-12.0]])
176
177 self.K_unaugmented = self.K
Brian Silverman4e55e582015-11-10 14:16:37 -0500178 self.K = numpy.matrix(numpy.zeros((2, 5)))
Austin Schuh8de10c72015-02-27 23:33:40 -0800179 self.K[0:2, 0:4] = self.K_unaugmented
Brian Silverman4e55e582015-11-10 14:16:37 -0500180 self.K[0, 4] = 1
181 self.K[1, 4] = 1
Austin Schuh6c20f202017-02-18 22:31:44 -0800182 glog.debug('Kal: %s', repr(self.KalmanGain))
Austin Schuh8de10c72015-02-27 23:33:40 -0800183 self.L = self.A * self.KalmanGain
184
Austin Schuh1a387962015-01-31 16:36:20 -0800185 self.InitializeState()
186
187
188def CapU(U):
189 if U[0, 0] - U[1, 0] > 24:
190 return numpy.matrix([[12], [-12]])
191 elif U[0, 0] - U[1, 0] < -24:
192 return numpy.matrix([[-12], [12]])
193 else:
194 max_u = max(U[0, 0], U[1, 0])
195 min_u = min(U[0, 0], U[1, 0])
196 if max_u > 12:
197 return U - (max_u - 12)
198 if min_u < -12:
199 return U - (min_u + 12)
200 return U
201
202
203def run_test(arm, initial_X, goal, max_separation_error=0.01,
204 show_graph=True, iterations=200, controller_arm=None,
205 observer_arm=None):
206 """Runs the arm plant with an initial condition and goal.
207
208 The tests themselves are not terribly sophisticated; I just test for
209 whether the goal has been reached and whether the separation goes
210 outside of the initial and goal values by more than max_separation_error.
211 Prints out something for a failure of either condition and returns
212 False if tests fail.
213 Args:
214 arm: arm object to use.
215 initial_X: starting state.
216 goal: goal state.
217 show_graph: Whether or not to display a graph showing the changing
218 states and voltages.
219 iterations: Number of timesteps to run the model for.
220 controller_arm: arm object to get K from, or None if we should
221 use arm.
222 observer_arm: arm object to use for the observer, or None if we should
223 use the actual state.
224 """
225
226 arm.X = initial_X
227
228 if controller_arm is None:
229 controller_arm = arm
230
231 if observer_arm is not None:
232 observer_arm.X_hat = initial_X + 0.01
233 observer_arm.X_hat = initial_X
234
235 # Various lists for graphing things.
236 t = []
237 x_avg = []
238 x_sep = []
239 x_hat_avg = []
240 x_hat_sep = []
241 v_avg = []
242 v_sep = []
243 u_left = []
244 u_right = []
245
246 sep_plot_gain = 100.0
247
248 for i in xrange(iterations):
249 X_hat = arm.X
250 if observer_arm is not None:
251 X_hat = observer_arm.X_hat
252 x_hat_avg.append(observer_arm.X_hat[0, 0])
253 x_hat_sep.append(observer_arm.X_hat[2, 0] * sep_plot_gain)
254 U = controller_arm.K * (goal - X_hat)
255 U = CapU(U)
256 x_avg.append(arm.X[0, 0])
257 v_avg.append(arm.X[1, 0])
258 x_sep.append(arm.X[2, 0] * sep_plot_gain)
259 v_sep.append(arm.X[3, 0])
260 if observer_arm is not None:
261 observer_arm.PredictObserver(U)
262 arm.Update(U)
263 if observer_arm is not None:
264 observer_arm.Y = arm.Y
265 observer_arm.CorrectObserver(U)
266
267 t.append(i * arm.dt)
268 u_left.append(U[0, 0])
269 u_right.append(U[1, 0])
270
Austin Schuh6c20f202017-02-18 22:31:44 -0800271 glog.debug(repr(numpy.linalg.inv(arm.A)))
272 glog.debug('delta time is %f', arm.dt)
273 glog.debug('Velocity at t=0 is %f %f %f %f', x_avg[0], v_avg[0], x_sep[0], v_sep[0])
274 glog.debug('Velocity at t=1+dt is %f %f %f %f', x_avg[1], v_avg[1], x_sep[1], v_sep[1])
Austin Schuh1a387962015-01-31 16:36:20 -0800275
276 if show_graph:
277 pylab.subplot(2, 1, 1)
278 pylab.plot(t, x_avg, label='x avg')
279 pylab.plot(t, x_sep, label='x sep')
280 if observer_arm is not None:
281 pylab.plot(t, x_hat_avg, label='x_hat avg')
282 pylab.plot(t, x_hat_sep, label='x_hat sep')
283 pylab.legend()
284
285 pylab.subplot(2, 1, 2)
286 pylab.plot(t, u_left, label='u left')
287 pylab.plot(t, u_right, label='u right')
288 pylab.legend()
289 pylab.show()
290
291
Austin Schuh8de10c72015-02-27 23:33:40 -0800292def run_integral_test(arm, initial_X, goal, observer_arm, disturbance,
293 max_separation_error=0.01, show_graph=True,
294 iterations=400):
295 """Runs the integral control arm plant with an initial condition and goal.
296
297 The tests themselves are not terribly sophisticated; I just test for
298 whether the goal has been reached and whether the separation goes
299 outside of the initial and goal values by more than max_separation_error.
300 Prints out something for a failure of either condition and returns
301 False if tests fail.
302 Args:
303 arm: arm object to use.
304 initial_X: starting state.
305 goal: goal state.
306 observer_arm: arm object to use for the observer.
307 show_graph: Whether or not to display a graph showing the changing
308 states and voltages.
309 iterations: Number of timesteps to run the model for.
310 disturbance: Voltage missmatch between controller and model.
311 """
312
313 arm.X = initial_X
314
315 # Various lists for graphing things.
316 t = []
317 x_avg = []
318 x_sep = []
319 x_hat_avg = []
320 x_hat_sep = []
321 v_avg = []
322 v_sep = []
323 u_left = []
324 u_right = []
325 u_error = []
326
327 sep_plot_gain = 100.0
328
329 unaugmented_goal = goal
330 goal = numpy.matrix(numpy.zeros((5, 1)))
331 goal[0:4, 0] = unaugmented_goal
332
333 for i in xrange(iterations):
334 X_hat = observer_arm.X_hat[0:4]
335
336 x_hat_avg.append(observer_arm.X_hat[0, 0])
337 x_hat_sep.append(observer_arm.X_hat[2, 0] * sep_plot_gain)
338
339 U = observer_arm.K * (goal - observer_arm.X_hat)
340 u_error.append(observer_arm.X_hat[4,0])
341 U = CapU(U)
342 x_avg.append(arm.X[0, 0])
343 v_avg.append(arm.X[1, 0])
344 x_sep.append(arm.X[2, 0] * sep_plot_gain)
345 v_sep.append(arm.X[3, 0])
346
347 observer_arm.PredictObserver(U)
348
349 arm.Update(U + disturbance)
350 observer_arm.Y = arm.Y
351 observer_arm.CorrectObserver(U)
352
353 t.append(i * arm.dt)
354 u_left.append(U[0, 0])
355 u_right.append(U[1, 0])
356
Austin Schuh6c20f202017-02-18 22:31:44 -0800357 glog.debug('End is %f', observer_arm.X_hat[4, 0])
Austin Schuh8de10c72015-02-27 23:33:40 -0800358
359 if show_graph:
360 pylab.subplot(2, 1, 1)
361 pylab.plot(t, x_avg, label='x avg')
362 pylab.plot(t, x_sep, label='x sep')
363 if observer_arm is not None:
364 pylab.plot(t, x_hat_avg, label='x_hat avg')
365 pylab.plot(t, x_hat_sep, label='x_hat sep')
366 pylab.legend()
367
368 pylab.subplot(2, 1, 2)
369 pylab.plot(t, u_left, label='u left')
370 pylab.plot(t, u_right, label='u right')
371 pylab.plot(t, u_error, label='u error')
372 pylab.legend()
373 pylab.show()
374
375
Austin Schuh1a387962015-01-31 16:36:20 -0800376def main(argv):
Austin Schuh6c20f202017-02-18 22:31:44 -0800377 if FLAGS.plot:
378 loaded_mass = 25
379 #loaded_mass = 0
380 arm = Arm(mass=13 + loaded_mass)
381 #arm_controller = Arm(mass=13 + 15)
382 #observer_arm = Arm(mass=13 + 15)
383 #observer_arm = None
Austin Schuh1a387962015-01-31 16:36:20 -0800384
Austin Schuh6c20f202017-02-18 22:31:44 -0800385 integral_arm = IntegralArm(mass=13 + loaded_mass)
386 integral_arm.X_hat[0, 0] += 0.02
387 integral_arm.X_hat[2, 0] += 0.02
388 integral_arm.X_hat[4] = 0
Austin Schuh8de10c72015-02-27 23:33:40 -0800389
Austin Schuh6c20f202017-02-18 22:31:44 -0800390 # Test moving the arm with constant separation.
391 initial_X = numpy.matrix([[0.0], [0.0], [0.0], [0.0]])
392 R = numpy.matrix([[0.0], [0.0], [0.0], [0.0]])
393 run_integral_test(arm, initial_X, R, integral_arm, disturbance=2)
Austin Schuh1a387962015-01-31 16:36:20 -0800394
395 # Write the generated constants out to a file.
Austin Schuh8de10c72015-02-27 23:33:40 -0800396 if len(argv) != 5:
Austin Schuh6c20f202017-02-18 22:31:44 -0800397 glog.fatal('Expected .h file name and .cc file name for the arm and augmented arm.')
Austin Schuh1a387962015-01-31 16:36:20 -0800398 else:
Austin Schuh6c20f202017-02-18 22:31:44 -0800399 namespaces = ['y2015', 'control_loops', 'fridge']
400 arm = Arm('Arm', mass=13)
401 loop_writer = control_loop.ControlLoopWriter('Arm', [arm],
402 namespaces=namespaces)
403 loop_writer.Write(argv[1], argv[2])
Austin Schuh1a387962015-01-31 16:36:20 -0800404
Austin Schuh6c20f202017-02-18 22:31:44 -0800405 integral_arm = IntegralArm('IntegralArm', mass=13)
406 loop_writer = control_loop.ControlLoopWriter('IntegralArm', [integral_arm],
407 namespaces=namespaces)
408 loop_writer.Write(argv[3], argv[4])
Austin Schuh8de10c72015-02-27 23:33:40 -0800409
Austin Schuh1a387962015-01-31 16:36:20 -0800410if __name__ == '__main__':
Austin Schuh6c20f202017-02-18 22:31:44 -0800411 argv = FLAGS(sys.argv)
412 glog.init()
413 sys.exit(main(argv))