blob: 7f35f565ec8fb86524c1c18a89de6b5bc80b0a69 [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 Schuh6927bc32024-07-14 17:24:56 -070070class TestSwervePhysics(unittest.TestCase):
Austin Schuh2a1abec2024-07-10 20:31:16 -070071 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,
Austin Schuh6927bc32024-07-14 17:24:56 -0700140 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
Austin Schuh2a1abec2024-07-10 20:31:16 -0700141 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(
Austin Schuh6927bc32024-07-14 17:24:56 -0700157 patch_velocity, -dynamics.CASTER * omega *
Austin Schuh2a1abec2024-07-10 20:31:16 -0700158 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]))
159
160 # Now, test that the rotation back to wheel coordinates works.
161 # The easiest way to do this is to point the wheel in a direction,
162 # move in that direction, and confirm that there is no lateral velocity.
163 for robot_angle in [0.0, 1.0, -5.0]:
164 for module_angle in [0.0, 1.0, -5.0]:
165 wheel_patch_velocity = numpy.array(
166 wheel_ground_velocity(
167 state_vector(velocity=numpy.array(
168 [[numpy.cos(robot_angle + module_angle)],
169 [numpy.sin(robot_angle + module_angle)]]),
170 theta=robot_angle,
171 module_angle=module_angle), self.I))
172
173 assert_array_almost_equal(wheel_patch_velocity,
174 numpy.array([[1], [0]]))
175
176 def test_slip_angle(self):
177 """Tests that the slip_angle calculation works."""
178 velocity = numpy.array([[1.5], [0.0]])
179
180 for i in range(4):
181 x = casadi.SX.sym("x")
182 y = casadi.SX.sym("y")
183 half_atan2 = casadi.Function('half_atan2', [y, x],
184 [dynamics.half_atan2(y, x)])
185
Austin Schuh2a1abec2024-07-10 20:31:16 -0700186 for wrap in range(-3, 3):
187 for theta in [0.0, 0.6, -0.4]:
188 module_angle = numpy.pi * wrap + theta
189
190 self.assertAlmostEqual(
191 theta,
192 half_atan2(numpy.sin(module_angle),
193 numpy.cos(module_angle)))
194
Austin Schuhb8b34be2024-07-14 16:06:19 -0700195 computed_angle = self.slip_angle[i](state_vector(
196 velocity=velocity,
197 module_angle=numpy.pi * wrap + theta), self.I)[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700198
199 self.assertAlmostEqual(theta, computed_angle)
200
Austin Schuhb8b34be2024-07-14 16:06:19 -0700201 def test_wheel_torque(self):
202 """Tests that the per module self aligning forces have the right signs."""
203 X = state_vector(module_angles=[-0.001, -0.001, 0.001, 0.001])
204 xdot_equal = self.swerve_physics(X, self.I)
205
206 self.assertGreater(xdot_equal[2, 0], 0.0)
207 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
208 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
209
210 self.assertGreater(xdot_equal[6, 0], 0.0)
211 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
212 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
213
214 self.assertLess(xdot_equal[10, 0], 0.0)
215 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
216 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
217
218 self.assertLess(xdot_equal[14, 0], 0.0)
219 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
220 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
221
222 # Shouldn't be spinning.
223 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
224
225 # Now, make the bot want to go left.
226 # The wheels will be going too fast based on our calcs, so they should be decelerating.
227 X = state_vector(module_angles=[0.01, 0.01, 0.01, 0.01])
228 xdot_left = self.swerve_physics(X, self.I)
229
230 self.assertLess(xdot_left[2, 0], -0.1)
231 self.assertLess(xdot_left[3, 0], 0.0)
232 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
233
234 self.assertLess(xdot_left[6, 0], -0.1)
235 self.assertLess(xdot_left[7, 0], 0.0)
236 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
237
238 self.assertLess(xdot_left[10, 0], -0.1)
239 self.assertLess(xdot_left[11, 0], 0.0)
240 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
241
242 self.assertLess(xdot_left[14, 0], -0.1)
243 self.assertLess(xdot_left[15, 0], 0.0)
244 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
245
246 self.assertGreater(xdot_left[19, 0], 0.0001)
247 self.assertGreater(xdot_left[20, 0], 0.1)
248 # Shouldn't be spinning.
249 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
250
251 # And now do it to the right too.
252 X = state_vector(module_angles=[-0.01, -0.01, -0.01, -0.01])
253 xdot_right = self.swerve_physics(X, self.I)
254
255 self.assertGreater(xdot_right[2, 0], 0.1)
256 self.assertLess(xdot_right[3, 0], 0.0)
257 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
258
259 self.assertGreater(xdot_right[6, 0], 0.1)
260 self.assertLess(xdot_right[7, 0], 0.0)
261 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
262
263 self.assertGreater(xdot_right[10, 0], 0.1)
264 self.assertLess(xdot_right[11, 0], 0.0)
265 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
266
267 self.assertGreater(xdot_right[14, 0], 0.1)
268 self.assertLess(xdot_right[15, 0], 0.0)
269 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
270
271 self.assertGreater(xdot_right[19, 0], 0.0001)
272 self.assertLess(xdot_right[20, 0], -0.1)
273 # Shouldn't be spinning.
274 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
275
Austin Schuh2a1abec2024-07-10 20:31:16 -0700276 def test_wheel_forces(self):
277 """Tests that the per module forces have the right signs."""
278 for i in range(4):
Austin Schuhb8b34be2024-07-14 16:06:19 -0700279 wheel_force = wrap_module(dynamics.wheel_force, i)
280
281 X = state_vector()
282 robot_equal = wheel_force(X, self.I)
283 xdot_equal = self.swerve_physics(X, self.I)
284 self.assertEqual(robot_equal[0, 0], 0.0)
285 self.assertEqual(robot_equal[1, 0], 0.0)
286 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
287 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700288
289 # Robot is moving faster than the wheels, it should decelerate.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700290 X = state_vector(dx=0.01)
291 robot_faster = wheel_force(X, self.I)
292 xdot_faster = self.swerve_physics(X, self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700293 self.assertLess(robot_faster[0, 0], -0.1)
294 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700295 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700296
297 # Robot is now going slower than the wheels. It should accelerate.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700298 X = state_vector(dx=-0.01)
299 robot_slower = wheel_force(X, self.I)
300 xdot_slower = self.swerve_physics(X, self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700301 self.assertGreater(robot_slower[0, 0], 0.1)
302 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700303 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700304
305 # Positive lateral velocity -> negative force.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700306 robot_left = wheel_force(state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700307 self.assertEqual(robot_left[0, 0], 0.0)
308 self.assertLess(robot_left[1, 0], -0.1)
309
310 # Negative lateral velocity -> positive force.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700311 robot_right = wheel_force(state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700312 self.assertEqual(robot_right[0, 0], 0.0)
313 self.assertGreater(robot_right[1, 0], 0.1)
314
315
316if __name__ == '__main__':
317 unittest.main()