blob: 64071336f6d57312a3513018a8b07ad139ee2f7d [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
Austin Schuh48d60c12017-02-04 21:58:58 -08002
John Park33858a32018-09-28 23:05:48 -07003from aos.util.trapezoid_profile import TrapezoidProfile
Austin Schuh48d60c12017-02-04 21:58:58 -08004from frc971.control_loops.python import control_loop
5from frc971.control_loops.python import controls
6import numpy
7import sys
Austin Schuh48d60c12017-02-04 21:58:58 -08008from matplotlib import pylab
9import gflags
10import glog
11
12FLAGS = gflags.FLAGS
13
14try:
Ravago Jones5127ccc2022-07-31 16:32:45 -070015 gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
Austin Schuh48d60c12017-02-04 21:58:58 -080016except gflags.DuplicateFlagError:
Ravago Jones5127ccc2022-07-31 16:32:45 -070017 pass
18
Austin Schuh48d60c12017-02-04 21:58:58 -080019
20class Turret(control_loop.ControlLoop):
Austin Schuh48d60c12017-02-04 21:58:58 -080021
Ravago Jones5127ccc2022-07-31 16:32:45 -070022 def __init__(self, name='Turret'):
23 super(Turret, self).__init__(name)
24 # Stall Torque in N m
25 self.stall_torque = 0.71
26 # Stall Current in Amps
27 self.stall_current = 134
28 self.free_speed_rpm = 18730.0
29 # Free Speed in rotations/second.
30 self.free_speed = self.free_speed_rpm / 60.0
31 # Free Current in Amps
32 self.free_current = 0.7
Austin Schuh48d60c12017-02-04 21:58:58 -080033
Ravago Jones5127ccc2022-07-31 16:32:45 -070034 # Resistance of the motor
35 self.resistance = 12.0 / self.stall_current
36 # Motor velocity constant
37 self.Kv = ((self.free_speed * 2.0 * numpy.pi) /
38 (12.0 - self.resistance * self.free_current))
39 # Torque constant
40 self.Kt = self.stall_torque / self.stall_current
41 # Gear ratio
42 self.G = (12.0 / 60.0) * (11.0 / 94.0)
Austin Schuh48d60c12017-02-04 21:58:58 -080043
Ravago Jones5127ccc2022-07-31 16:32:45 -070044 # Motor inertia in kg * m^2
45 self.motor_inertia = 0.00001187
Austin Schuh48d60c12017-02-04 21:58:58 -080046
Ravago Jones5127ccc2022-07-31 16:32:45 -070047 # Moment of inertia, measured in CAD.
48 # Extra mass to compensate for friction is added on.
49 self.J = 0.06 + self.motor_inertia * ((1.0 / self.G)**2.0)
50 glog.debug('Turret J is: %f', self.J)
Austin Schuh48d60c12017-02-04 21:58:58 -080051
Ravago Jones5127ccc2022-07-31 16:32:45 -070052 # Control loop time step
53 self.dt = 0.005
Austin Schuh48d60c12017-02-04 21:58:58 -080054
Ravago Jones5127ccc2022-07-31 16:32:45 -070055 # State is [position, velocity]
56 # Input is [Voltage]
Austin Schuh48d60c12017-02-04 21:58:58 -080057
Ravago Jones5127ccc2022-07-31 16:32:45 -070058 C1 = self.Kt / (self.resistance * self.J * self.Kv * self.G * self.G)
59 C2 = self.Kt / (self.J * self.resistance * self.G)
Austin Schuh48d60c12017-02-04 21:58:58 -080060
Ravago Jones5127ccc2022-07-31 16:32:45 -070061 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
Austin Schuh48d60c12017-02-04 21:58:58 -080062
Ravago Jones5127ccc2022-07-31 16:32:45 -070063 # Start with the unmodified input
64 self.B_continuous = numpy.matrix([[0], [C2]])
Austin Schuh48d60c12017-02-04 21:58:58 -080065
Ravago Jones5127ccc2022-07-31 16:32:45 -070066 self.C = numpy.matrix([[1, 0]])
67 self.D = numpy.matrix([[0]])
Austin Schuh48d60c12017-02-04 21:58:58 -080068
Ravago Jones5127ccc2022-07-31 16:32:45 -070069 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
70 self.B_continuous, self.dt)
Austin Schuh48d60c12017-02-04 21:58:58 -080071
Ravago Jones5127ccc2022-07-31 16:32:45 -070072 controllability = controls.ctrb(self.A, self.B)
Austin Schuh48d60c12017-02-04 21:58:58 -080073
Ravago Jones5127ccc2022-07-31 16:32:45 -070074 glog.debug('Free speed is %f',
75 -self.B_continuous[1, 0] / self.A_continuous[1, 1] * 12.0)
Austin Schuh48d60c12017-02-04 21:58:58 -080076
Ravago Jones5127ccc2022-07-31 16:32:45 -070077 # Calculate the LQR controller gain
78 q_pos = 0.20
79 q_vel = 5.0
80 self.Q = numpy.matrix([[(1.0 / (q_pos**2.0)), 0.0],
81 [0.0, (1.0 / (q_vel**2.0))]])
Austin Schuh48d60c12017-02-04 21:58:58 -080082
Ravago Jones5127ccc2022-07-31 16:32:45 -070083 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
84 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
Austin Schuh48d60c12017-02-04 21:58:58 -080085
Ravago Jones5127ccc2022-07-31 16:32:45 -070086 # Calculate the feed forwards gain.
87 q_pos_ff = 0.005
88 q_vel_ff = 1.0
89 self.Qff = numpy.matrix([[(1.0 / (q_pos_ff**2.0)), 0.0],
90 [0.0, (1.0 / (q_vel_ff**2.0))]])
Austin Schuh48d60c12017-02-04 21:58:58 -080091
Ravago Jones5127ccc2022-07-31 16:32:45 -070092 self.Kff = controls.TwoStateFeedForwards(self.B, self.Qff)
Austin Schuh48d60c12017-02-04 21:58:58 -080093
Ravago Jones5127ccc2022-07-31 16:32:45 -070094 q_pos = 0.10
95 q_vel = 1.65
96 self.Q = numpy.matrix([[(q_pos**2.0), 0.0], [0.0, (q_vel**2.0)]])
Austin Schuh48d60c12017-02-04 21:58:58 -080097
Ravago Jones5127ccc2022-07-31 16:32:45 -070098 r_volts = 0.025
99 self.R = numpy.matrix([[(r_volts**2.0)]])
Austin Schuh48d60c12017-02-04 21:58:58 -0800100
Ravago Jones5127ccc2022-07-31 16:32:45 -0700101 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
102 B=self.B,
103 C=self.C,
104 Q=self.Q,
105 R=self.R)
106 self.L = self.A * self.KalmanGain
Austin Schuh48d60c12017-02-04 21:58:58 -0800107
Ravago Jones5127ccc2022-07-31 16:32:45 -0700108 # 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.InitializeState()
114
Austin Schuh48d60c12017-02-04 21:58:58 -0800115
116class IntegralTurret(Turret):
Austin Schuh48d60c12017-02-04 21:58:58 -0800117
Ravago Jones5127ccc2022-07-31 16:32:45 -0700118 def __init__(self, name='IntegralTurret'):
119 super(IntegralTurret, self).__init__(name=name)
Austin Schuh48d60c12017-02-04 21:58:58 -0800120
Ravago Jones5127ccc2022-07-31 16:32:45 -0700121 self.A_continuous_unaugmented = self.A_continuous
122 self.B_continuous_unaugmented = self.B_continuous
Austin Schuh48d60c12017-02-04 21:58:58 -0800123
Ravago Jones5127ccc2022-07-31 16:32:45 -0700124 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
125 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
126 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
Austin Schuh48d60c12017-02-04 21:58:58 -0800127
Ravago Jones5127ccc2022-07-31 16:32:45 -0700128 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
129 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
Austin Schuh48d60c12017-02-04 21:58:58 -0800130
Ravago Jones5127ccc2022-07-31 16:32:45 -0700131 self.C_unaugmented = self.C
132 self.C = numpy.matrix(numpy.zeros((1, 3)))
133 self.C[0:1, 0:2] = self.C_unaugmented
Austin Schuh48d60c12017-02-04 21:58:58 -0800134
Ravago Jones5127ccc2022-07-31 16:32:45 -0700135 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
136 self.B_continuous, self.dt)
Austin Schuh48d60c12017-02-04 21:58:58 -0800137
Ravago Jones5127ccc2022-07-31 16:32:45 -0700138 q_pos = 0.12
139 q_vel = 2.00
140 q_voltage = 3.0
141 self.Q = numpy.matrix([[(q_pos**2.0), 0.0, 0.0],
142 [0.0, (q_vel**2.0), 0.0],
143 [0.0, 0.0, (q_voltage**2.0)]])
Austin Schuh48d60c12017-02-04 21:58:58 -0800144
Ravago Jones5127ccc2022-07-31 16:32:45 -0700145 r_pos = 0.05
146 self.R = numpy.matrix([[(r_pos**2.0)]])
Austin Schuh48d60c12017-02-04 21:58:58 -0800147
Ravago Jones5127ccc2022-07-31 16:32:45 -0700148 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
149 B=self.B,
150 C=self.C,
151 Q=self.Q,
152 R=self.R)
153 self.L = self.A * self.KalmanGain
Austin Schuh48d60c12017-02-04 21:58:58 -0800154
Ravago Jones5127ccc2022-07-31 16:32:45 -0700155 self.K_unaugmented = self.K
156 self.K = numpy.matrix(numpy.zeros((1, 3)))
157 self.K[0, 0:2] = self.K_unaugmented
158 self.K[0, 2] = 1
Austin Schuh48d60c12017-02-04 21:58:58 -0800159
Ravago Jones5127ccc2022-07-31 16:32:45 -0700160 self.Kff = numpy.concatenate(
161 (self.Kff, numpy.matrix(numpy.zeros((1, 1)))), axis=1)
162
163 self.InitializeState()
164
Austin Schuh48d60c12017-02-04 21:58:58 -0800165
166class ScenarioPlotter(object):
Austin Schuh48d60c12017-02-04 21:58:58 -0800167
Ravago Jones5127ccc2022-07-31 16:32:45 -0700168 def __init__(self):
169 # Various lists for graphing things.
170 self.t = []
171 self.x = []
172 self.v = []
173 self.a = []
174 self.x_hat = []
175 self.u = []
176 self.offset = []
177
178 def run_test(self,
179 turret,
180 end_goal,
181 controller_turret,
182 observer_turret=None,
183 iterations=200):
184 """Runs the turret plant with an initial condition and goal.
Austin Schuh48d60c12017-02-04 21:58:58 -0800185
186 Args:
187 turret: turret object to use.
188 end_goal: end_goal state.
189 controller_turret: Turret object to get K from, or None if we should
190 use turret.
191 observer_turret: Turret object to use for the observer, or None if we should
192 use the actual state.
193 iterations: Number of timesteps to run the model for.
194 """
195
Ravago Jones5127ccc2022-07-31 16:32:45 -0700196 if controller_turret is None:
197 controller_turret = turret
Austin Schuh48d60c12017-02-04 21:58:58 -0800198
Ravago Jones5127ccc2022-07-31 16:32:45 -0700199 vbat = 12.0
Austin Schuh48d60c12017-02-04 21:58:58 -0800200
Ravago Jones5127ccc2022-07-31 16:32:45 -0700201 if self.t:
202 initial_t = self.t[-1] + turret.dt
203 else:
204 initial_t = 0
Austin Schuh48d60c12017-02-04 21:58:58 -0800205
Ravago Jones5127ccc2022-07-31 16:32:45 -0700206 goal = numpy.concatenate((turret.X, numpy.matrix(numpy.zeros((1, 1)))),
207 axis=0)
Austin Schuh48d60c12017-02-04 21:58:58 -0800208
Ravago Jones5127ccc2022-07-31 16:32:45 -0700209 profile = TrapezoidProfile(turret.dt)
210 profile.set_maximum_acceleration(100.0)
211 profile.set_maximum_velocity(7.0)
212 profile.SetGoal(goal[0, 0])
Austin Schuh48d60c12017-02-04 21:58:58 -0800213
Ravago Jones5127ccc2022-07-31 16:32:45 -0700214 U_last = numpy.matrix(numpy.zeros((1, 1)))
215 for i in range(iterations):
216 observer_turret.Y = turret.Y
217 observer_turret.CorrectObserver(U_last)
Austin Schuh48d60c12017-02-04 21:58:58 -0800218
Ravago Jones5127ccc2022-07-31 16:32:45 -0700219 self.offset.append(observer_turret.X_hat[2, 0])
220 self.x_hat.append(observer_turret.X_hat[0, 0])
Austin Schuh48d60c12017-02-04 21:58:58 -0800221
Ravago Jones5127ccc2022-07-31 16:32:45 -0700222 next_goal = numpy.concatenate(
223 (profile.Update(end_goal[0, 0], end_goal[1, 0]),
224 numpy.matrix(numpy.zeros((1, 1)))),
225 axis=0)
Austin Schuh48d60c12017-02-04 21:58:58 -0800226
Ravago Jones5127ccc2022-07-31 16:32:45 -0700227 ff_U = controller_turret.Kff * (next_goal -
228 observer_turret.A * goal)
Austin Schuh48d60c12017-02-04 21:58:58 -0800229
Ravago Jones5127ccc2022-07-31 16:32:45 -0700230 U_uncapped = controller_turret.K * (goal -
231 observer_turret.X_hat) + ff_U
232 U_uncapped = controller_turret.K * (end_goal -
233 observer_turret.X_hat)
234 U = U_uncapped.copy()
235 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
236 self.x.append(turret.X[0, 0])
Austin Schuh48d60c12017-02-04 21:58:58 -0800237
Ravago Jones5127ccc2022-07-31 16:32:45 -0700238 if self.v:
239 last_v = self.v[-1]
240 else:
241 last_v = 0
Austin Schuh48d60c12017-02-04 21:58:58 -0800242
Ravago Jones5127ccc2022-07-31 16:32:45 -0700243 self.v.append(turret.X[1, 0])
244 self.a.append((self.v[-1] - last_v) / turret.dt)
Austin Schuh48d60c12017-02-04 21:58:58 -0800245
Ravago Jones5127ccc2022-07-31 16:32:45 -0700246 offset = 0.0
247 if i > 100:
248 offset = 2.0
249 turret.Update(U + offset)
Austin Schuh48d60c12017-02-04 21:58:58 -0800250
Ravago Jones5127ccc2022-07-31 16:32:45 -0700251 observer_turret.PredictObserver(U)
Austin Schuh48d60c12017-02-04 21:58:58 -0800252
Ravago Jones5127ccc2022-07-31 16:32:45 -0700253 self.t.append(initial_t + i * turret.dt)
254 self.u.append(U[0, 0])
Austin Schuh48d60c12017-02-04 21:58:58 -0800255
Ravago Jones5127ccc2022-07-31 16:32:45 -0700256 ff_U -= U_uncapped - U
257 goal = controller_turret.A * goal + controller_turret.B * ff_U
Austin Schuh48d60c12017-02-04 21:58:58 -0800258
Ravago Jones5127ccc2022-07-31 16:32:45 -0700259 if U[0, 0] != U_uncapped[0, 0]:
260 profile.MoveCurrentState(
261 numpy.matrix([[goal[0, 0]], [goal[1, 0]]]))
Austin Schuh48d60c12017-02-04 21:58:58 -0800262
Ravago Jones5127ccc2022-07-31 16:32:45 -0700263 glog.debug('Time: %f', self.t[-1])
264 glog.debug('goal_error %s', repr(end_goal - goal))
265 glog.debug('error %s', repr(observer_turret.X_hat - end_goal))
Austin Schuh48d60c12017-02-04 21:58:58 -0800266
Ravago Jones5127ccc2022-07-31 16:32:45 -0700267 def Plot(self):
268 pylab.subplot(3, 1, 1)
269 pylab.plot(self.t, self.x, label='x')
270 pylab.plot(self.t, self.x_hat, label='x_hat')
271 pylab.legend()
Austin Schuh48d60c12017-02-04 21:58:58 -0800272
Ravago Jones5127ccc2022-07-31 16:32:45 -0700273 pylab.subplot(3, 1, 2)
274 pylab.plot(self.t, self.u, label='u')
275 pylab.plot(self.t, self.offset, label='voltage_offset')
276 pylab.legend()
Austin Schuh48d60c12017-02-04 21:58:58 -0800277
Ravago Jones5127ccc2022-07-31 16:32:45 -0700278 pylab.subplot(3, 1, 3)
279 pylab.plot(self.t, self.a, label='a')
280 pylab.legend()
Austin Schuh48d60c12017-02-04 21:58:58 -0800281
Ravago Jones5127ccc2022-07-31 16:32:45 -0700282 pylab.show()
Austin Schuh48d60c12017-02-04 21:58:58 -0800283
284
285def main(argv):
Ravago Jones5127ccc2022-07-31 16:32:45 -0700286 argv = FLAGS(argv)
287 glog.init()
Austin Schuh48d60c12017-02-04 21:58:58 -0800288
Ravago Jones5127ccc2022-07-31 16:32:45 -0700289 scenario_plotter = ScenarioPlotter()
Austin Schuh48d60c12017-02-04 21:58:58 -0800290
Ravago Jones5127ccc2022-07-31 16:32:45 -0700291 turret = Turret()
292 turret_controller = IntegralTurret()
293 observer_turret = IntegralTurret()
Austin Schuh48d60c12017-02-04 21:58:58 -0800294
Ravago Jones5127ccc2022-07-31 16:32:45 -0700295 # Test moving the turret with constant separation.
296 initial_X = numpy.matrix([[0.0], [0.0]])
297 R = numpy.matrix([[numpy.pi / 2.0], [0.0], [0.0]])
298 scenario_plotter.run_test(turret,
299 end_goal=R,
300 controller_turret=turret_controller,
301 observer_turret=observer_turret,
302 iterations=200)
Austin Schuh48d60c12017-02-04 21:58:58 -0800303
Ravago Jones5127ccc2022-07-31 16:32:45 -0700304 if FLAGS.plot:
305 scenario_plotter.Plot()
Austin Schuh48d60c12017-02-04 21:58:58 -0800306
Ravago Jones5127ccc2022-07-31 16:32:45 -0700307 # Write the generated constants out to a file.
308 if len(argv) != 5:
309 glog.fatal(
310 'Expected .h file name and .cc file name for the turret and integral turret.'
311 )
312 else:
313 namespaces = ['y2017', 'control_loops', 'superstructure', 'turret']
314 turret = Turret('Turret')
315 loop_writer = control_loop.ControlLoopWriter('Turret', [turret],
316 namespaces=namespaces)
317 loop_writer.AddConstant(
318 control_loop.Constant('kFreeSpeed', '%f', turret.free_speed))
319 loop_writer.AddConstant(
320 control_loop.Constant('kOutputRatio', '%f', turret.G))
321 loop_writer.Write(argv[1], argv[2])
Austin Schuh48d60c12017-02-04 21:58:58 -0800322
Ravago Jones5127ccc2022-07-31 16:32:45 -0700323 integral_turret = IntegralTurret('IntegralTurret')
324 integral_loop_writer = control_loop.ControlLoopWriter(
325 'IntegralTurret', [integral_turret], namespaces=namespaces)
326 integral_loop_writer.Write(argv[3], argv[4])
327
Austin Schuh48d60c12017-02-04 21:58:58 -0800328
329if __name__ == '__main__':
Ravago Jones5127ccc2022-07-31 16:32:45 -0700330 sys.exit(main(sys.argv))