blob: 1f8bd76b8541620a5c9d4a1f119c34be03cd35df [file] [log] [blame]
Austin Schuh82a66dc2017-03-04 15:06:44 -08001#!/usr/bin/python
2
John Park33858a32018-09-28 23:05:48 -07003from aos.util.trapezoid_profile import TrapezoidProfile
Austin Schuh82a66dc2017-03-04 15:06:44 -08004from frc971.control_loops.python import control_loop
5from frc971.control_loops.python import controls
6from y2017.control_loops.python import turret
7from y2017.control_loops.python import indexer
8import numpy
9import sys
Austin Schuh82a66dc2017-03-04 15:06:44 -080010from matplotlib import pylab
11import gflags
12import glog
13
14FLAGS = gflags.FLAGS
15
16try:
17 gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
18except gflags.DuplicateFlagError:
19 pass
20
21
Austin Schuh82a66dc2017-03-04 15:06:44 -080022class ColumnController(control_loop.ControlLoop):
23 def __init__(self, name='Column'):
24 super(ColumnController, self).__init__(name)
25 self.turret = turret.Turret(name + 'Turret')
26 self.indexer = indexer.Indexer(name + 'Indexer')
27
28 # Control loop time step
29 self.dt = 0.005
30
31 # State is [position_indexer,
32 # velocity_indexer,
33 # position_shooter,
34 # velocity_shooter]
35 # Input is [volts_indexer, volts_shooter]
36 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
37 self.B_continuous = numpy.matrix(numpy.zeros((3, 2)))
38
Austin Schuhd5ccb862017-03-11 22:06:36 -080039 self.A_continuous[0, 0] = -(self.indexer.Kt / self.indexer.Kv / (self.indexer.J * self.indexer.resistance * self.indexer.G * self.indexer.G) +
Austin Schuh82a66dc2017-03-04 15:06:44 -080040 self.turret.Kt / self.turret.Kv / (self.indexer.J * self.turret.resistance * self.turret.G * self.turret.G))
Austin Schuhd5ccb862017-03-11 22:06:36 -080041 self.A_continuous[0, 2] = self.turret.Kt / self.turret.Kv / (self.indexer.J * self.turret.resistance * self.turret.G * self.turret.G)
42 self.B_continuous[0, 0] = self.indexer.Kt / (self.indexer.J * self.indexer.resistance * self.indexer.G)
43 self.B_continuous[0, 1] = -self.turret.Kt / (self.indexer.J * self.turret.resistance * self.turret.G)
Austin Schuh82a66dc2017-03-04 15:06:44 -080044
Austin Schuhd5ccb862017-03-11 22:06:36 -080045 self.A_continuous[1, 2] = 1
Austin Schuh82a66dc2017-03-04 15:06:44 -080046
Austin Schuhd5ccb862017-03-11 22:06:36 -080047 self.A_continuous[2, 0] = self.turret.Kt / self.turret.Kv / (self.turret.J * self.turret.resistance * self.turret.G * self.turret.G)
48 self.A_continuous[2, 2] = -self.turret.Kt / self.turret.Kv / (self.turret.J * self.turret.resistance * self.turret.G * self.turret.G)
Austin Schuh82a66dc2017-03-04 15:06:44 -080049
Austin Schuhd5ccb862017-03-11 22:06:36 -080050 self.B_continuous[2, 1] = self.turret.Kt / (self.turret.J * self.turret.resistance * self.turret.G)
Austin Schuh82a66dc2017-03-04 15:06:44 -080051
52 self.C = numpy.matrix([[1, 0, 0], [0, 1, 0]])
53 self.D = numpy.matrix([[0, 0], [0, 0]])
54
55 self.A, self.B = self.ContinuousToDiscrete(
56 self.A_continuous, self.B_continuous, self.dt)
57
Austin Schuhd5ccb862017-03-11 22:06:36 -080058 q_indexer_vel = 13.0
Austin Schuheb5c22e2017-04-09 18:30:28 -070059 q_pos = 0.05
Austin Schuhd5ccb862017-03-11 22:06:36 -080060 q_vel = 0.8
61 self.Q = numpy.matrix([[(1.0 / (q_indexer_vel ** 2.0)), 0.0, 0.0],
Austin Schuh82a66dc2017-03-04 15:06:44 -080062 [0.0, (1.0 / (q_pos ** 2.0)), 0.0],
63 [0.0, 0.0, (1.0 / (q_vel ** 2.0))]])
64
65 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0)), 0.0],
66 [0.0, (1.0 / (12.0 ** 2.0))]])
67 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
68
Austin Schuhd5ccb862017-03-11 22:06:36 -080069 glog.debug('Controller poles are ' + repr(numpy.linalg.eig(self.A - self.B * self.K)[0]))
70
Austin Schuh82a66dc2017-03-04 15:06:44 -080071 q_vel_indexer_ff = 0.000005
72 q_pos_ff = 0.0000005
73 q_vel_ff = 0.00008
74 self.Qff = numpy.matrix([[(1.0 / (q_vel_indexer_ff ** 2.0)), 0.0, 0.0],
75 [0.0, (1.0 / (q_pos_ff ** 2.0)), 0.0],
76 [0.0, 0.0, (1.0 / (q_vel_ff ** 2.0))]])
77
78 self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
79
80 self.U_max = numpy.matrix([[12.0], [12.0]])
81 self.U_min = numpy.matrix([[-12.0], [-12.0]])
82
83 self.InitializeState()
84
85
86class Column(ColumnController):
Austin Schuhd5ccb862017-03-11 22:06:36 -080087 def __init__(self, name='Column', disable_indexer=False):
Austin Schuh82a66dc2017-03-04 15:06:44 -080088 super(Column, self).__init__(name)
89 A_continuous = numpy.matrix(numpy.zeros((4, 4)))
90 B_continuous = numpy.matrix(numpy.zeros((4, 2)))
91
92 A_continuous[0, 1] = 1
93 A_continuous[1:, 1:] = self.A_continuous
94 B_continuous[1:, :] = self.B_continuous
95
96 self.A_continuous = A_continuous
97 self.B_continuous = B_continuous
98
99 self.A, self.B = self.ContinuousToDiscrete(
100 self.A_continuous, self.B_continuous, self.dt)
101
Austin Schuh82a66dc2017-03-04 15:06:44 -0800102 self.C = numpy.matrix([[1, 0, 0, 0], [-1, 0, 1, 0]])
103 self.D = numpy.matrix([[0, 0], [0, 0]])
104
105 orig_K = self.K
106 self.K = numpy.matrix(numpy.zeros((2, 4)))
107 self.K[:, 1:] = orig_K
108
Austin Schuhd5ccb862017-03-11 22:06:36 -0800109 glog.debug('K is ' + repr(self.K))
110 # TODO(austin): Do we want to damp velocity out or not when disabled?
111 #if disable_indexer:
112 # self.K[0, 1] = 0.0
113 # self.K[1, 1] = 0.0
114
Austin Schuh82a66dc2017-03-04 15:06:44 -0800115 orig_Kff = self.Kff
116 self.Kff = numpy.matrix(numpy.zeros((2, 4)))
117 self.Kff[:, 1:] = orig_Kff
118
119 q_pos = 0.12
120 q_vel = 2.00
121 self.Q = numpy.matrix([[(q_pos ** 2.0), 0.0, 0.0, 0.0],
122 [0.0, (q_vel ** 2.0), 0.0, 0.0],
123 [0.0, 0.0, (q_pos ** 2.0), 0.0],
124 [0.0, 0.0, 0.0, (q_vel ** 2.0)]])
125
126 r_pos = 0.05
127 self.R = numpy.matrix([[(r_pos ** 2.0), 0.0],
128 [0.0, (r_pos ** 2.0)]])
129
130 self.KalmanGain, self.Q_steady = controls.kalman(
131 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
132 self.L = self.A * self.KalmanGain
133
134 self.InitializeState()
135
136
137class IntegralColumn(Column):
Austin Schuhd5ccb862017-03-11 22:06:36 -0800138 def __init__(self, name='IntegralColumn', voltage_error_noise=None,
139 disable_indexer=False):
Austin Schuh82a66dc2017-03-04 15:06:44 -0800140 super(IntegralColumn, self).__init__(name)
141
142 A_continuous = numpy.matrix(numpy.zeros((6, 6)))
143 A_continuous[0:4, 0:4] = self.A_continuous
144 A_continuous[0:4:, 4:6] = self.B_continuous
145
146 B_continuous = numpy.matrix(numpy.zeros((6, 2)))
147 B_continuous[0:4, :] = self.B_continuous
148
149 self.A_continuous = A_continuous
150 self.B_continuous = B_continuous
Austin Schuh82a66dc2017-03-04 15:06:44 -0800151
152 self.A, self.B = self.ContinuousToDiscrete(
153 self.A_continuous, self.B_continuous, self.dt)
154
Austin Schuh82a66dc2017-03-04 15:06:44 -0800155 C = numpy.matrix(numpy.zeros((2, 6)))
156 C[0:2, 0:4] = self.C
157 self.C = C
Austin Schuh82a66dc2017-03-04 15:06:44 -0800158
159 self.D = numpy.matrix([[0, 0], [0, 0]])
160
161 orig_K = self.K
162 self.K = numpy.matrix(numpy.zeros((2, 6)))
163 self.K[:, 0:4] = orig_K
Austin Schuhd5ccb862017-03-11 22:06:36 -0800164
165 # TODO(austin): I'm not certain this is ideal. If someone spins the bottom
166 # at a constant rate, we'll learn a voltage offset. That should translate
167 # directly to a voltage on the turret to hold it steady. I'm also not
168 # convinced we care that much. If the indexer is off, it'll stop rather
169 # quickly anyways, so this is mostly a moot point.
170 if not disable_indexer:
171 self.K[0, 4] = 1
Austin Schuh82a66dc2017-03-04 15:06:44 -0800172 self.K[1, 5] = 1
173
174 orig_Kff = self.Kff
175 self.Kff = numpy.matrix(numpy.zeros((2, 6)))
176 self.Kff[:, 0:4] = orig_Kff
177
Austin Schuheb5c22e2017-04-09 18:30:28 -0700178 q_pos = 0.40
Austin Schuh82a66dc2017-03-04 15:06:44 -0800179 q_vel = 2.00
Austin Schuheb5c22e2017-04-09 18:30:28 -0700180 q_voltage = 8.0
Austin Schuh82a66dc2017-03-04 15:06:44 -0800181 if voltage_error_noise is not None:
182 q_voltage = voltage_error_noise
183
184 self.Q = numpy.matrix([[(q_pos ** 2.0), 0.0, 0.0, 0.0, 0.0, 0.0],
185 [0.0, (q_vel ** 2.0), 0.0, 0.0, 0.0, 0.0],
186 [0.0, 0.0, (q_pos ** 2.0), 0.0, 0.0, 0.0],
187 [0.0, 0.0, 0.0, (q_vel ** 2.0), 0.0, 0.0],
188 [0.0, 0.0, 0.0, 0.0, (q_voltage ** 2.0), 0.0],
189 [0.0, 0.0, 0.0, 0.0, 0.0, (q_voltage ** 2.0)]])
190
191 r_pos = 0.05
192 self.R = numpy.matrix([[(r_pos ** 2.0), 0.0],
193 [0.0, (r_pos ** 2.0)]])
194
195 self.KalmanGain, self.Q_steady = controls.kalman(
196 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
197 self.L = self.A * self.KalmanGain
198
199 self.InitializeState()
200
201
202class ScenarioPlotter(object):
203 def __init__(self):
204 # Various lists for graphing things.
205 self.t = []
206 self.xi = []
207 self.xt = []
208 self.vi = []
209 self.vt = []
210 self.ai = []
211 self.at = []
212 self.x_hat = []
213 self.ui = []
214 self.ut = []
215 self.ui_fb = []
216 self.ut_fb = []
217 self.offseti = []
218 self.offsett = []
219 self.turret_error = []
220
221 def run_test(self, column, end_goal,
222 controller_column,
223 observer_column=None,
224 iterations=200):
225 """Runs the column plant with an initial condition and goal.
226
227 Args:
228 column: column object to use.
229 end_goal: end_goal state.
230 controller_column: Intake object to get K from, or None if we should
231 use column.
232 observer_column: Intake object to use for the observer, or None if we should
233 use the actual state.
234 iterations: Number of timesteps to run the model for.
235 """
236
237 if controller_column is None:
238 controller_column = column
239
240 vbat = 12.0
241
242 if self.t:
243 initial_t = self.t[-1] + column.dt
244 else:
245 initial_t = 0
246
247 goal = numpy.concatenate((column.X, numpy.matrix(numpy.zeros((2, 1)))), axis=0)
248
249 profile = TrapezoidProfile(column.dt)
250 profile.set_maximum_acceleration(10.0)
251 profile.set_maximum_velocity(3.0)
252 profile.SetGoal(goal[2, 0])
253
254 U_last = numpy.matrix(numpy.zeros((2, 1)))
255 for i in xrange(iterations):
256 observer_column.Y = column.Y
257 observer_column.CorrectObserver(U_last)
258
259 self.offseti.append(observer_column.X_hat[4, 0])
260 self.offsett.append(observer_column.X_hat[5, 0])
261 self.x_hat.append(observer_column.X_hat[0, 0])
262
263 next_goal = numpy.concatenate(
264 (end_goal[0:2, :],
265 profile.Update(end_goal[2, 0], end_goal[3, 0]),
266 end_goal[4:6, :]),
267 axis=0)
268
269 ff_U = controller_column.Kff * (next_goal - observer_column.A * goal)
270 fb_U = controller_column.K * (goal - observer_column.X_hat)
271 self.turret_error.append((goal[2, 0] - column.X[2, 0]) * 100.0)
272 self.ui_fb.append(fb_U[0, 0])
273 self.ut_fb.append(fb_U[1, 0])
274
275 U_uncapped = ff_U + fb_U
276
277 U = U_uncapped.copy()
278 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
279 U[1, 0] = numpy.clip(U[1, 0], -vbat, vbat)
280 self.xi.append(column.X[0, 0])
281 self.xt.append(column.X[2, 0])
282
283 if self.vi:
284 last_vi = self.vi[-1]
285 else:
286 last_vi = 0
287 if self.vt:
288 last_vt = self.vt[-1]
289 else:
290 last_vt = 0
291
292 self.vi.append(column.X[1, 0])
293 self.vt.append(column.X[3, 0])
294 self.ai.append((self.vi[-1] - last_vi) / column.dt)
295 self.at.append((self.vt[-1] - last_vt) / column.dt)
296
297 offset = 0.0
298 if i > 100:
299 offset = 1.0
Austin Schuhd5ccb862017-03-11 22:06:36 -0800300 column.Update(U + numpy.matrix([[0.0], [offset]]))
Austin Schuh82a66dc2017-03-04 15:06:44 -0800301
302 observer_column.PredictObserver(U)
303
304 self.t.append(initial_t + i * column.dt)
305 self.ui.append(U[0, 0])
306 self.ut.append(U[1, 0])
307
308 ff_U -= U_uncapped - U
309 goal = controller_column.A * goal + controller_column.B * ff_U
310
311 if U[1, 0] != U_uncapped[1, 0]:
312 profile.MoveCurrentState(
313 numpy.matrix([[goal[2, 0]], [goal[3, 0]]]))
314
315 glog.debug('Time: %f', self.t[-1])
Austin Schuhd5ccb862017-03-11 22:06:36 -0800316 glog.debug('goal_error %s', repr((end_goal - goal).T))
317 glog.debug('error %s', repr((observer_column.X_hat - end_goal).T))
Austin Schuh82a66dc2017-03-04 15:06:44 -0800318
319 def Plot(self):
320 pylab.subplot(3, 1, 1)
Austin Schuhd5ccb862017-03-11 22:06:36 -0800321 pylab.plot(self.t, self.xi, label='x_indexer')
322 pylab.plot(self.t, self.xt, label='x_turret')
Austin Schuh82a66dc2017-03-04 15:06:44 -0800323 pylab.plot(self.t, self.x_hat, label='x_hat')
Austin Schuhd5ccb862017-03-11 22:06:36 -0800324 pylab.plot(self.t, self.turret_error, label='turret_error * 100')
Austin Schuh82a66dc2017-03-04 15:06:44 -0800325 pylab.legend()
326
327 pylab.subplot(3, 1, 2)
Austin Schuhd5ccb862017-03-11 22:06:36 -0800328 pylab.plot(self.t, self.ui, label='u_indexer')
329 pylab.plot(self.t, self.ui_fb, label='u_indexer_fb')
330 pylab.plot(self.t, self.ut, label='u_turret')
331 pylab.plot(self.t, self.ut_fb, label='u_turret_fb')
332 pylab.plot(self.t, self.offseti, label='voltage_offset_indexer')
333 pylab.plot(self.t, self.offsett, label='voltage_offset_turret')
Austin Schuh82a66dc2017-03-04 15:06:44 -0800334 pylab.legend()
335
336 pylab.subplot(3, 1, 3)
Austin Schuhd5ccb862017-03-11 22:06:36 -0800337 pylab.plot(self.t, self.ai, label='a_indexer')
338 pylab.plot(self.t, self.at, label='a_turret')
339 pylab.plot(self.t, self.vi, label='v_indexer')
340 pylab.plot(self.t, self.vt, label='v_turret')
Austin Schuh82a66dc2017-03-04 15:06:44 -0800341 pylab.legend()
342
343 pylab.show()
344
345
346def main(argv):
347 scenario_plotter = ScenarioPlotter()
348
349 column = Column()
350 column_controller = IntegralColumn()
351 observer_column = IntegralColumn()
352
353 initial_X = numpy.matrix([[0.0], [0.0], [0.0], [0.0]])
354 R = numpy.matrix([[0.0], [10.0], [5.0], [0.0], [0.0], [0.0]])
355 scenario_plotter.run_test(column, end_goal=R, controller_column=column_controller,
Austin Schuhd5ccb862017-03-11 22:06:36 -0800356 observer_column=observer_column, iterations=400)
Austin Schuh82a66dc2017-03-04 15:06:44 -0800357
358 if FLAGS.plot:
359 scenario_plotter.Plot()
360
361 if len(argv) != 7:
362 glog.fatal('Expected .h file name and .cc file names')
363 else:
364 namespaces = ['y2017', 'control_loops', 'superstructure', 'column']
365 column = Column('Column')
366 loop_writer = control_loop.ControlLoopWriter('Column', [column],
367 namespaces=namespaces)
Austin Schuhd5ccb862017-03-11 22:06:36 -0800368 loop_writer.AddConstant(control_loop.Constant(
369 'kIndexerFreeSpeed', '%f', column.indexer.free_speed))
370 loop_writer.AddConstant(control_loop.Constant(
371 'kIndexerOutputRatio', '%f', column.indexer.G))
372 loop_writer.AddConstant(control_loop.Constant(
373 'kTurretFreeSpeed', '%f', column.turret.free_speed))
374 loop_writer.AddConstant(control_loop.Constant(
375 'kTurretOutputRatio', '%f', column.turret.G))
Austin Schuh82a66dc2017-03-04 15:06:44 -0800376 loop_writer.Write(argv[1], argv[2])
377
Austin Schuhd5ccb862017-03-11 22:06:36 -0800378 # IntegralColumn controller 1 will disable the indexer.
Austin Schuh82a66dc2017-03-04 15:06:44 -0800379 integral_column = IntegralColumn('IntegralColumn')
Austin Schuhd5ccb862017-03-11 22:06:36 -0800380 disabled_integral_column = IntegralColumn('DisabledIntegralColumn',
381 disable_indexer=True)
Austin Schuh82a66dc2017-03-04 15:06:44 -0800382 integral_loop_writer = control_loop.ControlLoopWriter(
Austin Schuhd5ccb862017-03-11 22:06:36 -0800383 'IntegralColumn', [integral_column, disabled_integral_column],
384 namespaces=namespaces)
Austin Schuh82a66dc2017-03-04 15:06:44 -0800385 integral_loop_writer.Write(argv[3], argv[4])
386
387 stuck_integral_column = IntegralColumn('StuckIntegralColumn', voltage_error_noise=8.0)
388 stuck_integral_loop_writer = control_loop.ControlLoopWriter(
389 'StuckIntegralColumn', [stuck_integral_column], namespaces=namespaces)
390 stuck_integral_loop_writer.Write(argv[5], argv[6])
391
392
393if __name__ == '__main__':
394 argv = FLAGS(sys.argv)
395 glog.init()
396 sys.exit(main(argv))