blob: ac3e4c1a5092bfb219468e221556376721ecc926 [file] [log] [blame]
Austin Schuhf173eb82018-01-20 23:32:30 -08001#!/usr/bin/python
Michael Schuh10dd1e02018-01-20 13:19:44 -08002
Austin Schuhf173eb82018-01-20 23:32:30 -08003from frc971.control_loops.python import control_loop
4from frc971.control_loops.python import controls
Michael Schuh10dd1e02018-01-20 13:19:44 -08005import numpy
Austin Schuhf173eb82018-01-20 23:32:30 -08006import sys
7import matplotlib
8from matplotlib import pylab
9import gflags
10import glog
Michael Schuh10dd1e02018-01-20 13:19:44 -080011
Austin Schuhf173eb82018-01-20 23:32:30 -080012FLAGS = gflags.FLAGS
Michael Schuh10dd1e02018-01-20 13:19:44 -080013
Austin Schuhf173eb82018-01-20 23:32:30 -080014try:
15 gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
16except gflags.DuplicateFlagError:
17 pass
Michael Schuh10dd1e02018-01-20 13:19:44 -080018
Austin Schuhf173eb82018-01-20 23:32:30 -080019class Intake(control_loop.ControlLoop):
20 def __init__(self, name="Intake"):
21 super(Intake, self).__init__(name)
22 self.motor = control_loop.BAG()
23 # TODO(constants): Update all of these & retune poles.
24 # Stall Torque in N m
25 self.stall_torque = self.motor.stall_torque
26 # Stall Current in Amps
27 self.stall_current = self.motor.stall_current
28 # Free Speed in RPM
29 self.free_speed = self.motor.free_speed
30 # Free Current in Amps
31 self.free_current = self.motor.free_current
Michael Schuh10dd1e02018-01-20 13:19:44 -080032
Austin Schuhf173eb82018-01-20 23:32:30 -080033 # Resistance of the motor
34 self.resistance = self.motor.resistance
35 # Motor velocity constant
36 self.Kv = self.motor.Kv
37 # Torque constant
38 self.Kt = self.motor.Kt
39 # Gear ratio
40 self.G = 1.0 / 100.0
Michael Schuh10dd1e02018-01-20 13:19:44 -080041
Austin Schuhf173eb82018-01-20 23:32:30 -080042 self.motor_inertia = 0.000006
Michael Schuh10dd1e02018-01-20 13:19:44 -080043
Austin Schuhf173eb82018-01-20 23:32:30 -080044 # Series elastic moment of inertia
45 self.Je = self.motor_inertia / (self.G * self.G)
46 # Grabber moment of inertia
47 self.Jo = 0.295
Michael Schuh10dd1e02018-01-20 13:19:44 -080048
Austin Schuhf173eb82018-01-20 23:32:30 -080049 # Spring constant (N m / radian)
50 self.Ks = 30.0
Michael Schuh10dd1e02018-01-20 13:19:44 -080051
Austin Schuhf173eb82018-01-20 23:32:30 -080052 # Control loop time step
53 self.dt = 0.00505
Michael Schuh10dd1e02018-01-20 13:19:44 -080054
Austin Schuhf173eb82018-01-20 23:32:30 -080055 # State is [output_position, output_velocity,
56 # elastic_position, elastic_velocity]
57 # The output position is the absolute position of the intake arm.
58 # The elastic position is the absolute position of the motor side of the
59 # series elastic.
60 # Input is [voltage]
Michael Schuh10dd1e02018-01-20 13:19:44 -080061
Austin Schuhf173eb82018-01-20 23:32:30 -080062 self.A_continuous = numpy.matrix(
63 [[0.0, 1.0, 0.0, 0.0],
64 [(-self.Ks / self.Jo), 0.0, (self.Ks / self.Jo), 0.0],
65 [0.0, 0.0, 0.0, 1.0],
66 [(self.Ks / self.Je), 0.0, (-self.Ks / self.Je), \
67 -self.Kt / (self.Je * self.resistance * self.Kv * self.G * self.G)]])
Michael Schuh10dd1e02018-01-20 13:19:44 -080068
Austin Schuhf173eb82018-01-20 23:32:30 -080069 # Start with the unmodified input
70 self.B_continuous = numpy.matrix(
71 [[0.0],
72 [0.0],
73 [0.0],
74 [self.Kt / (self.G * self.Je * self.resistance)]])
Michael Schuh10dd1e02018-01-20 13:19:44 -080075
Austin Schuhf173eb82018-01-20 23:32:30 -080076 self.C = numpy.matrix([[1.0, 0.0, -1.0, 0.0],
77 [0.0, 0.0, 1.0, 0.0]])
78 self.D = numpy.matrix([[0.0],
79 [0.0]])
Michael Schuh10dd1e02018-01-20 13:19:44 -080080
Austin Schuhf173eb82018-01-20 23:32:30 -080081 self.A, self.B = self.ContinuousToDiscrete(
82 self.A_continuous, self.B_continuous, self.dt)
Michael Schuh10dd1e02018-01-20 13:19:44 -080083
Austin Schuhf173eb82018-01-20 23:32:30 -080084 controllability = controls.ctrb(self.A, self.B)
85 glog.debug('ctrb: ' + repr(numpy.linalg.matrix_rank(controllability)))
Michael Schuh10dd1e02018-01-20 13:19:44 -080086
Austin Schuhf173eb82018-01-20 23:32:30 -080087 observability = controls.ctrb(self.A.T, self.C.T)
88 glog.debug('obs: ' + repr(numpy.linalg.matrix_rank(observability)))
Michael Schuh10dd1e02018-01-20 13:19:44 -080089
Austin Schuhf173eb82018-01-20 23:32:30 -080090 glog.debug('A_continuous ' + repr(self.A_continuous))
91 glog.debug('B_continuous ' + repr(self.B_continuous))
Michael Schuh10dd1e02018-01-20 13:19:44 -080092
Austin Schuhf173eb82018-01-20 23:32:30 -080093 self.K = numpy.matrix(numpy.zeros((1, 4)))
Michael Schuh10dd1e02018-01-20 13:19:44 -080094
Austin Schuhf173eb82018-01-20 23:32:30 -080095 q_pos = 0.05
96 q_vel = 2.65
97 self.Q = numpy.matrix(numpy.diag([(q_pos ** 2.0), (q_vel ** 2.0),
98 (q_pos ** 2.0), (q_vel ** 2.0)]))
99
100 r_nm = 0.025
101 self.R = numpy.matrix(numpy.diag([(r_nm ** 2.0), (r_nm ** 2.0)]))
102
103 self.KalmanGain, self.Q_steady = controls.kalman(
104 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
105
106 self.L = self.A * self.KalmanGain
107
108 # The box formed by U_min and U_max must encompass all possible values,
109 # or else Austin's code gets angry.
110 self.U_max = numpy.matrix([[12.0]])
111 self.U_min = numpy.matrix([[-12.0]])
112
113 self.Kff = controls.TwoStateFeedForwards(self.B, self.Q)
114
115 self.InitializeState()
116
117
118class DelayedIntake(Intake):
119 def __init__(self, name="DelayedIntake"):
120 super(DelayedIntake, self).__init__(name=name)
121
122 self.A_undelayed = self.A
123 self.B_undelayed = self.B
124
125 self.C_unaugmented = self.C
126 self.C = numpy.matrix(numpy.zeros((2, 5)))
127 self.C[0:2, 0:4] = self.C_unaugmented
128
129 # Model this as X[4] is the last power. And then B applies to the last
130 # power. This lets us model the 1 cycle PWM delay accurately.
131 self.A = numpy.matrix(numpy.zeros((5, 5)))
132 self.A[0:4, 0:4] = self.A_undelayed
133 self.A[0:4, 4] = self.B_undelayed
134 self.B = numpy.matrix(numpy.zeros((5, 1)))
135 self.B[4, 0] = 1.0
136
137 # Coordinate transform fom absolute angles to relative angles.
138 # [output_position, output_velocity, spring_angle, spring_velocity, voltage]
139 abs_to_rel = numpy.matrix([[ 1.0, 0.0, 0.0, 0.0, 0.0],
140 [ 0.0, 1.0, 0.0, 0.0, 0.0],
141 [-1.0, 0.0, 1.0, 0.0, 0.0],
142 [ 0.0, -1.0, 0.0, 1.0, 0.0],
143 [ 0.0, 0.0, 0.0, 0.0, 1.0]])
144 # and back again.
145 rel_to_abs = numpy.matrix(numpy.linalg.inv(abs_to_rel))
146
147 # Now, get A and B in the relative coordinate system.
148 self.A_transformed_full = abs_to_rel * self.A * rel_to_abs
149 self.B_transformed_full = abs_to_rel * self.B
150
151 # Pull out the components of the dynamics which don't include the spring
152 # output positoin so we can do partial state feedback on what we care about.
153 self.A_transformed = self.A_transformed_full[1:5, 1:5]
154 self.B_transformed = self.B_transformed_full[1:5, 0]
155
156 glog.debug('A_transformed_full ' + str(self.A_transformed_full))
157 glog.debug('B_transformed_full ' + str(self.B_transformed_full))
158 glog.debug('A_transformed ' + str(self.A_transformed))
159 glog.debug('B_transformed ' + str(self.B_transformed))
160
161 # Now, let's design a controller in
162 # [output_velocity, spring_position, spring_velocity, delayed_voltage]
163 # space.
164
165 q_output_vel = 0.20
166 q_spring_pos = 0.05
167 q_spring_vel = 3.0
168 q_voltage = 100.0
169 self.Q_lqr = numpy.matrix(numpy.diag(
170 [1.0 / (q_output_vel ** 2.0),
171 1.0 / (q_spring_pos ** 2.0),
172 1.0 / (q_spring_vel ** 2.0),
173 1.0 / (q_voltage ** 2.0)]))
174
175 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0))]])
176
177 self.K_transformed = controls.dlqr(self.A_transformed, self.B_transformed,
178 self.Q_lqr, self.R)
179
180 # Write the controller back out in the absolute coordinate system.
181 self.K = numpy.hstack((numpy.matrix([[0.0]]),
182 self.K_transformed)) * abs_to_rel
183
184 glog.debug('Poles are %s for %s',
185 repr(numpy.linalg.eig(
186 self.A_transformed -
187 self.B_transformed * self.K_transformed)[0]), self._name)
188 glog.debug('K is %s', repr(self.K_transformed))
189
190 # Design a kalman filter here as well.
191 q_pos = 0.05
192 q_vel = 2.65
193 q_volts = 0.005
194 self.Q = numpy.matrix(numpy.diag([(q_pos ** 2.0), (q_vel ** 2.0),
195 (q_pos ** 2.0), (q_vel ** 2.0),
196 (q_volts ** 2.0)]))
197
198 r_nm = 0.025
199 self.R = numpy.matrix(numpy.diag([(r_nm ** 2.0), (r_nm ** 2.0)]))
200
201 self.KalmanGain, self.Q_steady = controls.kalman(
202 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
203
204 self.L = self.A * self.KalmanGain
205
206 # The box formed by U_min and U_max must encompass all possible values,
207 # or else Austin's code gets angry.
208 self.U_max = numpy.matrix([[12.0]])
209 self.U_min = numpy.matrix([[-12.0]])
210
211 self.InitializeState()
212
213
214class ScenarioPlotter(object):
215 def __init__(self):
216 # Various lists for graphing things.
217 self.t = []
218 self.x = []
219 self.v = []
220 self.goal_v = []
221 self.a = []
222 self.spring = []
223 self.x_hat = []
224 self.u = []
225
226 def run_test(self, intake, iterations=400, controller_intake=None,
227 observer_intake=None):
228 """Runs the intake plant with an initial condition and goal.
229
230 Test for whether the goal has been reached and whether the separation
231 goes outside of the initial and goal values by more than
232 max_separation_error.
233
234 Prints out something for a failure of either condition and returns
235 False if tests fail.
236 Args:
237 intake: intake object to use.
238 iterations: Number of timesteps to run the model for.
239 controller_intake: Intake object to get K from, or None if we should
240 use intake.
241 observer_intake: Intake object to use for the observer, or None if we
242 should use the actual state.
243 """
244
245 if controller_intake is None:
246 controller_intake = intake
247
248 vbat = 12.0
249
250 if self.t:
251 initial_t = self.t[-1] + intake.dt
252 else:
253 initial_t = 0
254
255 # Delay U by 1 cycle in our simulation to make it more realistic.
256 last_U = numpy.matrix([[0.0]])
257
258 for i in xrange(iterations):
259 X_hat = intake.X
260
261 if observer_intake is not None:
262 X_hat = observer_intake.X_hat
263 self.x_hat.append(observer_intake.X_hat[0, 0])
264
265 goal_angle = 3.0
266 goal_velocity = numpy.clip((goal_angle - X_hat[0, 0]) * 6.0, -10.0, 10.0)
267
268 self.goal_v.append(goal_velocity)
269
270 # Nominal: 1.8 N at 0.25 m -> 0.45 N m
271 # Nominal: 13 N at 0.25 m at 0.5 radians -> 3.25 N m -> 6 N m / radian
272
273 R = numpy.matrix([[0.0],
274 [goal_velocity],
275 [0.0],
276 [goal_velocity],
277 [goal_velocity / (intake.G * intake.Kv)]])
278 U = controller_intake.K * (R - X_hat) + R[4, 0]
279
280 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
281
282 self.x.append(intake.X[0, 0])
283 self.spring.append((intake.X[2, 0] - intake.X[0, 0]) * intake.Ks)
284
285 if self.v:
286 last_v = self.v[-1]
287 else:
288 last_v = 0
289
290 self.v.append(intake.X[1, 0])
291 self.a.append((self.v[-1] - last_v) / intake.dt)
292
293 if observer_intake is not None:
294 observer_intake.Y = intake.Y
295 observer_intake.CorrectObserver(U)
296
297 intake.Update(last_U + 0.0)
298
299 if observer_intake is not None:
300 observer_intake.PredictObserver(U)
301
302 self.t.append(initial_t + i * intake.dt)
303 self.u.append(U[0, 0])
304 last_U = U
305
306 def Plot(self):
307 pylab.subplot(3, 1, 1)
308 pylab.plot(self.t, self.x, label='x')
309 pylab.plot(self.t, self.x_hat, label='x_hat')
310 pylab.legend()
311
312 spring_ax1 = pylab.subplot(3, 1, 2)
313 spring_ax1.plot(self.t, self.u, 'k', label='u')
314 spring_ax2 = spring_ax1.twinx()
315 spring_ax2.plot(self.t, self.spring, label='spring_angle')
316 spring_ax1.legend(loc=2)
317 spring_ax2.legend()
318
319 accel_ax1 = pylab.subplot(3, 1, 3)
320 accel_ax1.plot(self.t, self.a, 'r', label='a')
321
322 accel_ax2 = accel_ax1.twinx()
323 accel_ax2.plot(self.t, self.v, label='v')
324 accel_ax2.plot(self.t, self.goal_v, label='goal_v')
325 accel_ax1.legend(loc=2)
326 accel_ax2.legend()
327
328 pylab.show()
329
330
331def main(argv):
332 scenario_plotter = ScenarioPlotter()
333
334 intake = Intake()
335 intake_controller = DelayedIntake()
336 observer_intake = DelayedIntake()
337
338 # Test moving the intake with constant separation.
339 scenario_plotter.run_test(intake, controller_intake=intake_controller,
340 observer_intake=observer_intake, iterations=200)
341
342 if FLAGS.plot:
343 scenario_plotter.Plot()
344
345 # Write the generated constants out to a file.
Sabina Davis9d28ca22018-02-04 16:57:19 -0800346 if len(argv) != 3:
347 glog.fatal('Expected .h file name and .cc file name for the intake.')
Austin Schuhf173eb82018-01-20 23:32:30 -0800348 else:
349 namespaces = ['y2018', 'control_loops', 'superstructure']
350 intake = Intake('Intake')
351 loop_writer = control_loop.ControlLoopWriter(
352 'Intake', [intake], namespaces=namespaces)
353 loop_writer.Write(argv[1], argv[2])
354
Michael Schuh10dd1e02018-01-20 13:19:44 -0800355if __name__ == '__main__':
Austin Schuhf173eb82018-01-20 23:32:30 -0800356 argv = FLAGS(sys.argv)
357 glog.init()
358 sys.exit(main(argv))