blob: 7f46b2c8d357df0752697561e0121a5b58415721 [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:
15 gflags.DEFINE_bool('plot', False, 'If true, plot the loop response.')
16except gflags.DuplicateFlagError:
17 pass
18
19class Shoulder(control_loop.ControlLoop):
Austin Schuhf59b6bc2016-03-11 21:26:19 -080020 def __init__(self, name="Shoulder", J=None):
Comran Morshed2ae094e2016-01-23 20:43:20 +000021 super(Shoulder, self).__init__(name)
22 # TODO(constants): Update all of these & retune poles.
23 # Stall Torque in N m
Austin Schuha2dd8672016-01-31 12:18:40 -080024 self.stall_torque = 0.71
Comran Morshed2ae094e2016-01-23 20:43:20 +000025 # Stall Current in Amps
Austin Schuha2dd8672016-01-31 12:18:40 -080026 self.stall_current = 134
Comran Morshed2ae094e2016-01-23 20:43:20 +000027 # Free Speed in RPM
Austin Schuha2dd8672016-01-31 12:18:40 -080028 self.free_speed = 18730
Comran Morshed2ae094e2016-01-23 20:43:20 +000029 # Free Current in Amps
Austin Schuha2dd8672016-01-31 12:18:40 -080030 self.free_current = 0.7
Comran Morshed2ae094e2016-01-23 20:43:20 +000031
32 # Resistance of the motor
33 self.R = 12.0 / self.stall_current
34 # Motor velocity constant
35 self.Kv = ((self.free_speed / 60.0 * 2.0 * numpy.pi) /
36 (12.0 - self.R * self.free_current))
37 # Torque constant
38 self.Kt = self.stall_torque / self.stall_current
39 # Gear ratio
Comran Morshed367d8e52016-03-05 14:45:47 +000040 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 +000041
Austin Schuhf59b6bc2016-03-11 21:26:19 -080042 if J is None:
43 self.J = 10.0
44 else:
45 self.J = J
Comran Morshed2ae094e2016-01-23 20:43:20 +000046
47 # Control loop time step
48 self.dt = 0.005
49
50 # State is [position, velocity]
51 # Input is [Voltage]
52
53 C1 = self.G * self.G * self.Kt / (self.R * self.J * self.Kv)
54 C2 = self.Kt * self.G / (self.J * self.R)
55
56 self.A_continuous = numpy.matrix(
57 [[0, 1],
58 [0, -C1]])
59
60 # Start with the unmodified input
61 self.B_continuous = numpy.matrix(
62 [[0],
63 [C2]])
64
65 self.C = numpy.matrix([[1, 0]])
66 self.D = numpy.matrix([[0]])
67
68 self.A, self.B = self.ContinuousToDiscrete(
69 self.A_continuous, self.B_continuous, self.dt)
70
71 controllability = controls.ctrb(self.A, self.B)
72
Austin Schuhf59b6bc2016-03-11 21:26:19 -080073 q_pos = 0.16
Austin Schuh2fc178f2016-04-24 19:09:26 -070074 q_vel = 0.95
Comran Morshed2ae094e2016-01-23 20:43:20 +000075 self.Q = numpy.matrix([[(1.0 / (q_pos ** 2.0)), 0.0],
76 [0.0, (1.0 / (q_vel ** 2.0))]])
77
78 self.R = numpy.matrix([[(1.0 / (12.0 ** 2.0))]])
79 self.K = controls.dlqr(self.A, self.B, self.Q, self.R)
80
Austin Schuha88c4072016-02-06 14:31:03 -080081 self.Kff = controls.TwoStateFeedForwards(self.B, self.Q)
82
83 glog.debug('Poles are %s for %s',
84 repr(numpy.linalg.eig(self.A - self.B * self.K)[0]), self._name)
Comran Morshed2ae094e2016-01-23 20:43:20 +000085
Comran Morshed2ae094e2016-01-23 20:43:20 +000086 q_pos = 0.05
87 q_vel = 2.65
88 self.Q = numpy.matrix([[(q_pos ** 2.0), 0.0],
89 [0.0, (q_vel ** 2.0)]])
90
91 r_volts = 0.025
92 self.R = numpy.matrix([[(r_volts ** 2.0)]])
93
94 self.KalmanGain, self.Q_steady = controls.kalman(
95 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
96
Comran Morshed2ae094e2016-01-23 20:43:20 +000097 self.L = self.A * self.KalmanGain
Comran Morshed2ae094e2016-01-23 20:43:20 +000098
99 # The box formed by U_min and U_max must encompass all possible values,
100 # or else Austin's code gets angry.
101 self.U_max = numpy.matrix([[12.0]])
102 self.U_min = numpy.matrix([[-12.0]])
103
104 self.InitializeState()
105
106class IntegralShoulder(Shoulder):
Austin Schuha2dd8672016-01-31 12:18:40 -0800107 def __init__(self, name="IntegralShoulder"):
108 super(IntegralShoulder, self).__init__(name=name)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000109
110 self.A_continuous_unaugmented = self.A_continuous
111 self.B_continuous_unaugmented = self.B_continuous
112
113 self.A_continuous = numpy.matrix(numpy.zeros((3, 3)))
114 self.A_continuous[0:2, 0:2] = self.A_continuous_unaugmented
115 self.A_continuous[0:2, 2] = self.B_continuous_unaugmented
116
117 self.B_continuous = numpy.matrix(numpy.zeros((3, 1)))
118 self.B_continuous[0:2, 0] = self.B_continuous_unaugmented
119
120 self.C_unaugmented = self.C
121 self.C = numpy.matrix(numpy.zeros((1, 3)))
122 self.C[0:1, 0:2] = self.C_unaugmented
123
124 self.A, self.B = self.ContinuousToDiscrete(self.A_continuous, self.B_continuous, self.dt)
125
126 q_pos = 0.08
127 q_vel = 4.00
128 q_voltage = 6.0
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)]])
132
133 r_pos = 0.05
134 self.R = numpy.matrix([[(r_pos ** 2.0)]])
135
136 self.KalmanGain, self.Q_steady = controls.kalman(
137 A=self.A, B=self.B, C=self.C, Q=self.Q, R=self.R)
138 self.L = self.A * self.KalmanGain
139
140 self.K_unaugmented = self.K
141 self.K = numpy.matrix(numpy.zeros((1, 3)))
142 self.K[0, 0:2] = self.K_unaugmented
143 self.K[0, 2] = 1
Austin Schuha88c4072016-02-06 14:31:03 -0800144 self.Kff_unaugmented = self.Kff
145 self.Kff = numpy.matrix(numpy.zeros((1, 3)))
146 self.Kff[0, 0:2] = self.Kff_unaugmented
Comran Morshed2ae094e2016-01-23 20:43:20 +0000147
148 self.InitializeState()
Austin Schuha2dd8672016-01-31 12:18:40 -0800149
Comran Morshed2ae094e2016-01-23 20:43:20 +0000150class ScenarioPlotter(object):
151 def __init__(self):
152 # Various lists for graphing things.
153 self.t = []
154 self.x = []
155 self.v = []
156 self.a = []
157 self.x_hat = []
158 self.u = []
Austin Schuha2dd8672016-01-31 12:18:40 -0800159 self.offset = []
Comran Morshed2ae094e2016-01-23 20:43:20 +0000160
161 def run_test(self, shoulder, goal, iterations=200, controller_shoulder=None,
162 observer_shoulder=None):
163 """Runs the shoulder plant with an initial condition and goal.
164
165 Test for whether the goal has been reached and whether the separation
166 goes outside of the initial and goal values by more than
167 max_separation_error.
168
169 Prints out something for a failure of either condition and returns
170 False if tests fail.
171 Args:
172 shoulder: shoulder object to use.
173 goal: goal state.
174 iterations: Number of timesteps to run the model for.
175 controller_shoulder: Shoulder object to get K from, or None if we should
176 use shoulder.
177 observer_shoulder: Shoulder object to use for the observer, or None if we should
178 use the actual state.
179 """
180
181 if controller_shoulder is None:
182 controller_shoulder = shoulder
183
184 vbat = 12.0
185
186 if self.t:
187 initial_t = self.t[-1] + shoulder.dt
188 else:
189 initial_t = 0
190
Austin Schuh5ea48472021-02-02 20:46:41 -0800191 for i in range(iterations):
Comran Morshed2ae094e2016-01-23 20:43:20 +0000192 X_hat = shoulder.X
193
194 if observer_shoulder is not None:
195 X_hat = observer_shoulder.X_hat
196 self.x_hat.append(observer_shoulder.X_hat[0, 0])
197
198 U = controller_shoulder.K * (goal - X_hat)
199 U[0, 0] = numpy.clip(U[0, 0], -vbat, vbat)
200 self.x.append(shoulder.X[0, 0])
201
202 if self.v:
203 last_v = self.v[-1]
204 else:
205 last_v = 0
206
207 self.v.append(shoulder.X[1, 0])
208 self.a.append((self.v[-1] - last_v) / shoulder.dt)
209
210 if observer_shoulder is not None:
211 observer_shoulder.Y = shoulder.Y
212 observer_shoulder.CorrectObserver(U)
Austin Schuha2dd8672016-01-31 12:18:40 -0800213 self.offset.append(observer_shoulder.X_hat[2, 0])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000214
Austin Schuha2dd8672016-01-31 12:18:40 -0800215 shoulder.Update(U + 2.0)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000216
217 if observer_shoulder is not None:
218 observer_shoulder.PredictObserver(U)
219
220 self.t.append(initial_t + i * shoulder.dt)
221 self.u.append(U[0, 0])
222
223 glog.debug('Time: %f', self.t[-1])
224
225 def Plot(self):
226 pylab.subplot(3, 1, 1)
227 pylab.plot(self.t, self.x, label='x')
228 pylab.plot(self.t, self.x_hat, label='x_hat')
229 pylab.legend()
230
231 pylab.subplot(3, 1, 2)
232 pylab.plot(self.t, self.u, label='u')
Austin Schuha2dd8672016-01-31 12:18:40 -0800233 pylab.plot(self.t, self.offset, label='voltage_offset')
234 pylab.legend()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000235
236 pylab.subplot(3, 1, 3)
237 pylab.plot(self.t, self.a, label='a')
Comran Morshed2ae094e2016-01-23 20:43:20 +0000238 pylab.legend()
Austin Schuha2dd8672016-01-31 12:18:40 -0800239
Comran Morshed2ae094e2016-01-23 20:43:20 +0000240 pylab.show()
241
242
243def main(argv):
244 argv = FLAGS(argv)
245
Comran Morshed2ae094e2016-01-23 20:43:20 +0000246 scenario_plotter = ScenarioPlotter()
247
Austin Schuha2dd8672016-01-31 12:18:40 -0800248 shoulder = Shoulder()
249 shoulder_controller = IntegralShoulder()
250 observer_shoulder = IntegralShoulder()
Comran Morshed2ae094e2016-01-23 20:43:20 +0000251
252 # Test moving the shoulder with constant separation.
253 initial_X = numpy.matrix([[0.0], [0.0]])
Austin Schuha2dd8672016-01-31 12:18:40 -0800254 R = numpy.matrix([[numpy.pi / 2.0], [0.0], [0.0]])
Comran Morshed2ae094e2016-01-23 20:43:20 +0000255 scenario_plotter.run_test(shoulder, goal=R, controller_shoulder=shoulder_controller,
256 observer_shoulder=observer_shoulder, iterations=200)
257
258 if FLAGS.plot:
259 scenario_plotter.Plot()
260
261 # Write the generated constants out to a file.
262 if len(argv) != 5:
263 glog.fatal('Expected .h file name and .cc file name for the shoulder and integral shoulder.')
264 else:
265 namespaces = ['y2016', 'control_loops', 'superstructure']
266 shoulder = Shoulder("Shoulder")
267 loop_writer = control_loop.ControlLoopWriter('Shoulder', [shoulder],
268 namespaces=namespaces)
269 loop_writer.Write(argv[1], argv[2])
270
Austin Schuha2dd8672016-01-31 12:18:40 -0800271 integral_shoulder = IntegralShoulder("IntegralShoulder")
Comran Morshed2ae094e2016-01-23 20:43:20 +0000272 integral_loop_writer = control_loop.ControlLoopWriter("IntegralShoulder", [integral_shoulder],
Austin Schuha2dd8672016-01-31 12:18:40 -0800273 namespaces=namespaces)
Comran Morshed2ae094e2016-01-23 20:43:20 +0000274 integral_loop_writer.Write(argv[3], argv[4])
275
276if __name__ == '__main__':
277 sys.exit(main(sys.argv))