blob: 0a3fdb6f048b5e281e50565512fbf574c0b6970d [file] [log] [blame]
Austin Schuh2a1abec2024-07-10 20:31:16 -07001#!/usr/bin/python3
2
3import numpy
4import sys, os
5import casadi
Austin Schuhbd75c482024-08-18 00:03:51 -07006import scipy
Austin Schuh2a1abec2024-07-10 20:31:16 -07007from numpy.testing import assert_array_equal, assert_array_almost_equal
8import unittest
9
Austin Schuh27694fa2024-07-20 16:29:49 -070010from frc971.control_loops.swerve import bigcaster_dynamics
Austin Schuh2a1abec2024-07-10 20:31:16 -070011from frc971.control_loops.swerve import dynamics
Austin Schuh27694fa2024-07-20 16:29:49 -070012from frc971.control_loops.swerve import nocaster_dynamics
justinT2107d41ed2024-07-31 21:12:31 -070013from frc971.control_loops.swerve import physics_test_utils as utils
Austin Schuhb8b34be2024-07-14 16:06:19 -070014
15
Austin Schuh6927bc32024-07-14 17:24:56 -070016class TestSwervePhysics(unittest.TestCase):
Austin Schuh2a1abec2024-07-10 20:31:16 -070017 I = numpy.zeros((8, 1))
18
Austin Schuh6ea789e2024-07-27 13:45:53 -070019 def to_velocity_state(self, X):
20 return dynamics.to_velocity_state(X)
21
22 def swerve_full_dynamics(self, X, U, skip_compare=False):
23 X_velocity = self.to_velocity_state(X)
24 Xdot = self.position_swerve_full_dynamics(X, U)
25 if not skip_compare:
26 velocity_states = self.to_velocity_state(Xdot)
27 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
28 self.assertLess(
29 numpy.linalg.norm(velocity_states - velocity_physics),
30 2e-2,
31 msg=
32 f'Norm failed, full physics -> {velocity_states.T}, velocity physics -> {velocity_physics}, difference -> {velocity_physics - velocity_states}',
33 )
34
35 return Xdot
36
Austin Schuh27694fa2024-07-20 16:29:49 -070037 def wrap(self, python_module):
Austin Schuh6ea789e2024-07-27 13:45:53 -070038 self.position_swerve_full_dynamics = utils.wrap(
39 python_module.swerve_full_dynamics)
40
41 evaluated_fn = python_module.velocity_swerve_physics(
42 casadi.SX.sym("X", dynamics.NUM_VELOCITY_STATES, 1),
43 casadi.SX.sym("U", 8, 1))
44 self.velocity_swerve_physics = lambda X, U: numpy.array(
45 evaluated_fn(X, U))
46
Austin Schuhb8b34be2024-07-14 16:06:19 -070047 self.contact_patch_velocity = [
justinT2107d41ed2024-07-31 21:12:31 -070048 utils.wrap_module(python_module.contact_patch_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -070049 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070050 ]
51 self.wheel_ground_velocity = [
justinT2107d41ed2024-07-31 21:12:31 -070052 utils.wrap_module(python_module.wheel_ground_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -070053 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070054 ]
55 self.wheel_slip_velocity = [
justinT2107d41ed2024-07-31 21:12:31 -070056 utils.wrap_module(python_module.wheel_slip_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -070057 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070058 ]
justinT2107d41ed2024-07-31 21:12:31 -070059 self.wheel_force = [
60 utils.wrap_module(python_module.wheel_force, i) for i in range(4)
61 ]
62 self.module_angular_accel = [
63 utils.wrap_module(python_module.module_angular_accel, i)
64 for i in range(4)
65 ]
66 self.F = [utils.wrap_module(python_module.F, i) for i in range(4)]
Austin Schuhb8b34be2024-07-14 16:06:19 -070067 self.mounting_location = [
justinT2107d41ed2024-07-31 21:12:31 -070068 utils.wrap_module(python_module.mounting_location, i)
69 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070070 ]
71
72 self.slip_angle = [
justinT2107d41ed2024-07-31 21:12:31 -070073 utils.wrap_module(python_module.slip_angle, i) for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070074 ]
75 self.slip_ratio = [
justinT2107d41ed2024-07-31 21:12:31 -070076 utils.wrap_module(python_module.slip_ratio, i) for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -070077 ]
justinT2107d41ed2024-07-31 21:12:31 -070078 self.Ms = [utils.wrap_module(python_module.Ms, i) for i in range(4)]
Austin Schuh27694fa2024-07-20 16:29:49 -070079
80 def setUp(self):
81 self.wrap(dynamics)
Austin Schuh2a1abec2024-07-10 20:31:16 -070082
83 def test_contact_patch_velocity(self):
84 """Tests that the contact patch velocity makes sense."""
85 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -070086 contact_patch_velocity = self.contact_patch_velocity[i]
87 wheel_ground_velocity = self.wheel_ground_velocity[i]
Austin Schuh2a1abec2024-07-10 20:31:16 -070088
89 # No angular velocity should result in just linear motion.
90 for velocity in [
91 numpy.array([[1.5], [0.0]]),
92 numpy.array([[0.0], [1.0]]),
93 numpy.array([[-1.5], [0.0]]),
94 numpy.array([[0.0], [-1.0]]),
95 numpy.array([[2.0], [-1.7]]),
96 ]:
97 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -070098 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -070099 utils.state_vector(velocity=velocity, theta=theta),
100 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700101
102 assert_array_equal(patch_velocity, velocity)
103
104 # Now, test that spinning the robot results in module velocities.
105 # We are assuming that each module is on a square robot.
106 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
107 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
108 for omega in [0.65, -0.1]:
109 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700110 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700111 utils.state_vector(
112 velocity=numpy.array([[0.0], [0.0]]),
113 theta=theta,
114 omega=omega,
115 module_angle=module_center_of_mass_angle,
116 ),
117 self.I,
118 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700119
120 assert_array_almost_equal(
121 patch_velocity,
Austin Schuh6927bc32024-07-14 17:24:56 -0700122 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
justinT2107d41ed2024-07-31 21:12:31 -0700123 dynamics.CASTER) * omega * numpy.array([
124 [-numpy.sin(theta + module_center_of_mass_angle)],
125 [numpy.cos(theta + module_center_of_mass_angle)],
126 ]),
127 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700128
129 # Point the wheel along +x, rotate it by theta, then spin it.
130 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700131 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700132 utils.state_vector(
Austin Schuhb8b34be2024-07-14 16:06:19 -0700133 velocity=numpy.array([[0.0], [0.0]]),
134 theta=-module_center_of_mass_angle,
135 module_omega=omega,
justinT2107d41ed2024-07-31 21:12:31 -0700136 module_angle=(theta + module_center_of_mass_angle),
137 ),
138 self.I,
139 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700140
141 assert_array_almost_equal(
justinT2107d41ed2024-07-31 21:12:31 -0700142 patch_velocity,
143 -dynamics.CASTER * omega *
144 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]),
145 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700146
147 # Now, test that the rotation back to wheel coordinates works.
148 # The easiest way to do this is to point the wheel in a direction,
149 # move in that direction, and confirm that there is no lateral velocity.
150 for robot_angle in [0.0, 1.0, -5.0]:
151 for module_angle in [0.0, 1.0, -5.0]:
152 wheel_patch_velocity = numpy.array(
153 wheel_ground_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700154 utils.state_vector(
155 velocity=numpy.array([
156 [numpy.cos(robot_angle + module_angle)],
157 [numpy.sin(robot_angle + module_angle)],
158 ]),
159 theta=robot_angle,
160 module_angle=module_angle,
161 ),
162 self.I,
163 ))
Austin Schuh2a1abec2024-07-10 20:31:16 -0700164
165 assert_array_almost_equal(wheel_patch_velocity,
166 numpy.array([[1], [0]]))
167
168 def test_slip_angle(self):
169 """Tests that the slip_angle calculation works."""
170 velocity = numpy.array([[1.5], [0.0]])
171
172 for i in range(4):
Austin Schuh27694fa2024-07-20 16:29:49 -0700173 for wrap in range(-1, 2):
Austin Schuh2a1abec2024-07-10 20:31:16 -0700174 for theta in [0.0, 0.6, -0.4]:
175 module_angle = numpy.pi * wrap + theta
176
Austin Schuhbd75c482024-08-18 00:03:51 -0700177 # We have redefined the angle to be the softened sin of the angle.
Austin Schuh27694fa2024-07-20 16:29:49 -0700178 # That way, when the module flips directions, the slip angle also flips
179 # directions to keep it stable.
justinT2107d41ed2024-07-31 21:12:31 -0700180 computed_angle = self.slip_angle[i](
181 utils.state_vector(velocity=velocity,
Austin Schuhbd75c482024-08-18 00:03:51 -0700182 module_angle=module_angle),
justinT2107d41ed2024-07-31 21:12:31 -0700183 self.I,
184 )[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700185
Austin Schuhbd75c482024-08-18 00:03:51 -0700186 # Compute out the expected value directly to confirm we got it right.
187 loggain = 20.0
188 vy = 1.5 * numpy.sin(-module_angle)
189 vx = 1.5 * numpy.cos(-module_angle)
190 expected = numpy.sin(-numpy.arctan2(
191 vy,
192 scipy.special.logsumexp([1.0, abs(vx) * loggain]) /
193 loggain))
Austin Schuh27694fa2024-07-20 16:29:49 -0700194
195 self.assertAlmostEqual(
196 expected,
197 computed_angle,
justinT2107d41ed2024-07-31 21:12:31 -0700198 msg=f"Trying wrap {wrap} theta {theta}",
199 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700200
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."""
Austin Schuh27694fa2024-07-20 16:29:49 -0700203 # Point all the modules in a little bit.
justinT2107d41ed2024-07-31 21:12:31 -0700204 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700205 velocity=numpy.array([[1.0], [0.0]]),
206 module_angles=[-0.001, -0.001, 0.001, 0.001],
207 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700208 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700209
210 self.assertGreater(xdot_equal[2, 0], 0.0)
211 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
212 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
213
214 self.assertGreater(xdot_equal[6, 0], 0.0)
215 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
216 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
217
218 self.assertLess(xdot_equal[10, 0], 0.0)
219 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
220 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
221
222 self.assertLess(xdot_equal[14, 0], 0.0)
223 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
224 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
225
226 # Shouldn't be spinning.
227 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
228
Austin Schuh27694fa2024-07-20 16:29:49 -0700229 # Now, make the bot want to go left by going to the other side.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700230 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700231 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700232 velocity=numpy.array([[1.0], [0.0]]),
233 module_angles=[0.01, 0.01, 0.01, 0.01],
234 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700235 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700236
Austin Schuh27694fa2024-07-20 16:29:49 -0700237 self.assertLess(xdot_left[2, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700238 self.assertLess(xdot_left[3, 0], 0.0)
239 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
240
Austin Schuh27694fa2024-07-20 16:29:49 -0700241 self.assertLess(xdot_left[6, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700242 self.assertLess(xdot_left[7, 0], 0.0)
243 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
244
Austin Schuh27694fa2024-07-20 16:29:49 -0700245 self.assertLess(xdot_left[10, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700246 self.assertLess(xdot_left[11, 0], 0.0)
247 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
248
Austin Schuh27694fa2024-07-20 16:29:49 -0700249 self.assertLess(xdot_left[14, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700250 self.assertLess(xdot_left[15, 0], 0.0)
251 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
252
253 self.assertGreater(xdot_left[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700254 self.assertGreater(xdot_left[20, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700255 # Shouldn't be spinning.
256 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
257
258 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700259 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700260 velocity=numpy.array([[1.0], [0.0]]),
261 module_angles=[-0.01, -0.01, -0.01, -0.01],
262 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700263 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700264
Austin Schuh27694fa2024-07-20 16:29:49 -0700265 self.assertGreater(xdot_right[2, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700266 self.assertLess(xdot_right[3, 0], 0.0)
267 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
268
Austin Schuh27694fa2024-07-20 16:29:49 -0700269 self.assertGreater(xdot_right[6, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700270 self.assertLess(xdot_right[7, 0], 0.0)
271 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
272
Austin Schuh27694fa2024-07-20 16:29:49 -0700273 self.assertGreater(xdot_right[10, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700274 self.assertLess(xdot_right[11, 0], 0.0)
275 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
276
Austin Schuh27694fa2024-07-20 16:29:49 -0700277 self.assertGreater(xdot_right[14, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700278 self.assertLess(xdot_right[15, 0], 0.0)
279 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
280
281 self.assertGreater(xdot_right[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700282 self.assertLess(xdot_right[20, 0], -0.05)
283 # Shouldn't be spinning.
284 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
285
286 def test_wheel_torque_backwards_nocaster(self):
287 """Tests that the per module self aligning forces have the right signs when going backwards."""
288 self.wrap(nocaster_dynamics)
289 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700290 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700291 velocity=numpy.array([[1.0], [0.0]]),
292 module_angles=[
293 numpy.pi - 0.001,
294 numpy.pi - 0.001,
295 numpy.pi + 0.001,
296 numpy.pi + 0.001,
297 ],
298 drive_wheel_velocity=-1,
299 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700300 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700301
302 self.assertGreater(xdot_equal[2, 0], 0.0, msg="Steering backwards")
303 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
304 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
305
306 self.assertGreater(xdot_equal[6, 0], 0.0, msg="Steering backwards")
307 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
308 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
309
310 self.assertLess(xdot_equal[10, 0], 0.0, msg="Steering backwards")
311 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
312 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
313
314 self.assertLess(xdot_equal[14, 0], 0.0, msg="Steering backwards")
315 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
316 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
317
318 # Shouldn't be spinning.
319 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
320
321 # Now, make the bot want to go left by going to the other side.
322 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700323 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700324 velocity=numpy.array([[1.0], [0.0]]),
325 module_angles=[numpy.pi + 0.01] * 4,
326 drive_wheel_velocity=-1,
327 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700328 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700329
330 self.assertLess(xdot_left[2, 0], -0.05)
331 self.assertGreater(xdot_left[3, 0], 0.0)
332 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
333
334 self.assertLess(xdot_left[6, 0], -0.05)
335 self.assertGreater(xdot_left[7, 0], 0.0)
336 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
337
338 self.assertLess(xdot_left[10, 0], -0.05)
339 self.assertGreater(xdot_left[11, 0], 0.0)
340 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
341
342 self.assertLess(xdot_left[14, 0], -0.05)
343 self.assertGreater(xdot_left[15, 0], 0.0)
344 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
345
346 self.assertGreater(xdot_left[19, 0], 0.0001)
347 self.assertGreater(xdot_left[20, 0], 0.05)
348 # Shouldn't be spinning.
349 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
350
351 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700352 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700353 velocity=numpy.array([[1.0], [0.0]]),
354 drive_wheel_velocity=-1,
355 module_angles=[-0.01 + numpy.pi] * 4,
356 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700357 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700358
359 self.assertGreater(xdot_right[2, 0], 0.05)
360 self.assertGreater(xdot_right[3, 0], 0.0)
361 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
362
363 self.assertGreater(xdot_right[6, 0], 0.05)
364 self.assertGreater(xdot_right[7, 0], 0.0)
365 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
366
367 self.assertGreater(xdot_right[10, 0], 0.05)
368 self.assertGreater(xdot_right[11, 0], 0.0)
369 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
370
371 self.assertGreater(xdot_right[14, 0], 0.05)
372 self.assertGreater(xdot_right[15, 0], 0.0)
373 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
374
375 self.assertGreater(xdot_right[19, 0], 0.0001)
376 self.assertLess(xdot_right[20, 0], -0.05)
377 # Shouldn't be spinning.
378 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
379
380 def test_wheel_torque_backwards_caster(self):
381 """Tests that the per module self aligning forces have the right signs when going backwards with a lot of caster."""
382 self.wrap(bigcaster_dynamics)
383 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700384 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700385 velocity=numpy.array([[1.0], [0.0]]),
386 module_angles=[
387 numpy.pi - 0.001,
388 numpy.pi - 0.001,
389 numpy.pi + 0.001,
390 numpy.pi + 0.001,
391 ],
392 drive_wheel_velocity=-1,
393 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700394 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700395
396 self.assertLess(xdot_equal[2, 0], 0.0, msg="Steering backwards")
397 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
398 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
399
400 self.assertLess(xdot_equal[6, 0], 0.0, msg="Steering backwards")
401 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
402 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
403
404 self.assertGreater(xdot_equal[10, 0], 0.0, msg="Steering backwards")
405 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
406 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
407
408 self.assertGreater(xdot_equal[14, 0], 0.0, msg="Steering backwards")
409 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
410 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
411
412 # Shouldn't be spinning.
413 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
414
415 # Now, make the bot want to go left by going to the other side.
416 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700417 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700418 velocity=numpy.array([[1.0], [0.0]]),
419 module_angles=[numpy.pi + 0.01] * 4,
420 drive_wheel_velocity=-1,
421 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700422 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700423
424 self.assertGreater(xdot_left[2, 0], -0.05)
425 self.assertGreater(xdot_left[3, 0], 0.0)
426 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
427
428 self.assertGreater(xdot_left[6, 0], -0.05)
429 self.assertGreater(xdot_left[7, 0], 0.0)
430 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
431
432 self.assertGreater(xdot_left[10, 0], -0.05)
433 self.assertGreater(xdot_left[11, 0], 0.0)
434 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
435
436 self.assertGreater(xdot_left[14, 0], -0.05)
437 self.assertGreater(xdot_left[15, 0], 0.0)
438 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
439
440 self.assertGreater(xdot_left[19, 0], 0.0001)
441 self.assertGreater(xdot_left[20, 0], 0.05)
442 # Shouldn't be spinning.
443 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
444
445 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700446 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700447 velocity=numpy.array([[1.0], [0.0]]),
448 drive_wheel_velocity=-1,
449 module_angles=[-0.01 + numpy.pi] * 4,
450 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700451 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700452
453 self.assertLess(xdot_right[2, 0], 0.05)
454 self.assertGreater(xdot_right[3, 0], 0.0)
455 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
456
457 self.assertLess(xdot_right[6, 0], 0.05)
458 self.assertGreater(xdot_right[7, 0], 0.0)
459 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
460
461 self.assertLess(xdot_right[10, 0], 0.05)
462 self.assertGreater(xdot_right[11, 0], 0.0)
463 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
464
465 self.assertLess(xdot_right[14, 0], 0.05)
466 self.assertGreater(xdot_right[15, 0], 0.0)
467 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
468
469 self.assertGreater(xdot_right[19, 0], 0.0001)
470 self.assertLess(xdot_right[20, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700471 # Shouldn't be spinning.
472 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
473
Austin Schuh2a1abec2024-07-10 20:31:16 -0700474 def test_wheel_forces(self):
475 """Tests that the per module forces have the right signs."""
476 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700477 wheel_force = self.wheel_force[i]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700478
justinT2107d41ed2024-07-31 21:12:31 -0700479 X = utils.state_vector()
Austin Schuhb8b34be2024-07-14 16:06:19 -0700480 robot_equal = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700481 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700482 self.assertEqual(robot_equal[0, 0], 0.0)
483 self.assertEqual(robot_equal[1, 0], 0.0)
484 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
485 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700486
487 # Robot is moving faster than the wheels, it should decelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700488 X = utils.state_vector(dx=0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700489 robot_faster = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700490 xdot_faster = self.swerve_full_dynamics(X,
491 self.I,
492 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700493 self.assertLess(robot_faster[0, 0], -0.1)
494 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700495 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700496
497 # Robot is now going slower than the wheels. It should accelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700498 X = utils.state_vector(dx=-0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700499 robot_slower = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700500 xdot_slower = self.swerve_full_dynamics(X,
501 self.I,
502 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700503 self.assertGreater(robot_slower[0, 0], 0.1)
504 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700505 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700506
507 # Positive lateral velocity -> negative force.
justinT2107d41ed2024-07-31 21:12:31 -0700508 robot_left = wheel_force(utils.state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700509 self.assertEqual(robot_left[0, 0], 0.0)
510 self.assertLess(robot_left[1, 0], -0.1)
511
512 # Negative lateral velocity -> positive force.
justinT2107d41ed2024-07-31 21:12:31 -0700513 robot_right = wheel_force(utils.state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700514 self.assertEqual(robot_right[0, 0], 0.0)
515 self.assertGreater(robot_right[1, 0], 0.1)
516
Austin Schuh3233d7f2024-07-27 17:51:52 -0700517 def test_steer_accel(self):
518 """Tests that applying a steer torque accelerates the steer reasonably."""
519
520 for velocity in [
521 numpy.array([[0.0], [0.0]]),
522 numpy.array([[1.0], [0.0]]),
523 numpy.array([[2.0], [0.0]])
524 ]:
525 module_angles = [0.0] * 4
526
527 X = utils.state_vector(
528 velocity=velocity,
529 omega=0.0,
530 module_angles=module_angles,
531 )
532 steer_I = numpy.array([[1.0], [0.0]] * 4)
533 xdot = self.swerve_full_dynamics(X, steer_I)
534
535 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS0, 0],
536 1.4,
537 places=0)
538 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS1, 0],
539 1.4,
540 places=0)
541 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS2, 0],
542 1.4,
543 places=0)
544 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS3, 0],
545 1.4,
546 places=0)
547
548 def test_drive_accel(self):
549 """Tests that applying a drive torque accelerates the drive reasonably."""
550
551 for velocity in [
552 numpy.array([[0.01], [0.0]]),
553 numpy.array([[1.0], [0.0]]),
554 numpy.array([[2.0], [0.0]])
555 ]:
556 module_angles = [0.0] * 4
557
558 X = utils.state_vector(
559 velocity=velocity,
560 omega=0.0,
561 module_angles=module_angles,
562 )
563 steer_I = numpy.array([[0.0], [100.0]] * 4)
564 # Since the wheels are spinning at the same speed as the ground, there is no force accelerating the robot. Which means there is not steering moment. The two physics diverge pretty heavily.
565 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
566
567 self.assertGreater(xdot[dynamics.STATE_OMEGAD0, 0], 100.0)
568 self.assertGreater(xdot[dynamics.STATE_OMEGAD1, 0], 100.0)
569 self.assertGreater(xdot[dynamics.STATE_OMEGAD2, 0], 100.0)
570 self.assertGreater(xdot[dynamics.STATE_OMEGAD3, 0], 100.0)
571
572 X = utils.state_vector(
573 velocity=velocity,
574 omega=0.0,
575 module_angles=module_angles,
576 )
577 steer_I = numpy.array([[0.0], [-100.0]] * 4)
578 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
579
580 self.assertLess(xdot[dynamics.STATE_OMEGAD0, 0], -100.0)
581 self.assertLess(xdot[dynamics.STATE_OMEGAD1, 0], -100.0)
582 self.assertLess(xdot[dynamics.STATE_OMEGAD2, 0], -100.0)
583 self.assertLess(xdot[dynamics.STATE_OMEGAD3, 0], -100.0)
584
Austin Schuh98fbbe82024-08-18 23:07:26 -0700585 def test_steer_coupling(self):
586 """Tests that the steer coupling factor cancels out steer coupling torque."""
587 steer_I = numpy.array(
588 [[dynamics.STEER_CURRENT_COUPLING_FACTOR * 10.0], [10.0]] * 4)
589
590 X = utils.state_vector(
591 velocity=numpy.array([[0.0], [0.0]]),
592 omega=0.0,
593 )
594 X_velocity = self.to_velocity_state(X)
595 Xdot = self.velocity_swerve_physics(X_velocity, steer_I)
596
597 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS0, 0], 0.0)
598 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS1, 0], 0.0)
599 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS2, 0], 0.0)
600 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS3, 0], 0.0)
601
Austin Schuh2a1abec2024-07-10 20:31:16 -0700602
justinT2107d41ed2024-07-31 21:12:31 -0700603if __name__ == "__main__":
Austin Schuh2a1abec2024-07-10 20:31:16 -0700604 unittest.main()