blob: 21fa4ecb96896a5e778407b16c53d5e51a6290a2 [file] [log] [blame]
Austin Schuhb5d302f2019-01-20 20:51:19 -08001#!/usr/bin/python
2
3from aos.util.trapezoid_profile import TrapezoidProfile
4from frc971.control_loops.python import control_loop
5from frc971.control_loops.python import controls
6import numpy
7import sys
8from matplotlib import pylab
9import glog
10
11
12class LinearSystemParams(object):
13 def __init__(self,
14 name,
15 motor,
16 G,
17 radius,
18 mass,
19 q_pos,
20 q_vel,
21 kalman_q_pos,
22 kalman_q_vel,
23 kalman_q_voltage,
24 kalman_r_position,
25 dt=0.005):
26 self.name = name
27 self.motor = motor
28 self.G = G
29 self.radius = radius
30 self.mass = mass
31 self.q_pos = q_pos
32 self.q_vel = q_vel
33 self.kalman_q_pos = kalman_q_pos
34 self.kalman_q_vel = kalman_q_vel
35 self.kalman_q_voltage = kalman_q_voltage
36 self.kalman_r_position = kalman_r_position
37 self.dt = dt
38
39
40class LinearSystem(control_loop.ControlLoop):
41 def __init__(self, params, name='LinearSystem'):
42 super(LinearSystem, self).__init__(name)
43 self.params = params
44
45 self.motor = params.motor
46
47 # Gear ratio
48 self.G = params.G
49 self.radius = params.radius
50
51 # 5.4 kg of moving mass for the linear_system
52 self.mass = params.mass + self.motor.motor_inertia / (
53 (self.G * self.radius)**2.0)
54
55 # Control loop time step
56 self.dt = params.dt
57
58 # State is [position, velocity]
59 # Input is [Voltage]
60 C1 = self.motor.Kt / (self.G * self.G * self.radius * self.radius *
61 self.motor.resistance * self.mass *
62 self.motor.Kv)
63 C2 = self.motor.Kt / (self.G * self.radius * self.motor.resistance *
64 self.mass)
65
66 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
67
68 # Start with the unmodified input
69 self.B_continuous = numpy.matrix([[0], [C2]])
70 glog.debug(repr(self.A_continuous))
71 glog.debug(repr(self.B_continuous))
72
73 self.C = numpy.matrix([[1, 0]])
74 self.D = numpy.matrix([[0]])
75
76 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
77 self.B_continuous, self.dt)
78
79 controllability = controls.ctrb(self.A, self.B)
80 glog.debug('Controllability of %d',
81 numpy.linalg.matrix_rank(controllability))
82 glog.debug('Mass: %f', self.mass)
83 glog.debug('Stall force: %f', self.motor.stall_torque / self.G / self.radius)
84 glog.debug('Stall acceleration: %f', self.motor.stall_torque / self.G /
85 self.radius / self.mass)
86
87 glog.debug('Free speed is %f',
88 -self.B_continuous[1, 0] / self.A_continuous[1, 1] * 12.0)
89
90 self.Q = numpy.matrix([[(1.0 / (self.params.q_pos**2.0)), 0.0],
91 [0.0, (1.0 / (self.params.q_vel**2.0))]])
92
93 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
94 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
95
96 q_pos_ff = 0.005
97 q_vel_ff = 1.0
98 self.Qff = numpy.matrix([[(1.0 / (q_pos_ff**2.0)), 0.0],
99 [0.0, (1.0 / (q_vel_ff**2.0))]])
100
101 self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
102
103 glog.debug('K %s', repr(self.K))
104 glog.debug('Poles are %s',
105 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]))
106
107 self.Q = numpy.matrix([[(self.params.kalman_q_pos**2.0), 0.0],
108 [0.0, (self.params.kalman_q_vel**2.0)]])
109
110 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
111
112 self.KalmanGain, self.Q_steady = controls.kalman(
113 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
114
115 glog.debug('Kal %s', repr(self.KalmanGain))
116
117 # The box formed by U_min and U_max must encompass all possible values,
118 # or else Austin's code gets angry.
119 self.U_max = numpy.matrix([[12.0]])
120 self.U_min = numpy.matrix([[-12.0]])
121
122 self.InitializeState()
123
124
125class IntegralLinearSystem(LinearSystem):
126 def __init__(self, params, name='IntegralLinearSystem'):
127 super(IntegralLinearSystem, self).__init__(params, name=name)
128
129 self.kalman_q_voltage = params.kalman_q_voltage
130
131 self.A_continuous_unaugmented = self.A_continuous
132 self.B_continuous_unaugmented = self.B_continuous
133
134 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
135 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
136 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
137
138 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
139 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
140
141 self.C_unaugmented = self.C
142 self.C = numpy.matrix(numpy.zeros((1, 3)))
143 self.C[0:1, 0:2] = self.C_unaugmented
144
145 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
146 self.B_continuous, self.dt)
147
148 self.Q = numpy.matrix(
149 [[(self.params.kalman_q_pos**2.0), 0.0, 0.0],
150 [0.0, (self.params.kalman_q_vel**2.0), 0.0],
151 [0.0, 0.0, (self.params.kalman_q_voltage**2.0)]])
152
153 self.R = numpy.matrix([[(self.params.kalman_r_position**2.0)]])
154
155 self.KalmanGain, self.Q_steady = controls.kalman(
156 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
157
158 self.K_unaugmented = self.K
159 self.K = numpy.matrix(numpy.zeros((1, 3)))
160 self.K[0, 0:2] = self.K_unaugmented
161 self.K[0, 2] = 1
162
163 self.Kff = numpy.concatenate(
164 (self.Kff, numpy.matrix(numpy.zeros((1, 1)))), axis=1)
165
166 self.InitializeState()
167
168
169def RunTest(plant,
170 end_goal,
171 controller,
172 observer=None,
173 duration=1.0,
174 use_profile=True,
175 kick_time=0.5,
176 kick_magnitude=0.0,
177 max_velocity=0.3):
178 """Runs the plant with an initial condition and goal.
179
180 Args:
181 plant: plant object to use.
182 end_goal: end_goal state.
183 controller: Intake object to get K from, or None if we should
184 use plant.
185 observer: Intake object to use for the observer, or None if we should
186 use the actual state.
187 duration: float, time in seconds to run the simulation for.
188 kick_time: float, time in seconds to kick the robot.
189 kick_magnitude: float, disturbance in volts to apply.
190 max_velocity: float, the max speed in m/s to profile.
191 """
192 t_plot = []
193 x_plot = []
194 v_plot = []
195 a_plot = []
196 x_goal_plot = []
197 v_goal_plot = []
198 x_hat_plot = []
199 u_plot = []
200 offset_plot = []
201
202 if controller is None:
203 controller = plant
204
205 vbat = 12.0
206
207 goal = numpy.concatenate(
208 (plant.X, numpy.matrix(numpy.zeros((1, 1)))), axis=0)
209
210 profile = TrapezoidProfile(plant.dt)
211 profile.set_maximum_acceleration(10.0)
212 profile.set_maximum_velocity(max_velocity)
213 profile.SetGoal(goal[0, 0])
214
215 U_last = numpy.matrix(numpy.zeros((1, 1)))
216 iterations = int(duration / plant.dt)
217 for i in xrange(iterations):
218 t = i * plant.dt
219 observer.Y = plant.Y
220 observer.CorrectObserver(U_last)
221
222 offset_plot.append(observer.X_hat[2, 0])
223 x_hat_plot.append(observer.X_hat[0, 0])
224
225 next_goal = numpy.concatenate(
226 (profile.Update(end_goal[0, 0], end_goal[1, 0]),
227 numpy.matrix(numpy.zeros((1, 1)))),
228 axis=0)
229
230 ff_U = controller.Kff * (next_goal - observer.A * goal)
231
232 if use_profile:
233 U_uncapped = controller.K * (goal - observer.X_hat) + ff_U
234 x_goal_plot.append(goal[0, 0])
235 v_goal_plot.append(goal[1, 0])
236 else:
237 U_uncapped = controller.K * (end_goal - observer.X_hat)
238 x_goal_plot.append(end_goal[0, 0])
239 v_goal_plot.append(end_goal[1, 0])
240
241 U = U_uncapped.copy()
242 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
243 x_plot.append(plant.X[0, 0])
244
245 if v_plot:
246 last_v = v_plot[-1]
247 else:
248 last_v = 0
249
250 v_plot.append(plant.X[1, 0])
251 a_plot.append((v_plot[-1] - last_v) / plant.dt)
252
253 u_offset = 0.0
254 if t >= kick_time:
255 u_offset = kick_magnitude
256 plant.Update(U + u_offset)
257
258 observer.PredictObserver(U)
259
260 t_plot.append(t)
261 u_plot.append(U[0, 0])
262
263 ff_U -= U_uncapped - U
264 goal = controller.A * goal + controller.B * ff_U
265
266 if U[0, 0] != U_uncapped[0, 0]:
267 profile.MoveCurrentState(
268 numpy.matrix([[goal[0, 0]], [goal[1, 0]]]))
269
270 glog.debug('Time: %f', t_plot[-1])
271 glog.debug('goal_error %s', repr(end_goal - goal))
272 glog.debug('error %s', repr(observer.X_hat - end_goal))
273
274 pylab.subplot(3, 1, 1)
275 pylab.plot(t_plot, x_plot, label='x')
276 pylab.plot(t_plot, x_hat_plot, label='x_hat')
277 pylab.plot(t_plot, x_goal_plot, label='x_goal')
278 pylab.legend()
279
280 pylab.subplot(3, 1, 2)
281 pylab.plot(t_plot, u_plot, label='u')
282 pylab.plot(t_plot, offset_plot, label='voltage_offset')
283 pylab.legend()
284
285 pylab.subplot(3, 1, 3)
286 pylab.plot(t_plot, a_plot, label='a')
287 pylab.legend()
288
289 pylab.show()
290
291
292def PlotStep(params, R):
293 """Plots a step move to the goal.
294
295 Args:
296 R: numpy.matrix(2, 1), the goal"""
297 plant = LinearSystem(params, params.name)
298 controller = IntegralLinearSystem(params, params.name)
299 observer = IntegralLinearSystem(params, params.name)
300
301 # Test moving the system.
302 initial_X = numpy.matrix([[0.0], [0.0]])
303 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
304 augmented_R[0:2, :] = R
305 RunTest(
306 plant,
307 end_goal=augmented_R,
308 controller=controller,
309 observer=observer,
310 duration=2.0,
311 use_profile=False,
312 kick_time=1.0,
313 kick_magnitude=0.0)
314
315
316def PlotKick(params, R):
317 """Plots a step motion with a kick at 1.0 seconds.
318
319 Args:
320 R: numpy.matrix(2, 1), the goal"""
321 plant = LinearSystem(params, params.name)
322 controller = IntegralLinearSystem(params, params.name)
323 observer = IntegralLinearSystem(params, params.name)
324
325 # Test moving the system.
326 initial_X = numpy.matrix([[0.0], [0.0]])
327 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
328 augmented_R[0:2, :] = R
329 RunTest(
330 plant,
331 end_goal=augmented_R,
332 controller=controller,
333 observer=observer,
334 duration=2.0,
335 use_profile=False,
336 kick_time=1.0,
337 kick_magnitude=2.0)
338
339
340def PlotMotion(params, R, max_velocity=0.3):
341 """Plots a trapezoidal motion.
342
343 Args:
344 R: numpy.matrix(2, 1), the goal,
345 max_velocity: float, The max velocity of the profile.
346 """
347 plant = LinearSystem(params, params.name)
348 controller = IntegralLinearSystem(params, params.name)
349 observer = IntegralLinearSystem(params, params.name)
350
351 # Test moving the system.
352 initial_X = numpy.matrix([[0.0], [0.0]])
353 augmented_R = numpy.matrix(numpy.zeros((3, 1)))
354 augmented_R[0:2, :] = R
355 RunTest(
356 plant,
357 end_goal=augmented_R,
358 controller=controller,
359 observer=observer,
360 duration=2.0,
361 use_profile=True,
362 max_velocity=max_velocity)
363
364
365def WriteLinearSystem(params, plant_files, controller_files, year_namespaces):
366 """Writes out the constants for a linear system to a file.
367
368 Args:
369 params: LinearSystemParams, the parameters defining the system.
370 plant_files: list of strings, the cc and h files for the plant.
371 controller_files: list of strings, the cc and h files for the integral
372 controller.
373 year_namespaces: list of strings, the namespace list to use.
374 """
375 # Write the generated constants out to a file.
376 linear_system = LinearSystem(params, params.name)
377 loop_writer = control_loop.ControlLoopWriter(
378 linear_system.name, [linear_system], namespaces=year_namespaces)
379 loop_writer.AddConstant(
380 control_loop.Constant('kFreeSpeed', '%f', linear_system.motor.
381 free_speed / (2.0 * numpy.pi)))
382 loop_writer.AddConstant(
383 control_loop.Constant('kOutputRatio', '%f', linear_system.G *
384 linear_system.radius))
385 loop_writer.Write(plant_files[0], plant_files[1])
386
387 integral_linear_system = IntegralLinearSystem(params,
388 'Integral' + params.name)
389 integral_loop_writer = control_loop.ControlLoopWriter(
390 integral_linear_system.name, [integral_linear_system],
391 namespaces=year_namespaces)
392 integral_loop_writer.Write(controller_files[0], controller_files[1])