blob: f29d3b511cb21d75bf9d1d5881fa5aed07f03b98 [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
Austin Schuhf173eb82018-01-20 23:32:30 -0800106 # The box formed by U_min and U_max must encompass all possible values,
107 # or else Austin's code gets angry.
108 self.U_max = numpy.matrix([[12.0]])
109 self.U_min = numpy.matrix([[-12.0]])
110
Austin Schuhf173eb82018-01-20 23:32:30 -0800111 self.InitializeState()
112
113
114class DelayedIntake(Intake):
115 def __init__(self, name="DelayedIntake"):
116 super(DelayedIntake, self).__init__(name=name)
117
118 self.A_undelayed = self.A
119 self.B_undelayed = self.B
120
121 self.C_unaugmented = self.C
122 self.C = numpy.matrix(numpy.zeros((2, 5)))
123 self.C[0:2, 0:4] = self.C_unaugmented
124
125 # Model this as X[4] is the last power. And then B applies to the last
126 # power. This lets us model the 1 cycle PWM delay accurately.
127 self.A = numpy.matrix(numpy.zeros((5, 5)))
128 self.A[0:4, 0:4] = self.A_undelayed
129 self.A[0:4, 4] = self.B_undelayed
130 self.B = numpy.matrix(numpy.zeros((5, 1)))
131 self.B[4, 0] = 1.0
132
133 # Coordinate transform fom absolute angles to relative angles.
134 # [output_position, output_velocity, spring_angle, spring_velocity, voltage]
135 abs_to_rel = numpy.matrix([[ 1.0, 0.0, 0.0, 0.0, 0.0],
136 [ 0.0, 1.0, 0.0, 0.0, 0.0],
137 [-1.0, 0.0, 1.0, 0.0, 0.0],
138 [ 0.0, -1.0, 0.0, 1.0, 0.0],
139 [ 0.0, 0.0, 0.0, 0.0, 1.0]])
140 # and back again.
141 rel_to_abs = numpy.matrix(numpy.linalg.inv(abs_to_rel))
142
143 # Now, get A and B in the relative coordinate system.
144 self.A_transformed_full = abs_to_rel * self.A * rel_to_abs
145 self.B_transformed_full = abs_to_rel * self.B
146
147 # Pull out the components of the dynamics which don't include the spring
148 # output positoin so we can do partial state feedback on what we care about.
149 self.A_transformed = self.A_transformed_full[1:5, 1:5]
150 self.B_transformed = self.B_transformed_full[1:5, 0]
151
152 glog.debug('A_transformed_full ' + str(self.A_transformed_full))
153 glog.debug('B_transformed_full ' + str(self.B_transformed_full))
154 glog.debug('A_transformed ' + str(self.A_transformed))
155 glog.debug('B_transformed ' + str(self.B_transformed))
156
157 # Now, let's design a controller in
158 # [output_velocity, spring_position, spring_velocity, delayed_voltage]
159 # space.
160
161 q_output_vel = 0.20
162 q_spring_pos = 0.05
163 q_spring_vel = 3.0
164 q_voltage = 100.0
165 self.Q_lqr = numpy.matrix(numpy.diag(
166 [1.0 / (q_output_vel ** 2.0),
167 1.0 / (q_spring_pos ** 2.0),
168 1.0 / (q_spring_vel ** 2.0),
169 1.0 / (q_voltage ** 2.0)]))
170
171 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0))]])
172
173 self.K_transformed = controls.dlqr(self.A_transformed, self.B_transformed,
174 self.Q_lqr, self.R)
175
176 # Write the controller back out in the absolute coordinate system.
177 self.K = numpy.hstack((numpy.matrix([[0.0]]),
178 self.K_transformed)) * abs_to_rel
179
180 glog.debug('Poles are %s for %s',
181 repr(numpy.linalg.eig(
182 self.A_transformed -
183 self.B_transformed * self.K_transformed)[0]), self._name)
184 glog.debug('K is %s', repr(self.K_transformed))
185
186 # Design a kalman filter here as well.
187 q_pos = 0.05
188 q_vel = 2.65
189 q_volts = 0.005
190 self.Q = numpy.matrix(numpy.diag([(q_pos ** 2.0), (q_vel ** 2.0),
191 (q_pos ** 2.0), (q_vel ** 2.0),
192 (q_volts ** 2.0)]))
193
194 r_nm = 0.025
195 self.R = numpy.matrix(numpy.diag([(r_nm ** 2.0), (r_nm ** 2.0)]))
196
197 self.KalmanGain, self.Q_steady = controls.kalman(
198 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
199
Austin Schuhf173eb82018-01-20 23:32:30 -0800200 # The box formed by U_min and U_max must encompass all possible values,
201 # or else Austin's code gets angry.
202 self.U_max = numpy.matrix([[12.0]])
203 self.U_min = numpy.matrix([[-12.0]])
204
205 self.InitializeState()
206
207
208class ScenarioPlotter(object):
209 def __init__(self):
210 # Various lists for graphing things.
211 self.t = []
212 self.x = []
213 self.v = []
214 self.goal_v = []
215 self.a = []
216 self.spring = []
217 self.x_hat = []
218 self.u = []
219
220 def run_test(self, intake, iterations=400, controller_intake=None,
221 observer_intake=None):
222 """Runs the intake plant with an initial condition and goal.
223
224 Test for whether the goal has been reached and whether the separation
225 goes outside of the initial and goal values by more than
226 max_separation_error.
227
228 Prints out something for a failure of either condition and returns
229 False if tests fail.
230 Args:
231 intake: intake object to use.
232 iterations: Number of timesteps to run the model for.
233 controller_intake: Intake object to get K from, or None if we should
234 use intake.
235 observer_intake: Intake object to use for the observer, or None if we
236 should use the actual state.
237 """
238
239 if controller_intake is None:
240 controller_intake = intake
241
242 vbat = 12.0
243
244 if self.t:
245 initial_t = self.t[-1] + intake.dt
246 else:
247 initial_t = 0
248
249 # Delay U by 1 cycle in our simulation to make it more realistic.
250 last_U = numpy.matrix([[0.0]])
251
252 for i in xrange(iterations):
253 X_hat = intake.X
254
255 if observer_intake is not None:
256 X_hat = observer_intake.X_hat
257 self.x_hat.append(observer_intake.X_hat[0, 0])
258
259 goal_angle = 3.0
260 goal_velocity = numpy.clip((goal_angle - X_hat[0, 0]) * 6.0, -10.0, 10.0)
261
262 self.goal_v.append(goal_velocity)
263
264 # Nominal: 1.8 N at 0.25 m -> 0.45 N m
265 # Nominal: 13 N at 0.25 m at 0.5 radians -> 3.25 N m -> 6 N m / radian
266
267 R = numpy.matrix([[0.0],
268 [goal_velocity],
269 [0.0],
270 [goal_velocity],
271 [goal_velocity / (intake.G * intake.Kv)]])
272 U = controller_intake.K * (R - X_hat) + R[4, 0]
273
274 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
275
276 self.x.append(intake.X[0, 0])
277 self.spring.append((intake.X[2, 0] - intake.X[0, 0]) * intake.Ks)
278
279 if self.v:
280 last_v = self.v[-1]
281 else:
282 last_v = 0
283
284 self.v.append(intake.X[1, 0])
285 self.a.append((self.v[-1] - last_v) / intake.dt)
286
287 if observer_intake is not None:
288 observer_intake.Y = intake.Y
289 observer_intake.CorrectObserver(U)
290
291 intake.Update(last_U + 0.0)
292
293 if observer_intake is not None:
294 observer_intake.PredictObserver(U)
295
296 self.t.append(initial_t + i * intake.dt)
297 self.u.append(U[0, 0])
298 last_U = U
299
300 def Plot(self):
301 pylab.subplot(3, 1, 1)
302 pylab.plot(self.t, self.x, label='x')
303 pylab.plot(self.t, self.x_hat, label='x_hat')
304 pylab.legend()
305
306 spring_ax1 = pylab.subplot(3, 1, 2)
307 spring_ax1.plot(self.t, self.u, 'k', label='u')
308 spring_ax2 = spring_ax1.twinx()
309 spring_ax2.plot(self.t, self.spring, label='spring_angle')
310 spring_ax1.legend(loc=2)
311 spring_ax2.legend()
312
313 accel_ax1 = pylab.subplot(3, 1, 3)
314 accel_ax1.plot(self.t, self.a, 'r', label='a')
315
316 accel_ax2 = accel_ax1.twinx()
317 accel_ax2.plot(self.t, self.v, label='v')
318 accel_ax2.plot(self.t, self.goal_v, label='goal_v')
319 accel_ax1.legend(loc=2)
320 accel_ax2.legend()
321
322 pylab.show()
323
324
325def main(argv):
326 scenario_plotter = ScenarioPlotter()
327
328 intake = Intake()
329 intake_controller = DelayedIntake()
330 observer_intake = DelayedIntake()
331
332 # Test moving the intake with constant separation.
333 scenario_plotter.run_test(intake, controller_intake=intake_controller,
334 observer_intake=observer_intake, iterations=200)
335
336 if FLAGS.plot:
337 scenario_plotter.Plot()
338
339 # Write the generated constants out to a file.
Sabina Davis3922dfa2018-02-10 23:10:05 -0800340 if len(argv) != 5:
341 glog.fatal('Expected .h file name and .cc file name for intake and delayed_intake.')
Austin Schuhf173eb82018-01-20 23:32:30 -0800342 else:
Sabina Davis3922dfa2018-02-10 23:10:05 -0800343 namespaces = ['y2018', 'control_loops', 'superstructure', 'intake']
Austin Schuhf173eb82018-01-20 23:32:30 -0800344 intake = Intake('Intake')
345 loop_writer = control_loop.ControlLoopWriter(
346 'Intake', [intake], namespaces=namespaces)
347 loop_writer.Write(argv[1], argv[2])
348
Sabina Davis3922dfa2018-02-10 23:10:05 -0800349 delayed_intake = DelayedIntake('DelayedIntake')
350 loop_writer = control_loop.ControlLoopWriter(
351 'DelayedIntake', [delayed_intake], namespaces=namespaces)
352 loop_writer.Write(argv[3], argv[4])
353
Michael Schuh10dd1e02018-01-20 13:19:44 -0800354if __name__ == '__main__':
Austin Schuhf173eb82018-01-20 23:32:30 -0800355 argv = FLAGS(sys.argv)
356 glog.init()
357 sys.exit(main(argv))