blob: 12dcc3910047207141e451edb89f97af216d2a97 [file] [log] [blame]
Austin Schuh085eab92020-11-26 13:54:51 -08001#!/usr/bin/python3
Comran Morshed2ae094e2016-01-23 20:43:20 +00002
3from frc971.control_loops.python import control_loop
4from frc971.control_loops.python import controls
Comran Morshed2ae094e2016-01-23 20:43:20 +00005import numpy
6import sys
Comran Morshed2ae094e2016-01-23 20:43:20 +00007from matplotlib import pylab
8import gflags
9import glog
10
11FLAGS = gflags.FLAGS
12
13try:
Ravago Jones5127ccc2022-07-31 16:32:45 -070014 gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
Comran Morshed2ae094e2016-01-23 20:43:20 +000015except gflags.DuplicateFlagError:
Ravago Jones5127ccc2022-07-31 16:32:45 -070016 pass
17
Comran Morshed2ae094e2016-01-23 20:43:20 +000018
19class Wrist(control_loop.ControlLoop):
Comran Morshed2ae094e2016-01-23 20:43:20 +000020
Ravago Jones5127ccc2022-07-31 16:32:45 -070021 def __init__(self, name="Wrist"):
22 super(Wrist, self).__init__(name)
23 # TODO(constants): Update all of these & retune poles.
24 # Stall Torque in N m
25 self.stall_torque = 0.71
26 # Stall Current in Amps
27 self.stall_current = 134
28 # Free Speed in RPM
29 self.free_speed = 18730
30 # Free Current in Amps
31 self.free_current = 0.7
Comran Morshed2ae094e2016-01-23 20:43:20 +000032
Ravago Jones5127ccc2022-07-31 16:32:45 -070033 # Resistance of the motor
34 self.R = 12.0 / self.stall_current
35 # Motor velocity constant
36 self.Kv = ((self.free_speed / 60.0 * 2.0 * numpy.pi) /
37 (12.0 - self.R * self.free_current))
38 # Torque constant
39 self.Kt = self.stall_torque / self.stall_current
40 # Gear ratio
41 self.G = (56.0 / 12.0) * (54.0 / 14.0) * (64.0 / 18.0) * (48.0 / 16.0)
Comran Morshed2ae094e2016-01-23 20:43:20 +000042
Ravago Jones5127ccc2022-07-31 16:32:45 -070043 self.J = 0.35
Comran Morshed2ae094e2016-01-23 20:43:20 +000044
Ravago Jones5127ccc2022-07-31 16:32:45 -070045 # Control loop time step
46 self.dt = 0.005
Comran Morshed2ae094e2016-01-23 20:43:20 +000047
Ravago Jones5127ccc2022-07-31 16:32:45 -070048 # State is [position, velocity]
49 # Input is [Voltage]
Comran Morshed2ae094e2016-01-23 20:43:20 +000050
Ravago Jones5127ccc2022-07-31 16:32:45 -070051 C1 = self.G * self.G * self.Kt / (self.R * self.J * self.Kv)
52 C2 = self.Kt * self.G / (self.J * self.R)
Comran Morshed2ae094e2016-01-23 20:43:20 +000053
Ravago Jones5127ccc2022-07-31 16:32:45 -070054 self.A_continuous = numpy.matrix([[0, 1], [0, -C1]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000055
Ravago Jones5127ccc2022-07-31 16:32:45 -070056 # Start with the unmodified input
57 self.B_continuous = numpy.matrix([[0], [C2]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000058
Ravago Jones5127ccc2022-07-31 16:32:45 -070059 self.C = numpy.matrix([[1, 0]])
60 self.D = numpy.matrix([[0]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000061
Ravago Jones5127ccc2022-07-31 16:32:45 -070062 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
63 self.B_continuous, self.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +000064
Ravago Jones5127ccc2022-07-31 16:32:45 -070065 controllability = controls.ctrb(self.A, self.B)
Comran Morshed2ae094e2016-01-23 20:43:20 +000066
Ravago Jones5127ccc2022-07-31 16:32:45 -070067 q_pos = 0.20
68 q_vel = 8.0
69 self.Q = numpy.matrix([[(1.0 / (q_pos**2.0)), 0.0],
70 [0.0, (1.0 / (q_vel**2.0))]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000071
Ravago Jones5127ccc2022-07-31 16:32:45 -070072 self.R = numpy.matrix([[(1.0 / (12.0**2.0))]])
73 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
Comran Morshed2ae094e2016-01-23 20:43:20 +000074
Ravago Jones5127ccc2022-07-31 16:32:45 -070075 glog.debug('Poles are %s for %s',
76 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]),
77 self._name)
Comran Morshed2ae094e2016-01-23 20:43:20 +000078
Ravago Jones5127ccc2022-07-31 16:32:45 -070079 q_pos = 0.05
80 q_vel = 2.65
81 self.Q = numpy.matrix([[(q_pos**2.0), 0.0], [0.0, (q_vel**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000082
Ravago Jones5127ccc2022-07-31 16:32:45 -070083 r_volts = 0.025
84 self.R = numpy.matrix([[(r_volts**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +000085
Ravago Jones5127ccc2022-07-31 16:32:45 -070086 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
87 B=self.B,
88 C=self.C,
89 Q=self.Q,
90 R=self.R)
Comran Morshed2ae094e2016-01-23 20:43:20 +000091
Ravago Jones5127ccc2022-07-31 16:32:45 -070092 self.L = self.A * self.KalmanGain
Comran Morshed2ae094e2016-01-23 20:43:20 +000093
Ravago Jones5127ccc2022-07-31 16:32:45 -070094 # The box formed by U_min and U_max must encompass all possible values,
95 # or else Austin's code gets angry.
96 self.U_max = numpy.matrix([[12.0]])
97 self.U_min = numpy.matrix([[-12.0]])
Austin Schuha88c4072016-02-06 14:31:03 -080098
Ravago Jones5127ccc2022-07-31 16:32:45 -070099 self.Kff = controls.TwoStateFeedForwards(self.B, self.Q)
100
101 self.InitializeState()
102
Comran Morshed2ae094e2016-01-23 20:43:20 +0000103
104class IntegralWrist(Wrist):
Comran Morshed2ae094e2016-01-23 20:43:20 +0000105
Ravago Jones5127ccc2022-07-31 16:32:45 -0700106 def __init__(self, name="IntegralWrist"):
107 super(IntegralWrist, self).__init__(name=name)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000108
Ravago Jones5127ccc2022-07-31 16:32:45 -0700109 self.A_continuous_unaugmented = self.A_continuous
110 self.B_continuous_unaugmented = self.B_continuous
Comran Morshed2ae094e2016-01-23 20:43:20 +0000111
Ravago Jones5127ccc2022-07-31 16:32:45 -0700112 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
113 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
114 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000115
Ravago Jones5127ccc2022-07-31 16:32:45 -0700116 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
117 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000118
Ravago Jones5127ccc2022-07-31 16:32:45 -0700119 self.C_unaugmented = self.C
120 self.C = numpy.matrix(numpy.zeros((1, 3)))
121 self.C[0:1, 0:2] = self.C_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000122
Ravago Jones5127ccc2022-07-31 16:32:45 -0700123 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous,
124 self.B_continuous, self.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000125
Ravago Jones5127ccc2022-07-31 16:32:45 -0700126 q_pos = 0.08
127 q_vel = 4.00
128 q_voltage = 1.5
129 self.Q = numpy.matrix([[(q_pos**2.0), 0.0, 0.0],
130 [0.0, (q_vel**2.0), 0.0],
131 [0.0, 0.0, (q_voltage**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000132
Ravago Jones5127ccc2022-07-31 16:32:45 -0700133 r_pos = 0.05
134 self.R = numpy.matrix([[(r_pos**2.0)]])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000135
Ravago Jones5127ccc2022-07-31 16:32:45 -0700136 self.KalmanGain, self.Q_steady = controls.kalman(A=self.A,
137 B=self.B,
138 C=self.C,
139 Q=self.Q,
140 R=self.R)
141 self.L = self.A * self.KalmanGain
Comran Morshed2ae094e2016-01-23 20:43:20 +0000142
Ravago Jones5127ccc2022-07-31 16:32:45 -0700143 self.K_unaugmented = self.K
144 self.K = numpy.matrix(numpy.zeros((1, 3)))
145 self.K[0, 0:2] = self.K_unaugmented
146 self.K[0, 2] = 1
147 self.Kff_unaugmented = self.Kff
148 self.Kff = numpy.matrix(numpy.zeros((1, 3)))
149 self.Kff[0, 0:2] = self.Kff_unaugmented
150
151 self.InitializeState()
152
Austin Schuhf0c0b7f2016-01-31 00:29:13 -0800153
Comran Morshed2ae094e2016-01-23 20:43:20 +0000154class ScenarioPlotter(object):
Comran Morshed2ae094e2016-01-23 20:43:20 +0000155
Ravago Jones5127ccc2022-07-31 16:32:45 -0700156 def __init__(self):
157 # Various lists for graphing things.
158 self.t = []
159 self.x = []
160 self.v = []
161 self.a = []
162 self.x_hat = []
163 self.u = []
164 self.offset = []
165
166 def run_test(self,
167 wrist,
168 goal,
169 iterations=200,
170 controller_wrist=None,
171 observer_wrist=None):
172 """Runs the wrist plant with an initial condition and goal.
Comran Morshed2ae094e2016-01-23 20:43:20 +0000173
174 Test for whether the goal has been reached and whether the separation
175 goes outside of the initial and goal values by more than
176 max_separation_error.
177
178 Prints out something for a failure of either condition and returns
179 False if tests fail.
180 Args:
181 wrist: wrist object to use.
182 goal: goal state.
183 iterations: Number of timesteps to run the model for.
184 controller_wrist: Wrist object to get K from, or None if we should
185 use wrist.
186 observer_wrist: Wrist object to use for the observer, or None if we should
187 use the actual state.
188 """
189
Ravago Jones5127ccc2022-07-31 16:32:45 -0700190 if controller_wrist is None:
191 controller_wrist = wrist
Comran Morshed2ae094e2016-01-23 20:43:20 +0000192
Ravago Jones5127ccc2022-07-31 16:32:45 -0700193 vbat = 12.0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000194
Ravago Jones5127ccc2022-07-31 16:32:45 -0700195 if self.t:
196 initial_t = self.t[-1] + wrist.dt
197 else:
198 initial_t = 0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000199
Ravago Jones5127ccc2022-07-31 16:32:45 -0700200 for i in range(iterations):
201 X_hat = wrist.X
Comran Morshed2ae094e2016-01-23 20:43:20 +0000202
Ravago Jones5127ccc2022-07-31 16:32:45 -0700203 if observer_wrist is not None:
204 X_hat = observer_wrist.X_hat
205 self.x_hat.append(observer_wrist.X_hat[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000206
Ravago Jones5127ccc2022-07-31 16:32:45 -0700207 U = controller_wrist.K * (goal - X_hat)
208 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
209 self.x.append(wrist.X[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000210
Ravago Jones5127ccc2022-07-31 16:32:45 -0700211 if self.v:
212 last_v = self.v[-1]
213 else:
214 last_v = 0
Comran Morshed2ae094e2016-01-23 20:43:20 +0000215
Ravago Jones5127ccc2022-07-31 16:32:45 -0700216 self.v.append(wrist.X[1, 0])
217 self.a.append((self.v[-1] - last_v) / wrist.dt)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000218
Ravago Jones5127ccc2022-07-31 16:32:45 -0700219 if observer_wrist is not None:
220 observer_wrist.Y = wrist.Y
221 observer_wrist.CorrectObserver(U)
222 self.offset.append(observer_wrist.X_hat[2, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000223
Ravago Jones5127ccc2022-07-31 16:32:45 -0700224 wrist.Update(U + 2.0)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000225
Ravago Jones5127ccc2022-07-31 16:32:45 -0700226 if observer_wrist is not None:
227 observer_wrist.PredictObserver(U)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000228
Ravago Jones5127ccc2022-07-31 16:32:45 -0700229 self.t.append(initial_t + i * wrist.dt)
230 self.u.append(U[0, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000231
Ravago Jones5127ccc2022-07-31 16:32:45 -0700232 glog.debug('Time: %f', self.t[-1])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000233
Ravago Jones5127ccc2022-07-31 16:32:45 -0700234 def Plot(self):
235 pylab.subplot(3, 1, 1)
236 pylab.plot(self.t, self.x, label='x')
237 pylab.plot(self.t, self.x_hat, label='x_hat')
238 pylab.legend()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000239
Ravago Jones5127ccc2022-07-31 16:32:45 -0700240 pylab.subplot(3, 1, 2)
241 pylab.plot(self.t, self.u, label='u')
242 pylab.plot(self.t, self.offset, label='voltage_offset')
243 pylab.legend()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000244
Ravago Jones5127ccc2022-07-31 16:32:45 -0700245 pylab.subplot(3, 1, 3)
246 pylab.plot(self.t, self.a, label='a')
247 pylab.legend()
Austin Schuhf0c0b7f2016-01-31 00:29:13 -0800248
Ravago Jones5127ccc2022-07-31 16:32:45 -0700249 pylab.show()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000250
251
252def main(argv):
Ravago Jones5127ccc2022-07-31 16:32:45 -0700253 scenario_plotter = ScenarioPlotter()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000254
Ravago Jones5127ccc2022-07-31 16:32:45 -0700255 wrist = Wrist()
256 wrist_controller = IntegralWrist()
257 observer_wrist = IntegralWrist()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000258
Ravago Jones5127ccc2022-07-31 16:32:45 -0700259 # Test moving the wrist with constant separation.
260 initial_X = numpy.matrix([[0.0], [0.0]])
261 R = numpy.matrix([[1.0], [0.0], [0.0]])
262 scenario_plotter.run_test(wrist,
263 goal=R,
264 controller_wrist=wrist_controller,
265 observer_wrist=observer_wrist,
266 iterations=200)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000267
Ravago Jones5127ccc2022-07-31 16:32:45 -0700268 if FLAGS.plot:
269 scenario_plotter.Plot()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000270
Ravago Jones5127ccc2022-07-31 16:32:45 -0700271 # Write the generated constants out to a file.
272 if len(argv) != 5:
273 glog.fatal(
274 'Expected .h file name and .cc file name for the wrist and integral wrist.'
275 )
276 else:
277 namespaces = ['y2016', 'control_loops', 'superstructure']
278 wrist = Wrist('Wrist')
279 loop_writer = control_loop.ControlLoopWriter('Wrist', [wrist],
280 namespaces=namespaces)
281 loop_writer.Write(argv[1], argv[2])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000282
Ravago Jones5127ccc2022-07-31 16:32:45 -0700283 integral_wrist = IntegralWrist('IntegralWrist')
284 integral_loop_writer = control_loop.ControlLoopWriter(
285 'IntegralWrist', [integral_wrist], namespaces=namespaces)
286 integral_loop_writer.Write(argv[3], argv[4])
287
Comran Morshed2ae094e2016-01-23 20:43:20 +0000288
289if __name__ == '__main__':
Ravago Jones5127ccc2022-07-31 16:32:45 -0700290 argv = FLAGS(sys.argv)
291 sys.exit(main(argv))