blob: 5665fd9412a4afa630fd9b944b553611a81b0fb5 [file] [log] [blame]
Campbell Crowley33e0e3d2017-12-27 17:55:40 -08001#!/usr/bin/python
2
3from frc971.control_loops.python import control_loop
4from frc971.control_loops.python import controls
5import numpy
6import sys
7from matplotlib import pylab
8import glog
9
10class DrivetrainParams(object):
11 def __init__(self, J, mass, robot_radius, wheel_radius, G_high, G_low,
12 q_pos_low, q_pos_high, q_vel_low, q_vel_high,
13 motor_type = control_loop.CIM(), num_motors = 2, dt = 0.00505,
14 controller_poles=[0.90, 0.90], observer_poles=[0.02, 0.02]):
15 """Defines all constants of a drivetrain.
16
17 Args:
18 J: float, Moment of inertia of drivetrain in kg m^2
19 mass: float, Mass of the robot in kg.
20 robot_radius: float, Radius of the robot, in meters (requires tuning by
21 hand).
22 wheel_radius: float, Radius of the wheels, in meters.
23 G_high: float, Gear ratio for high gear.
24 G_low: float, Gear ratio for low gear.
25 dt: float, Control loop time step.
26 q_pos_low: float, q position low gear.
27 q_pos_high: float, q position high gear.
28 q_vel_low: float, q velocity low gear.
29 q_vel_high: float, q velocity high gear.
30 motor_type: object, class of values defining the motor in drivetrain.
31 num_motors: int, number of motors on one side of drivetrain.
32 controller_poles: array, An array of poles. (See control_loop.py)
33 observer_poles: array, An array of poles. (See control_loop.py)
34 """
35
36 self.J = J
37 self.mass = mass
38 self.robot_radius = robot_radius
39 self.wheel_radius = wheel_radius
40 self.G_high = G_high
41 self.G_low = G_low
42 self.dt = dt
43 self.q_pos_low = q_pos_low
44 self.q_pos_high = q_pos_high
45 self.q_vel_low = q_vel_low
46 self.q_vel_high = q_vel_high
47 self.motor_type = motor_type
48 self.num_motors = num_motors
49 self.controller_poles = controller_poles
50 self.observer_poles = observer_poles
51
52class Drivetrain(control_loop.ControlLoop):
53 def __init__(self, drivetrain_params, name="Drivetrain", left_low=True,
54 right_low=True):
55 """Defines a base drivetrain for a robot.
56
57 Args:
58 drivetrain_params: DrivetrainParams, class of values defining the drivetrain.
59 name: string, Name of this drivetrain.
60 left_low: bool, Whether the left is in high gear.
61 right_low: bool, Whether the right is in high gear.
62 """
63 super(Drivetrain, self).__init__(name)
64
65 # Moment of inertia of the drivetrain in kg m^2
66 self.J = drivetrain_params.J
67 # Mass of the robot, in kg.
68 self.mass = drivetrain_params.mass
69 # Radius of the robot, in meters (requires tuning by hand)
70 self.robot_radius = drivetrain_params.robot_radius
71 # Radius of the wheels, in meters.
72 self.r = drivetrain_params.wheel_radius
73
74 # Gear ratios
75 self.G_low = drivetrain_params.G_low
76 self.G_high = drivetrain_params.G_high
77 if left_low:
78 self.Gl = self.G_low
79 else:
80 self.Gl = self.G_high
81 if right_low:
82 self.Gr = self.G_low
83 else:
84 self.Gr = self.G_high
85
86 # Control loop time step
87 self.dt = drivetrain_params.dt
88
89 self.BuildDrivetrain(drivetrain_params.motor_type, drivetrain_params.num_motors);
90
91 if left_low or right_low:
92 q_pos = drivetrain_params.q_pos_low
93 q_vel = drivetrain_params.q_vel_low
94 else:
95 q_pos = drivetrain_params.q_pos_high
96 q_vel = drivetrain_params.q_vel_high
97
98 self.BuildDrivetrainController(q_pos, q_vel)
99
100 self.InitializeState()
101
102 def BuildDrivetrain(self, motor, num_motors_per_side):
103 self.motor = motor
104 # Number of motors per side
105 self.num_motors = num_motors_per_side
106 # Stall Torque in N m
107 self.stall_torque = motor.stall_torque * self.num_motors * 0.60
108 # Stall Current in Amps
109 self.stall_current = motor.stall_current * self.num_motors
110 # Free Speed in rad/s
111 self.free_speed = motor.free_speed
112 # Free Current in Amps
113 self.free_current = motor.free_current * self.num_motors
114
115 # Effective motor resistance in ohms.
116 self.resistance = 12.0 / self.stall_current
117
118 # Resistance of the motor, divided by the number of motors.
119 # Motor velocity constant
120 self.Kv = (self.free_speed / (12.0 - self.resistance * self.free_current))
121 # Torque constant
122 self.Kt = self.stall_torque / self.stall_current
123
124 # These describe the way that a given side of a robot will be influenced
125 # by the other side. Units of 1 / kg.
126 self.msp = 1.0 / self.mass + self.robot_radius * self.robot_radius / self.J
127 self.msn = 1.0 / self.mass - self.robot_radius * self.robot_radius / self.J
128 # The calculations which we will need for A and B.
129 self.tcl = self.Kt / self.Kv / (self.Gl * self.Gl * self.resistance * self.r * self.r)
130 self.tcr = self.Kt / self.Kv / (self.Gr * self.Gr * self.resistance * self.r * self.r)
131 self.mpl = self.Kt / (self.Gl * self.resistance * self.r)
132 self.mpr = self.Kt / (self.Gr * self.resistance * self.r)
133
134 # State feedback matrices
135 # X will be of the format
136 # [[positionl], [velocityl], [positionr], velocityr]]
137 self.A_continuous = numpy.matrix(
138 [[0, 1, 0, 0],
139 [0, -self.msp * self.tcl, 0, -self.msn * self.tcr],
140 [0, 0, 0, 1],
141 [0, -self.msn * self.tcl, 0, -self.msp * self.tcr]])
142 self.B_continuous = numpy.matrix(
143 [[0, 0],
144 [self.msp * self.mpl, self.msn * self.mpr],
145 [0, 0],
146 [self.msn * self.mpl, self.msp * self.mpr]])
147 self.C = numpy.matrix([[1, 0, 0, 0],
148 [0, 0, 1, 0]])
149 self.D = numpy.matrix([[0, 0],
150 [0, 0]])
151
152 self.A, self.B = self.ContinuousToDiscrete(
153 self.A_continuous, self.B_continuous, self.dt)
154
155 def BuildDrivetrainController(self, q_pos, q_vel):
156 # Tune the LQR controller
157 self.Q = numpy.matrix([[(1.0 / (q_pos ** 2.0)), 0.0, 0.0, 0.0],
158 [0.0, (1.0 / (q_vel ** 2.0)), 0.0, 0.0],
159 [0.0, 0.0, (1.0 / (q_pos ** 2.0)), 0.0],
160 [0.0, 0.0, 0.0, (1.0 / (q_vel ** 2.0))]])
161
162 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0)), 0.0],
163 [0.0, (1.0 / (12.0 ** 2.0))]])
164 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
165
166 glog.debug('DT q_pos %f q_vel %s %s', q_pos, q_vel, self._name)
167 glog.debug(str(numpy.linalg.eig(self.A - self.B * self.K)[0]))
168 glog.debug('K %s', repr(self.K))
169
170 self.hlp = 0.3
171 self.llp = 0.4
172 self.PlaceObserverPoles([self.hlp, self.hlp, self.llp, self.llp])
173
174 self.U_max = numpy.matrix([[12.0], [12.0]])
175 self.U_min = numpy.matrix([[-12.0], [-12.0]])
176
177class KFDrivetrain(Drivetrain):
178 def __init__(self, drivetrain_params, name="KFDrivetrain",
179 left_low=True, right_low=True):
180 """Kalman filter values of a drivetrain.
181
182 Args:
183 drivetrain_params: DrivetrainParams, class of values defining the drivetrain.
184 name: string, Name of this drivetrain.
185 left_low: bool, Whether the left is in high gear.
186 right_low: bool, Whether the right is in high gear.
187 """
188 super(KFDrivetrain, self).__init__(drivetrain_params, name, left_low, right_low)
189
190 self.unaugmented_A_continuous = self.A_continuous
191 self.unaugmented_B_continuous = self.B_continuous
192
193 # The practical voltage applied to the wheels is
194 # V_left = U_left + left_voltage_error
195 #
196 # The states are
197 # [left position, left velocity, right position, right velocity,
198 # left voltage error, right voltage error, angular_error]
199 #
200 # The left and right positions are filtered encoder positions and are not
201 # adjusted for heading error.
202 # The turn velocity as computed by the left and right velocities is
203 # adjusted by the gyro velocity.
204 # The angular_error is the angular velocity error between the wheel speed
205 # and the gyro speed.
206 self.A_continuous = numpy.matrix(numpy.zeros((7, 7)))
207 self.B_continuous = numpy.matrix(numpy.zeros((7, 2)))
208 self.A_continuous[0:4,0:4] = self.unaugmented_A_continuous
209 self.A_continuous[0:4,4:6] = self.unaugmented_B_continuous
210 self.B_continuous[0:4,0:2] = self.unaugmented_B_continuous
211 self.A_continuous[0,6] = 1
212 self.A_continuous[2,6] = -1
213
214 self.A, self.B = self.ContinuousToDiscrete(
215 self.A_continuous, self.B_continuous, self.dt)
216
217 self.C = numpy.matrix([[1, 0, 0, 0, 0, 0, 0],
218 [0, 0, 1, 0, 0, 0, 0],
219 [0, -0.5 / drivetrain_params.robot_radius, 0, 0.5 / drivetrain_params.robot_radius, 0, 0, 0]])
220
221 self.D = numpy.matrix([[0, 0],
222 [0, 0],
223 [0, 0]])
224
225 q_pos = 0.05
226 q_vel = 1.00
227 q_voltage = 10.0
228 q_encoder_uncertainty = 2.00
229
230 self.Q = numpy.matrix([[(q_pos ** 2.0), 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
231 [0.0, (q_vel ** 2.0), 0.0, 0.0, 0.0, 0.0, 0.0],
232 [0.0, 0.0, (q_pos ** 2.0), 0.0, 0.0, 0.0, 0.0],
233 [0.0, 0.0, 0.0, (q_vel ** 2.0), 0.0, 0.0, 0.0],
234 [0.0, 0.0, 0.0, 0.0, (q_voltage ** 2.0), 0.0, 0.0],
235 [0.0, 0.0, 0.0, 0.0, 0.0, (q_voltage ** 2.0), 0.0],
236 [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, (q_encoder_uncertainty ** 2.0)]])
237
238 r_pos = 0.0001
239 r_gyro = 0.000001
240 self.R = numpy.matrix([[(r_pos ** 2.0), 0.0, 0.0],
241 [0.0, (r_pos ** 2.0), 0.0],
242 [0.0, 0.0, (r_gyro ** 2.0)]])
243
244 # Solving for kf gains.
245 self.KalmanGain, self.Q_steady = controls.kalman(
246 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
247
248 self.L = self.A * self.KalmanGain
249
250 unaug_K = self.K
251
252 # Implement a nice closed loop controller for use by the closed loop
253 # controller.
254 self.K = numpy.matrix(numpy.zeros((self.B.shape[1], self.A.shape[0])))
255 self.K[0:2, 0:4] = unaug_K
256 self.K[0, 4] = 1.0
257 self.K[1, 5] = 1.0
258
259 self.Qff = numpy.matrix(numpy.zeros((4, 4)))
260 qff_pos = 0.005
261 qff_vel = 1.00
262 self.Qff[0, 0] = 1.0 / qff_pos ** 2.0
263 self.Qff[1, 1] = 1.0 / qff_vel ** 2.0
264 self.Qff[2, 2] = 1.0 / qff_pos ** 2.0
265 self.Qff[3, 3] = 1.0 / qff_vel ** 2.0
266 self.Kff = numpy.matrix(numpy.zeros((2, 7)))
267 self.Kff[0:2, 0:4] = controls.TwoStateFeedForwards(self.B[0:4,:], self.Qff)
268
269 self.InitializeState()
270
271
272def WriteDrivetrain(drivetrain_files, kf_drivetrain_files, year_namespace,
273 drivetrain_params):
274
275 # Write the generated constants out to a file.
276 drivetrain_low_low = Drivetrain(name="DrivetrainLowLow",
277 left_low=True, right_low=True, drivetrain_params=drivetrain_params)
278 drivetrain_low_high = Drivetrain(name="DrivetrainLowHigh",
279 left_low=True, right_low=False, drivetrain_params=drivetrain_params)
280 drivetrain_high_low = Drivetrain(name="DrivetrainHighLow",
281 left_low=False, right_low=True, drivetrain_params=drivetrain_params)
282 drivetrain_high_high = Drivetrain(name="DrivetrainHighHigh",
283 left_low=False, right_low=False, drivetrain_params=drivetrain_params)
284
285 kf_drivetrain_low_low = KFDrivetrain(name="KFDrivetrainLowLow",
286 left_low=True, right_low=True, drivetrain_params=drivetrain_params)
287 kf_drivetrain_low_high = KFDrivetrain(name="KFDrivetrainLowHigh",
288 left_low=True, right_low=False, drivetrain_params=drivetrain_params)
289 kf_drivetrain_high_low = KFDrivetrain(name="KFDrivetrainHighLow",
290 left_low=False, right_low=True, drivetrain_params=drivetrain_params)
291 kf_drivetrain_high_high = KFDrivetrain(name="KFDrivetrainHighHigh",
292 left_low=False, right_low=False, drivetrain_params=drivetrain_params)
293
294 namespaces = [year_namespace, 'control_loops', 'drivetrain']
295 dog_loop_writer = control_loop.ControlLoopWriter(
296 "Drivetrain", [drivetrain_low_low, drivetrain_low_high,
297 drivetrain_high_low, drivetrain_high_high],
298 namespaces = namespaces)
299 dog_loop_writer.AddConstant(control_loop.Constant("kDt", "%f",
300 drivetrain_low_low.dt))
301 dog_loop_writer.AddConstant(control_loop.Constant("kStallTorque", "%f",
302 drivetrain_low_low.stall_torque))
303 dog_loop_writer.AddConstant(control_loop.Constant("kStallCurrent", "%f",
304 drivetrain_low_low.stall_current))
305 dog_loop_writer.AddConstant(control_loop.Constant("kFreeSpeed", "%f",
306 drivetrain_low_low.free_speed))
307 dog_loop_writer.AddConstant(control_loop.Constant("kFreeCurrent", "%f",
308 drivetrain_low_low.free_current))
309 dog_loop_writer.AddConstant(control_loop.Constant("kJ", "%f",
310 drivetrain_low_low.J))
311 dog_loop_writer.AddConstant(control_loop.Constant("kMass", "%f",
312 drivetrain_low_low.mass))
313 dog_loop_writer.AddConstant(control_loop.Constant("kRobotRadius", "%f",
314 drivetrain_low_low.robot_radius))
315 dog_loop_writer.AddConstant(control_loop.Constant("kWheelRadius", "%f",
316 drivetrain_low_low.r))
317 dog_loop_writer.AddConstant(control_loop.Constant("kR", "%f",
318 drivetrain_low_low.resistance))
319 dog_loop_writer.AddConstant(control_loop.Constant("kV", "%f",
320 drivetrain_low_low.Kv))
321 dog_loop_writer.AddConstant(control_loop.Constant("kT", "%f",
322 drivetrain_low_low.Kt))
323 dog_loop_writer.AddConstant(control_loop.Constant("kLowGearRatio", "%f",
324 drivetrain_low_low.G_low))
325 dog_loop_writer.AddConstant(control_loop.Constant("kHighGearRatio", "%f",
326 drivetrain_high_high.G_high))
327 dog_loop_writer.AddConstant(control_loop.Constant("kHighOutputRatio", "%f",
328 drivetrain_high_high.G_high * drivetrain_high_high.r))
329
330 dog_loop_writer.Write(drivetrain_files[0], drivetrain_files[1])
331
332 kf_loop_writer = control_loop.ControlLoopWriter(
333 "KFDrivetrain", [kf_drivetrain_low_low, kf_drivetrain_low_high,
334 kf_drivetrain_high_low, kf_drivetrain_high_high],
335 namespaces = namespaces)
336 kf_loop_writer.Write(kf_drivetrain_files[0], kf_drivetrain_files[1])
337
338def PlotDrivetrainMotions(drivetrain_params):
339 # Simulate the response of the system to a step input.
340 drivetrain = Drivetrain(left_low=False, right_low=False, drivetrain_params=drivetrain_params)
341 simulated_left = []
342 simulated_right = []
343 for _ in xrange(100):
344 drivetrain.Update(numpy.matrix([[12.0], [12.0]]))
345 simulated_left.append(drivetrain.X[0, 0])
346 simulated_right.append(drivetrain.X[2, 0])
347
348 pylab.rc('lines', linewidth=4)
349 pylab.plot(range(100), simulated_left, label='left position')
350 pylab.plot(range(100), simulated_right, 'r--', label='right position')
351 pylab.suptitle('Acceleration Test\n12 Volt Step Input')
352 pylab.legend(loc='lower right')
353 pylab.show()
354
355 # Simulate forwards motion.
356 drivetrain = Drivetrain(left_low=False, right_low=False, drivetrain_params=drivetrain_params)
357 close_loop_left = []
358 close_loop_right = []
359 left_power = []
360 right_power = []
361 R = numpy.matrix([[1.0], [0.0], [1.0], [0.0]])
362 for _ in xrange(300):
363 U = numpy.clip(drivetrain.K * (R - drivetrain.X_hat),
364 drivetrain.U_min, drivetrain.U_max)
365 drivetrain.UpdateObserver(U)
366 drivetrain.Update(U)
367 close_loop_left.append(drivetrain.X[0, 0])
368 close_loop_right.append(drivetrain.X[2, 0])
369 left_power.append(U[0, 0])
370 right_power.append(U[1, 0])
371
372 pylab.plot(range(300), close_loop_left, label='left position')
373 pylab.plot(range(300), close_loop_right, 'm--', label='right position')
374 pylab.plot(range(300), left_power, label='left power')
375 pylab.plot(range(300), right_power, '--', label='right power')
376 pylab.suptitle('Linear Move\nLeft and Right Position going to 1')
377 pylab.legend()
378 pylab.show()
379
380 # Try turning in place
381 drivetrain = Drivetrain(drivetrain_params=drivetrain_params)
382 close_loop_left = []
383 close_loop_right = []
384 R = numpy.matrix([[-1.0], [0.0], [1.0], [0.0]])
385 for _ in xrange(200):
386 U = numpy.clip(drivetrain.K * (R - drivetrain.X_hat),
387 drivetrain.U_min, drivetrain.U_max)
388 drivetrain.UpdateObserver(U)
389 drivetrain.Update(U)
390 close_loop_left.append(drivetrain.X[0, 0])
391 close_loop_right.append(drivetrain.X[2, 0])
392
393 pylab.plot(range(200), close_loop_left, label='left position')
394 pylab.plot(range(200), close_loop_right, label='right position')
395 pylab.suptitle('Angular Move\nLeft position going to -1 and right position going to 1')
396 pylab.legend(loc='center right')
397 pylab.show()
398
399 # Try turning just one side.
400 drivetrain = Drivetrain(drivetrain_params=drivetrain_params)
401 close_loop_left = []
402 close_loop_right = []
403 R = numpy.matrix([[0.0], [0.0], [1.0], [0.0]])
404 for _ in xrange(300):
405 U = numpy.clip(drivetrain.K * (R - drivetrain.X_hat),
406 drivetrain.U_min, drivetrain.U_max)
407 drivetrain.UpdateObserver(U)
408 drivetrain.Update(U)
409 close_loop_left.append(drivetrain.X[0, 0])
410 close_loop_right.append(drivetrain.X[2, 0])
411
412 pylab.plot(range(300), close_loop_left, label='left position')
413 pylab.plot(range(300), close_loop_right, label='right position')
414 pylab.suptitle('Pivot\nLeft position not changing and right position going to 1')
415 pylab.legend(loc='center right')
416 pylab.show()