blob: d4060897442e353e546be2abf28a08172ce75eb9 [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
Comran Morshed2ae094e2016-01-23 20:43:20 +00002
Comran Morshed2ae094e2016-01-23 20:43:20 +00003import numpy
4import sys
Austin Schuha88c4072016-02-06 14:31:03 -08005
6from frc971.control_loops.python import control_loop
7from frc971.control_loops.python import controls
Comran Morshed2ae094e2016-01-23 20:43:20 +00008from 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.')
Comran Morshed2ae094e2016-01-23 20:43:20 +000016except gflags.DuplicateFlagError:
Ravago Jones5127ccc2022-07-31 16:32:45 -070017 pass
18
Comran Morshed2ae094e2016-01-23 20:43:20 +000019
20class Shoulder(control_loop.ControlLoop):
Comran Morshed2ae094e2016-01-23 20:43:20 +000021
Ravago Jones5127ccc2022-07-31 16:32:45 -070022 def __init__(self, name="Shoulder", J=None):
23 super(Shoulder, self).__init__(name)
24 # TODO(constants): Update all of these & retune poles.
25 # Stall Torque in N m
26 self.stall_torque = 0.71
27 # Stall Current in Amps
28 self.stall_current = 134
29 # Free Speed in RPM
30 self.free_speed = 18730
31 # Free Current in Amps
32 self.free_current = 0.7
Comran Morshed2ae094e2016-01-23 20:43:20 +000033
Ravago Jones5127ccc2022-07-31 16:32:45 -070034 # Resistance of the motor
35 self.R = 12.0 / self.stall_current
36 # Motor velocity constant
37 self.Kv = ((self.free_speed / 60.0 * 2.0 * numpy.pi) /
38 (12.0 - self.R * self.free_current))
39 # Torque constant
40 self.Kt = self.stall_torque / self.stall_current
41 # Gear ratio
42 self.G = (56.0 / 12.0) * (64.0 / 14.0) * (72.0 / 18.0) * (42.0 / 12.0)
Comran Morshed2ae094e2016-01-23 20:43:20 +000043
Ravago Jones5127ccc2022-07-31 16:32:45 -070044 if J is None:
45 self.J = 10.0
46 else:
47 self.J = J
Comran Morshed2ae094e2016-01-23 20:43:20 +000048
Ravago Jones5127ccc2022-07-31 16:32:45 -070049 # Control loop time step
50 self.dt = 0.005
Comran Morshed2ae094e2016-01-23 20:43:20 +000051
Ravago Jones5127ccc2022-07-31 16:32:45 -070052 # State is [position, velocity]
53 # Input is [Voltage]
Comran Morshed2ae094e2016-01-23 20:43:20 +000054
Ravago Jones5127ccc2022-07-31 16:32:45 -070055 C1 = self.G * self.G * self.Kt / (self.R * self.J * self.Kv)
56 C2 = self.Kt * self.G / (self.J * self.R)
Comran Morshed2ae094e2016-01-23 20:43:20 +000057
Ravago Jones5127ccc2022-07-31 16:32:45 -070058 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000059
Ravago Jones5127ccc2022-07-31 16:32:45 -070060 # Start with the unmodified input
61 self.B_continuous = numpy.matrix([[0], [C2]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000062
Ravago Jones5127ccc2022-07-31 16:32:45 -070063 self.C = numpy.matrix([[1, 0]])
64 self.D = numpy.matrix([[0]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000065
Ravago Jones5127ccc2022-07-31 16:32:45 -070066 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
67 self.B_continuous, self.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +000068
Ravago Jones5127ccc2022-07-31 16:32:45 -070069 controllability = controls.ctrb(self.A, self.B)
Comran Morshed2ae094e2016-01-23 20:43:20 +000070
Ravago Jones5127ccc2022-07-31 16:32:45 -070071 q_pos = 0.16
72 q_vel = 0.95
73 self.Q = numpy.matrix([[(1.0 / (q_pos**2.0)), 0.0],
74 [0.0, (1.0 / (q_vel**2.0))]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000075
Ravago Jones5127ccc2022-07-31 16:32:45 -070076 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
77 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
Austin Schuha88c4072016-02-06 14:31:03 -080078
Ravago Jones5127ccc2022-07-31 16:32:45 -070079 self.Kff = controls.TwoStateFeedForwards(self.B, self.Q)
Comran Morshed2ae094e2016-01-23 20:43:20 +000080
Ravago Jones5127ccc2022-07-31 16:32:45 -070081 glog.debug('Poles are %s for %s',
82 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]),
83 self._name)
Comran Morshed2ae094e2016-01-23 20:43:20 +000084
Ravago Jones5127ccc2022-07-31 16:32:45 -070085 q_pos = 0.05
86 q_vel = 2.65
87 self.Q = numpy.matrix([[(q_pos**2.0), 0.0], [0.0, (q_vel**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000088
Ravago Jones5127ccc2022-07-31 16:32:45 -070089 r_volts = 0.025
90 self.R = numpy.matrix([[(r_volts**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000091
Ravago Jones5127ccc2022-07-31 16:32:45 -070092 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
93 B=self.B,
94 C=self.C,
95 Q=self.Q,
96 R=self.R)
Comran Morshed2ae094e2016-01-23 20:43:20 +000097
Ravago Jones5127ccc2022-07-31 16:32:45 -070098 self.L = self.A * self.KalmanGain
Comran Morshed2ae094e2016-01-23 20:43:20 +000099
Ravago Jones5127ccc2022-07-31 16:32:45 -0700100 # The box formed by U_min and U_max must encompass all possible values,
101 # or else Austin's code gets angry.
102 self.U_max = numpy.matrix([[12.0]])
103 self.U_min = numpy.matrix([[-12.0]])
104
105 self.InitializeState()
106
Comran Morshed2ae094e2016-01-23 20:43:20 +0000107
108class IntegralShoulder(Shoulder):
Comran Morshed2ae094e2016-01-23 20:43:20 +0000109
Ravago Jones5127ccc2022-07-31 16:32:45 -0700110 def __init__(self, name="IntegralShoulder"):
111 super(IntegralShoulder, self).__init__(name=name)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000112
Ravago Jones5127ccc2022-07-31 16:32:45 -0700113 self.A_continuous_unaugmented = self.A_continuous
114 self.B_continuous_unaugmented = self.B_continuous
Comran Morshed2ae094e2016-01-23 20:43:20 +0000115
Ravago Jones5127ccc2022-07-31 16:32:45 -0700116 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
117 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
118 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000119
Ravago Jones5127ccc2022-07-31 16:32:45 -0700120 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
121 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000122
Ravago Jones5127ccc2022-07-31 16:32:45 -0700123 self.C_unaugmented = self.C
124 self.C = numpy.matrix(numpy.zeros((1, 3)))
125 self.C[0:1, 0:2] = self.C_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000126
Ravago Jones5127ccc2022-07-31 16:32:45 -0700127 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
128 self.B_continuous, self.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000129
Ravago Jones5127ccc2022-07-31 16:32:45 -0700130 q_pos = 0.08
131 q_vel = 4.00
132 q_voltage = 6.0
133 self.Q = numpy.matrix([[(q_pos**2.0), 0.0, 0.0],
134 [0.0, (q_vel**2.0), 0.0],
135 [0.0, 0.0, (q_voltage**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000136
Ravago Jones5127ccc2022-07-31 16:32:45 -0700137 r_pos = 0.05
138 self.R = numpy.matrix([[(r_pos**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000139
Ravago Jones5127ccc2022-07-31 16:32:45 -0700140 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
141 B=self.B,
142 C=self.C,
143 Q=self.Q,
144 R=self.R)
145 self.L = self.A * self.KalmanGain
Comran Morshed2ae094e2016-01-23 20:43:20 +0000146
Ravago Jones5127ccc2022-07-31 16:32:45 -0700147 self.K_unaugmented = self.K
148 self.K = numpy.matrix(numpy.zeros((1, 3)))
149 self.K[0, 0:2] = self.K_unaugmented
150 self.K[0, 2] = 1
151 self.Kff_unaugmented = self.Kff
152 self.Kff = numpy.matrix(numpy.zeros((1, 3)))
153 self.Kff[0, 0:2] = self.Kff_unaugmented
154
155 self.InitializeState()
156
Austin Schuha2dd8672016-01-31 12:18:40 -0800157
Comran Morshed2ae094e2016-01-23 20:43:20 +0000158class ScenarioPlotter(object):
Comran Morshed2ae094e2016-01-23 20:43:20 +0000159
Ravago Jones5127ccc2022-07-31 16:32:45 -0700160 def __init__(self):
161 # Various lists for graphing things.
162 self.t = []
163 self.x = []
164 self.v = []
165 self.a = []
166 self.x_hat = []
167 self.u = []
168 self.offset = []
169
170 def run_test(self,
171 shoulder,
172 goal,
173 iterations=200,
174 controller_shoulder=None,
175 observer_shoulder=None):
176 """Runs the shoulder plant with an initial condition and goal.
Comran Morshed2ae094e2016-01-23 20:43:20 +0000177
178 Test for whether the goal has been reached and whether the separation
179 goes outside of the initial and goal values by more than
180 max_separation_error.
181
182 Prints out something for a failure of either condition and returns
183 False if tests fail.
184 Args:
185 shoulder: shoulder object to use.
186 goal: goal state.
187 iterations: Number of timesteps to run the model for.
188 controller_shoulder: Shoulder object to get K from, or None if we should
189 use shoulder.
190 observer_shoulder: Shoulder object to use for the observer, or None if we should
191 use the actual state.
192 """
193
Ravago Jones5127ccc2022-07-31 16:32:45 -0700194 if controller_shoulder is None:
195 controller_shoulder = shoulder
Comran Morshed2ae094e2016-01-23 20:43:20 +0000196
Ravago Jones5127ccc2022-07-31 16:32:45 -0700197 vbat = 12.0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000198
Ravago Jones5127ccc2022-07-31 16:32:45 -0700199 if self.t:
200 initial_t = self.t[-1] + shoulder.dt
201 else:
202 initial_t = 0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000203
Ravago Jones5127ccc2022-07-31 16:32:45 -0700204 for i in range(iterations):
205 X_hat = shoulder.X
Comran Morshed2ae094e2016-01-23 20:43:20 +0000206
Ravago Jones5127ccc2022-07-31 16:32:45 -0700207 if observer_shoulder is not None:
208 X_hat = observer_shoulder.X_hat
209 self.x_hat.append(observer_shoulder.X_hat[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000210
Ravago Jones5127ccc2022-07-31 16:32:45 -0700211 U = controller_shoulder.K * (goal - X_hat)
212 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
213 self.x.append(shoulder.X[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000214
Ravago Jones5127ccc2022-07-31 16:32:45 -0700215 if self.v:
216 last_v = self.v[-1]
217 else:
218 last_v = 0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000219
Ravago Jones5127ccc2022-07-31 16:32:45 -0700220 self.v.append(shoulder.X[1, 0])
221 self.a.append((self.v[-1] - last_v) / shoulder.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000222
Ravago Jones5127ccc2022-07-31 16:32:45 -0700223 if observer_shoulder is not None:
224 observer_shoulder.Y = shoulder.Y
225 observer_shoulder.CorrectObserver(U)
226 self.offset.append(observer_shoulder.X_hat[2, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000227
Ravago Jones5127ccc2022-07-31 16:32:45 -0700228 shoulder.Update(U + 2.0)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000229
Ravago Jones5127ccc2022-07-31 16:32:45 -0700230 if observer_shoulder is not None:
231 observer_shoulder.PredictObserver(U)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000232
Ravago Jones5127ccc2022-07-31 16:32:45 -0700233 self.t.append(initial_t + i * shoulder.dt)
234 self.u.append(U[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000235
Ravago Jones5127ccc2022-07-31 16:32:45 -0700236 glog.debug('Time: %f', self.t[-1])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000237
Ravago Jones5127ccc2022-07-31 16:32:45 -0700238 def Plot(self):
239 pylab.subplot(3, 1, 1)
240 pylab.plot(self.t, self.x, label='x')
241 pylab.plot(self.t, self.x_hat, label='x_hat')
242 pylab.legend()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000243
Ravago Jones5127ccc2022-07-31 16:32:45 -0700244 pylab.subplot(3, 1, 2)
245 pylab.plot(self.t, self.u, label='u')
246 pylab.plot(self.t, self.offset, label='voltage_offset')
247 pylab.legend()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000248
Ravago Jones5127ccc2022-07-31 16:32:45 -0700249 pylab.subplot(3, 1, 3)
250 pylab.plot(self.t, self.a, label='a')
251 pylab.legend()
Austin Schuha2dd8672016-01-31 12:18:40 -0800252
Ravago Jones5127ccc2022-07-31 16:32:45 -0700253 pylab.show()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000254
255
256def main(argv):
Ravago Jones5127ccc2022-07-31 16:32:45 -0700257 argv = FLAGS(argv)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000258
Ravago Jones5127ccc2022-07-31 16:32:45 -0700259 scenario_plotter = ScenarioPlotter()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000260
Ravago Jones5127ccc2022-07-31 16:32:45 -0700261 shoulder = Shoulder()
262 shoulder_controller = IntegralShoulder()
263 observer_shoulder = IntegralShoulder()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000264
Ravago Jones5127ccc2022-07-31 16:32:45 -0700265 # Test moving the shoulder with constant separation.
266 initial_X = numpy.matrix([[0.0], [0.0]])
267 R = numpy.matrix([[numpy.pi / 2.0], [0.0], [0.0]])
268 scenario_plotter.run_test(shoulder,
269 goal=R,
270 controller_shoulder=shoulder_controller,
271 observer_shoulder=observer_shoulder,
272 iterations=200)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000273
Ravago Jones5127ccc2022-07-31 16:32:45 -0700274 if FLAGS.plot:
275 scenario_plotter.Plot()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000276
Ravago Jones5127ccc2022-07-31 16:32:45 -0700277 # Write the generated constants out to a file.
278 if len(argv) != 5:
279 glog.fatal(
280 'Expected .h file name and .cc file name for the shoulder and integral shoulder.'
281 )
282 else:
283 namespaces = ['y2016', 'control_loops', 'superstructure']
284 shoulder = Shoulder("Shoulder")
285 loop_writer = control_loop.ControlLoopWriter('Shoulder', [shoulder],
286 namespaces=namespaces)
287 loop_writer.Write(argv[1], argv[2])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000288
Ravago Jones5127ccc2022-07-31 16:32:45 -0700289 integral_shoulder = IntegralShoulder("IntegralShoulder")
290 integral_loop_writer = control_loop.ControlLoopWriter(
291 "IntegralShoulder", [integral_shoulder], namespaces=namespaces)
292 integral_loop_writer.Write(argv[3], argv[4])
293
Comran Morshed2ae094e2016-01-23 20:43:20 +0000294
295if __name__ == '__main__':
Ravago Jones5127ccc2022-07-31 16:32:45 -0700296 sys.exit(main(sys.argv))