blob: 12367894fa52706dde134c816b92c5a6996c950c [file] [log] [blame]
Austin Schuh2a1abec2024-07-10 20:31:16 -07001#!/usr/bin/python3
2
3import numpy
4import sys, os
5import casadi
6from numpy.testing import assert_array_equal, assert_array_almost_equal
7import unittest
8
9from frc971.control_loops.swerve import dynamics
10
11
12def state_vector(velocity=numpy.array([[1.0], [0.0]]),
13 dx=0.0,
14 dy=0.0,
15 theta=0.0,
16 omega=0.0,
17 module_omega=0.0,
Austin Schuhb8b34be2024-07-14 16:06:19 -070018 module_angle=0.0,
19 drive_wheel_velocity=None,
20 module_angles=None):
Austin Schuh2a1abec2024-07-10 20:31:16 -070021 """Returns the state vector with the requested state."""
22 X_initial = numpy.zeros((25, 1))
23 # All the wheels are spinning at the speed needed to hit the velocity in m/s
Austin Schuhb8b34be2024-07-14 16:06:19 -070024 drive_wheel_velocity = (drive_wheel_velocity
25 or numpy.linalg.norm(velocity))
26
Austin Schuh2a1abec2024-07-10 20:31:16 -070027 X_initial[2, 0] = module_omega
Austin Schuhb8b34be2024-07-14 16:06:19 -070028 X_initial[3, 0] = drive_wheel_velocity / (dynamics.WHEEL_RADIUS)
Austin Schuh2a1abec2024-07-10 20:31:16 -070029
30 X_initial[6, 0] = module_omega
Austin Schuhb8b34be2024-07-14 16:06:19 -070031 X_initial[7, 0] = drive_wheel_velocity / (dynamics.WHEEL_RADIUS)
Austin Schuh2a1abec2024-07-10 20:31:16 -070032
33 X_initial[10, 0] = module_omega
Austin Schuhb8b34be2024-07-14 16:06:19 -070034 X_initial[11, 0] = drive_wheel_velocity / (dynamics.WHEEL_RADIUS)
Austin Schuh2a1abec2024-07-10 20:31:16 -070035
36 X_initial[14, 0] = module_omega
Austin Schuhb8b34be2024-07-14 16:06:19 -070037 X_initial[15, 0] = drive_wheel_velocity / (dynamics.WHEEL_RADIUS)
Austin Schuh2a1abec2024-07-10 20:31:16 -070038
39 X_initial[0, 0] = module_angle
40 X_initial[4, 0] = module_angle
41 X_initial[8, 0] = module_angle
42 X_initial[12, 0] = module_angle
43
Austin Schuhb8b34be2024-07-14 16:06:19 -070044 if module_angles is not None:
45 assert (len(module_angles) == 4)
46 X_initial[0, 0] = module_angles[0]
47 X_initial[4, 0] = module_angles[1]
48 X_initial[8, 0] = module_angles[2]
49 X_initial[12, 0] = module_angles[3]
50
Austin Schuh2a1abec2024-07-10 20:31:16 -070051 X_initial[18, 0] = theta
52
53 X_initial[19, 0] = velocity[0, 0] + dx
54 X_initial[20, 0] = velocity[1, 0] + dy
55 X_initial[21, 0] = omega
56
57 return X_initial
58
59
Austin Schuhb8b34be2024-07-14 16:06:19 -070060def wrap(fn):
61 evaluated_fn = fn(casadi.SX.sym("X", 25, 1), casadi.SX.sym("U", 8, 1))
62 return lambda X, U: numpy.array(evaluated_fn(X, U))
63
64
65def wrap_module(fn, i):
66 evaluated_fn = fn(i, casadi.SX.sym("X", 25, 1), casadi.SX.sym("U", 8, 1))
67 return lambda X, U: numpy.array(evaluated_fn(X, U))
68
69
Austin Schuh2a1abec2024-07-10 20:31:16 -070070class TestHPolytope(unittest.TestCase):
71 I = numpy.zeros((8, 1))
72
73 def setUp(self):
Austin Schuhb8b34be2024-07-14 16:06:19 -070074 self.swerve_physics = wrap(dynamics.swerve_physics)
75 self.contact_patch_velocity = [
76 wrap_module(dynamics.contact_patch_velocity, i) for i in range(4)
77 ]
78 self.wheel_ground_velocity = [
79 wrap_module(dynamics.wheel_ground_velocity, i) for i in range(4)
80 ]
81 self.wheel_slip_velocity = [
82 wrap_module(dynamics.wheel_slip_velocity, i) for i in range(4)
83 ]
84 self.wheel_force = [
85 wrap_module(dynamics.wheel_force, i) for i in range(4)
86 ]
87 self.module_angular_accel = [
88 wrap_module(dynamics.module_angular_accel, i) for i in range(4)
89 ]
90 self.F = [wrap_module(dynamics.F, i) for i in range(4)]
91 self.mounting_location = [
92 wrap_module(dynamics.mounting_location, i) for i in range(4)
93 ]
94
95 self.slip_angle = [
96 wrap_module(dynamics.slip_angle, i) for i in range(4)
97 ]
98 self.slip_ratio = [
99 wrap_module(dynamics.slip_ratio, i) for i in range(4)
100 ]
101 self.Ms = [wrap_module(dynamics.Ms, i) for i in range(4)]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700102
103 def test_contact_patch_velocity(self):
104 """Tests that the contact patch velocity makes sense."""
105 for i in range(4):
Austin Schuhb8b34be2024-07-14 16:06:19 -0700106 contact_patch_velocity = wrap_module(
107 dynamics.contact_patch_velocity, i)
108 wheel_ground_velocity = wrap_module(dynamics.wheel_ground_velocity,
109 i)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700110
111 # No angular velocity should result in just linear motion.
112 for velocity in [
113 numpy.array([[1.5], [0.0]]),
114 numpy.array([[0.0], [1.0]]),
115 numpy.array([[-1.5], [0.0]]),
116 numpy.array([[0.0], [-1.0]]),
117 numpy.array([[2.0], [-1.7]]),
118 ]:
119 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -0700120 patch_velocity = contact_patch_velocity(
121 state_vector(velocity=velocity, theta=theta), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700122
123 assert_array_equal(patch_velocity, velocity)
124
125 # Now, test that spinning the robot results in module velocities.
126 # We are assuming that each module is on a square robot.
127 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
128 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
129 for omega in [0.65, -0.1]:
130 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700131 patch_velocity = contact_patch_velocity(
132 state_vector(velocity=numpy.array([[0.0], [0.0]]),
133 theta=theta,
134 omega=omega,
135 module_angle=module_center_of_mass_angle),
136 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700137
138 assert_array_almost_equal(
139 patch_velocity,
140 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) +
141 dynamics.CASTER) * omega * numpy.array([[
142 -numpy.sin(theta + module_center_of_mass_angle)
143 ], [numpy.cos(theta + module_center_of_mass_angle)]]))
144
145 # Point the wheel along +x, rotate it by theta, then spin it.
146 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700147 patch_velocity = contact_patch_velocity(
148 state_vector(
149 velocity=numpy.array([[0.0], [0.0]]),
150 theta=-module_center_of_mass_angle,
151 module_omega=omega,
152 module_angle=(theta +
153 module_center_of_mass_angle)),
154 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700155
156 assert_array_almost_equal(
157 patch_velocity,
158 dynamics.CASTER * omega *
159 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]))
160
161 # Now, test that the rotation back to wheel coordinates works.
162 # The easiest way to do this is to point the wheel in a direction,
163 # move in that direction, and confirm that there is no lateral velocity.
164 for robot_angle in [0.0, 1.0, -5.0]:
165 for module_angle in [0.0, 1.0, -5.0]:
166 wheel_patch_velocity = numpy.array(
167 wheel_ground_velocity(
168 state_vector(velocity=numpy.array(
169 [[numpy.cos(robot_angle + module_angle)],
170 [numpy.sin(robot_angle + module_angle)]]),
171 theta=robot_angle,
172 module_angle=module_angle), self.I))
173
174 assert_array_almost_equal(wheel_patch_velocity,
175 numpy.array([[1], [0]]))
176
177 def test_slip_angle(self):
178 """Tests that the slip_angle calculation works."""
179 velocity = numpy.array([[1.5], [0.0]])
180
181 for i in range(4):
182 x = casadi.SX.sym("x")
183 y = casadi.SX.sym("y")
184 half_atan2 = casadi.Function('half_atan2', [y, x],
185 [dynamics.half_atan2(y, x)])
186
Austin Schuh2a1abec2024-07-10 20:31:16 -0700187 for wrap in range(-3, 3):
188 for theta in [0.0, 0.6, -0.4]:
189 module_angle = numpy.pi * wrap + theta
190
191 self.assertAlmostEqual(
192 theta,
193 half_atan2(numpy.sin(module_angle),
194 numpy.cos(module_angle)))
195
Austin Schuhb8b34be2024-07-14 16:06:19 -0700196 computed_angle = self.slip_angle[i](state_vector(
197 velocity=velocity,
198 module_angle=numpy.pi * wrap + theta), self.I)[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700199
200 self.assertAlmostEqual(theta, computed_angle)
201
Austin Schuhb8b34be2024-07-14 16:06:19 -0700202 def test_wheel_torque(self):
203 """Tests that the per module self aligning forces have the right signs."""
204 X = state_vector(module_angles=[-0.001, -0.001, 0.001, 0.001])
205 xdot_equal = self.swerve_physics(X, self.I)
206
207 self.assertGreater(xdot_equal[2, 0], 0.0)
208 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
209 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
210
211 self.assertGreater(xdot_equal[6, 0], 0.0)
212 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
213 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
214
215 self.assertLess(xdot_equal[10, 0], 0.0)
216 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
217 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
218
219 self.assertLess(xdot_equal[14, 0], 0.0)
220 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
221 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
222
223 # Shouldn't be spinning.
224 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
225
226 # Now, make the bot want to go left.
227 # The wheels will be going too fast based on our calcs, so they should be decelerating.
228 X = state_vector(module_angles=[0.01, 0.01, 0.01, 0.01])
229 xdot_left = self.swerve_physics(X, self.I)
230
231 self.assertLess(xdot_left[2, 0], -0.1)
232 self.assertLess(xdot_left[3, 0], 0.0)
233 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
234
235 self.assertLess(xdot_left[6, 0], -0.1)
236 self.assertLess(xdot_left[7, 0], 0.0)
237 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
238
239 self.assertLess(xdot_left[10, 0], -0.1)
240 self.assertLess(xdot_left[11, 0], 0.0)
241 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
242
243 self.assertLess(xdot_left[14, 0], -0.1)
244 self.assertLess(xdot_left[15, 0], 0.0)
245 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
246
247 self.assertGreater(xdot_left[19, 0], 0.0001)
248 self.assertGreater(xdot_left[20, 0], 0.1)
249 # Shouldn't be spinning.
250 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
251
252 # And now do it to the right too.
253 X = state_vector(module_angles=[-0.01, -0.01, -0.01, -0.01])
254 xdot_right = self.swerve_physics(X, self.I)
255
256 self.assertGreater(xdot_right[2, 0], 0.1)
257 self.assertLess(xdot_right[3, 0], 0.0)
258 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
259
260 self.assertGreater(xdot_right[6, 0], 0.1)
261 self.assertLess(xdot_right[7, 0], 0.0)
262 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
263
264 self.assertGreater(xdot_right[10, 0], 0.1)
265 self.assertLess(xdot_right[11, 0], 0.0)
266 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
267
268 self.assertGreater(xdot_right[14, 0], 0.1)
269 self.assertLess(xdot_right[15, 0], 0.0)
270 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
271
272 self.assertGreater(xdot_right[19, 0], 0.0001)
273 self.assertLess(xdot_right[20, 0], -0.1)
274 # Shouldn't be spinning.
275 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
276
Austin Schuh2a1abec2024-07-10 20:31:16 -0700277 def test_wheel_forces(self):
278 """Tests that the per module forces have the right signs."""
279 for i in range(4):
Austin Schuhb8b34be2024-07-14 16:06:19 -0700280 wheel_force = wrap_module(dynamics.wheel_force, i)
281
282 X = state_vector()
283 robot_equal = wheel_force(X, self.I)
284 xdot_equal = self.swerve_physics(X, self.I)
285 self.assertEqual(robot_equal[0, 0], 0.0)
286 self.assertEqual(robot_equal[1, 0], 0.0)
287 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
288 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700289
290 # Robot is moving faster than the wheels, it should decelerate.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700291 X = state_vector(dx=0.01)
292 robot_faster = wheel_force(X, self.I)
293 xdot_faster = self.swerve_physics(X, self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700294 self.assertLess(robot_faster[0, 0], -0.1)
295 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700296 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700297
298 # Robot is now going slower than the wheels. It should accelerate.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700299 X = state_vector(dx=-0.01)
300 robot_slower = wheel_force(X, self.I)
301 xdot_slower = self.swerve_physics(X, self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700302 self.assertGreater(robot_slower[0, 0], 0.1)
303 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700304 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700305
306 # Positive lateral velocity -> negative force.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700307 robot_left = wheel_force(state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700308 self.assertEqual(robot_left[0, 0], 0.0)
309 self.assertLess(robot_left[1, 0], -0.1)
310
311 # Negative lateral velocity -> positive force.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700312 robot_right = wheel_force(state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700313 self.assertEqual(robot_right[0, 0], 0.0)
314 self.assertGreater(robot_right[1, 0], 0.1)
315
316
317if __name__ == '__main__':
318 unittest.main()