blob: bc366fa6a883aef9399a7678962151b1c86b76d2 [file] [log] [blame]
Austin Schuh76534f32024-09-02 13:52:45 -07001#!/usr/bin/env python3
2import jax
3
4# casadi uses doubles. jax likes floats. We want to make sure the physics
5# matches really precisely, so force doubles for the tests.
6jax.config.update("jax_enable_x64", True)
Austin Schuh2a1abec2024-07-10 20:31:16 -07007
8import numpy
Austin Schuh76534f32024-09-02 13:52:45 -07009
10numpy.set_printoptions(precision=20)
Austin Schuh2a1abec2024-07-10 20:31:16 -070011import sys, os
12import casadi
Austin Schuhbd75c482024-08-18 00:03:51 -070013import scipy
Austin Schuh2a1abec2024-07-10 20:31:16 -070014from numpy.testing import assert_array_equal, assert_array_almost_equal
15import unittest
16
Austin Schuh27694fa2024-07-20 16:29:49 -070017from frc971.control_loops.swerve import bigcaster_dynamics
Austin Schuh2a1abec2024-07-10 20:31:16 -070018from frc971.control_loops.swerve import dynamics
Austin Schuh27694fa2024-07-20 16:29:49 -070019from frc971.control_loops.swerve import nocaster_dynamics
justinT2107d41ed2024-07-31 21:12:31 -070020from frc971.control_loops.swerve import physics_test_utils as utils
Austin Schuh5dac2292024-10-19 13:56:58 -070021from frc971.control_loops.swerve import casadi_velocity_mpc_lib
Austin Schuh76534f32024-09-02 13:52:45 -070022from frc971.control_loops.swerve import jax_dynamics
James Kuszmaulef14ab42024-09-14 15:05:24 -070023from frc971.control_loops.swerve.cpp_dynamics import swerve_dynamics as cpp_dynamics
Austin Schuhb8b34be2024-07-14 16:06:19 -070024
25
Austin Schuh6927bc32024-07-14 17:24:56 -070026class TestSwervePhysics(unittest.TestCase):
Austin Schuh2a1abec2024-07-10 20:31:16 -070027 I = numpy.zeros((8, 1))
Austin Schuh76534f32024-09-02 13:52:45 -070028 coefficients = jax_dynamics.Coefficients()
Austin Schuh2a1abec2024-07-10 20:31:16 -070029
Austin Schuh6ea789e2024-07-27 13:45:53 -070030 def to_velocity_state(self, X):
31 return dynamics.to_velocity_state(X)
32
33 def swerve_full_dynamics(self, X, U, skip_compare=False):
34 X_velocity = self.to_velocity_state(X)
35 Xdot = self.position_swerve_full_dynamics(X, U)
Austin Schuh76534f32024-09-02 13:52:45 -070036
Austin Schuh6ea789e2024-07-27 13:45:53 -070037 if not skip_compare:
Austin Schuh76534f32024-09-02 13:52:45 -070038 Xdot_velocity = self.to_velocity_state(Xdot)
Austin Schuh6ea789e2024-07-27 13:45:53 -070039 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
Austin Schuh76534f32024-09-02 13:52:45 -070040
Austin Schuh6ea789e2024-07-27 13:45:53 -070041 self.assertLess(
Austin Schuh76534f32024-09-02 13:52:45 -070042 numpy.linalg.norm(Xdot_velocity - velocity_physics),
Austin Schuh6ea789e2024-07-27 13:45:53 -070043 2e-2,
44 msg=
Austin Schuh76534f32024-09-02 13:52:45 -070045 f'Norm failed, full physics -> {X_velocity.T}, velocity physics -> {velocity_physics}, difference -> {velocity_physics - X_velocity}',
Austin Schuh6ea789e2024-07-27 13:45:53 -070046 )
47
Austin Schuh76534f32024-09-02 13:52:45 -070048 self.validate_dynamics_equality(X, U)
49
Austin Schuh6ea789e2024-07-27 13:45:53 -070050 return Xdot
51
Austin Schuh76534f32024-09-02 13:52:45 -070052 def validate_dynamics_equality(self, X, U):
53 """Tests that both the JAX code and casadi code produce identical answers.
54
55 Note:
56 If the symengine code has been updated, you likely need to update the JAX
57 by hand. We had trouble code generating it with good performance.
58 """
59 X_velocity = self.to_velocity_state(X)
60
61 Xdot = self.position_swerve_full_dynamics(X, U)
62 Xdot_jax = jax_dynamics.full_dynamics(self.coefficients, X[:, 0], U[:,
63 0])
justinT21a4fa3e22024-10-12 17:09:50 -070064 self.assertEqual(Xdot.shape[0], Xdot_jax.shape[0])
Austin Schuh76534f32024-09-02 13:52:45 -070065
66 self.assertLess(
67 numpy.linalg.norm(Xdot[:, 0] - Xdot_jax),
68 2e-8,
69 msg=
70 f'Xdot: {Xdot[:, 0]}, Xdot_jax: {Xdot_jax}, diff: {(Xdot[:, 0] - Xdot_jax)}',
71 )
72
73 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
74 velocity_physics_jax = jax_dynamics.velocity_dynamics(
75 self.coefficients, X_velocity[:, 0], U[:, 0])
76
justinT21a4fa3e22024-10-12 17:09:50 -070077 self.assertEqual(velocity_physics.shape[0],
78 velocity_physics_jax.shape[0])
79
Austin Schuh76534f32024-09-02 13:52:45 -070080 self.assertLess(
81 numpy.linalg.norm(velocity_physics[:, 0] - velocity_physics_jax),
82 2e-8,
83 msg=
84 f'Xdot: {velocity_physics[:, 0]}, Xdot_jax: {velocity_physics_jax}, diff: {(velocity_physics[:, 0] - velocity_physics_jax)}',
85 )
86
87 def wrap_and_validate(self, function, i):
88 """Wraps a function, and validates JAX and casadi agree.
89
90 We want to do it every time we check any intermediate, since the tests
91 are designed to test all the corner cases, but they don't all do it
92 through the main dynamics function above.
93 """
94 wrapped_fn = utils.wrap_module(function, i)
95
96 def do(X, U):
97 self.validate_dynamics_equality(X, U)
98 return wrapped_fn(X, U)
99
100 return do
101
Austin Schuh27694fa2024-07-20 16:29:49 -0700102 def wrap(self, python_module):
Austin Schuh76534f32024-09-02 13:52:45 -0700103 # Only update on change to avoid re-jiting things.
104 if self.coefficients.caster != python_module.CASTER:
105 self.coefficients = self.coefficients._replace(
106 caster=python_module.CASTER)
107
Austin Schuh6ea789e2024-07-27 13:45:53 -0700108 self.position_swerve_full_dynamics = utils.wrap(
109 python_module.swerve_full_dynamics)
110
111 evaluated_fn = python_module.velocity_swerve_physics(
112 casadi.SX.sym("X", dynamics.NUM_VELOCITY_STATES, 1),
113 casadi.SX.sym("U", 8, 1))
114 self.velocity_swerve_physics = lambda X, U: numpy.array(
115 evaluated_fn(X, U))
116
Austin Schuhb8b34be2024-07-14 16:06:19 -0700117 self.contact_patch_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700118 self.wrap_and_validate(python_module.contact_patch_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700119 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700120 ]
121 self.wheel_ground_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700122 self.wrap_and_validate(python_module.wheel_ground_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700123 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700124 ]
125 self.wheel_slip_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700126 self.wrap_and_validate(python_module.wheel_slip_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700127 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700128 ]
justinT2107d41ed2024-07-31 21:12:31 -0700129 self.wheel_force = [
Austin Schuh76534f32024-09-02 13:52:45 -0700130 self.wrap_and_validate(python_module.wheel_force, i)
justinT2107d41ed2024-07-31 21:12:31 -0700131 for i in range(4)
132 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700133 self.module_angular_accel = [
134 self.wrap_and_validate(python_module.module_angular_accel, i)
135 for i in range(4)
136 ]
137 self.F = [self.wrap_and_validate(python_module.F, i) for i in range(4)]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700138 self.mounting_location = [
Austin Schuh76534f32024-09-02 13:52:45 -0700139 self.wrap_and_validate(python_module.mounting_location, i)
justinT2107d41ed2024-07-31 21:12:31 -0700140 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700141 ]
142
143 self.slip_angle = [
Austin Schuh76534f32024-09-02 13:52:45 -0700144 self.wrap_and_validate(python_module.slip_angle, i)
145 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700146 ]
147 self.slip_ratio = [
Austin Schuh76534f32024-09-02 13:52:45 -0700148 self.wrap_and_validate(python_module.slip_ratio, i)
149 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700150 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700151 self.Ms = [
152 self.wrap_and_validate(python_module.Ms, i) for i in range(4)
153 ]
Austin Schuh27694fa2024-07-20 16:29:49 -0700154
155 def setUp(self):
156 self.wrap(dynamics)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700157
158 def test_contact_patch_velocity(self):
159 """Tests that the contact patch velocity makes sense."""
160 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700161 contact_patch_velocity = self.contact_patch_velocity[i]
162 wheel_ground_velocity = self.wheel_ground_velocity[i]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700163
164 # No angular velocity should result in just linear motion.
165 for velocity in [
166 numpy.array([[1.5], [0.0]]),
167 numpy.array([[0.0], [1.0]]),
168 numpy.array([[-1.5], [0.0]]),
169 numpy.array([[0.0], [-1.0]]),
170 numpy.array([[2.0], [-1.7]]),
171 ]:
172 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -0700173 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700174 utils.state_vector(velocity=velocity, theta=theta),
175 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700176
177 assert_array_equal(patch_velocity, velocity)
178
179 # Now, test that spinning the robot results in module velocities.
180 # We are assuming that each module is on a square robot.
181 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
182 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
183 for omega in [0.65, -0.1]:
184 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700185 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700186 utils.state_vector(
187 velocity=numpy.array([[0.0], [0.0]]),
188 theta=theta,
189 omega=omega,
190 module_angle=module_center_of_mass_angle,
191 ),
192 self.I,
193 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700194
195 assert_array_almost_equal(
196 patch_velocity,
Austin Schuh6927bc32024-07-14 17:24:56 -0700197 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
justinT2107d41ed2024-07-31 21:12:31 -0700198 dynamics.CASTER) * omega * numpy.array([
199 [-numpy.sin(theta + module_center_of_mass_angle)],
200 [numpy.cos(theta + module_center_of_mass_angle)],
201 ]),
202 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700203
204 # Point the wheel along +x, rotate it by theta, then spin it.
205 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700206 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700207 utils.state_vector(
Austin Schuhb8b34be2024-07-14 16:06:19 -0700208 velocity=numpy.array([[0.0], [0.0]]),
209 theta=-module_center_of_mass_angle,
210 module_omega=omega,
justinT2107d41ed2024-07-31 21:12:31 -0700211 module_angle=(theta + module_center_of_mass_angle),
212 ),
213 self.I,
214 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700215
216 assert_array_almost_equal(
justinT2107d41ed2024-07-31 21:12:31 -0700217 patch_velocity,
218 -dynamics.CASTER * omega *
219 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]),
220 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700221
222 # Now, test that the rotation back to wheel coordinates works.
223 # The easiest way to do this is to point the wheel in a direction,
224 # move in that direction, and confirm that there is no lateral velocity.
225 for robot_angle in [0.0, 1.0, -5.0]:
226 for module_angle in [0.0, 1.0, -5.0]:
227 wheel_patch_velocity = numpy.array(
228 wheel_ground_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700229 utils.state_vector(
230 velocity=numpy.array([
231 [numpy.cos(robot_angle + module_angle)],
232 [numpy.sin(robot_angle + module_angle)],
233 ]),
234 theta=robot_angle,
235 module_angle=module_angle,
236 ),
237 self.I,
238 ))
Austin Schuh2a1abec2024-07-10 20:31:16 -0700239
240 assert_array_almost_equal(wheel_patch_velocity,
241 numpy.array([[1], [0]]))
242
243 def test_slip_angle(self):
244 """Tests that the slip_angle calculation works."""
245 velocity = numpy.array([[1.5], [0.0]])
246
247 for i in range(4):
Austin Schuh27694fa2024-07-20 16:29:49 -0700248 for wrap in range(-1, 2):
Austin Schuh2a1abec2024-07-10 20:31:16 -0700249 for theta in [0.0, 0.6, -0.4]:
250 module_angle = numpy.pi * wrap + theta
251
Austin Schuhbd75c482024-08-18 00:03:51 -0700252 # We have redefined the angle to be the softened sin of the angle.
Austin Schuh27694fa2024-07-20 16:29:49 -0700253 # That way, when the module flips directions, the slip angle also flips
254 # directions to keep it stable.
justinT2107d41ed2024-07-31 21:12:31 -0700255 computed_angle = self.slip_angle[i](
256 utils.state_vector(velocity=velocity,
Austin Schuhbd75c482024-08-18 00:03:51 -0700257 module_angle=module_angle),
justinT2107d41ed2024-07-31 21:12:31 -0700258 self.I,
259 )[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700260
Austin Schuhbd75c482024-08-18 00:03:51 -0700261 # Compute out the expected value directly to confirm we got it right.
262 loggain = 20.0
263 vy = 1.5 * numpy.sin(-module_angle)
264 vx = 1.5 * numpy.cos(-module_angle)
265 expected = numpy.sin(-numpy.arctan2(
266 vy,
267 scipy.special.logsumexp([1.0, abs(vx) * loggain]) /
268 loggain))
Austin Schuh27694fa2024-07-20 16:29:49 -0700269
Austin Schuh76534f32024-09-02 13:52:45 -0700270 jax_expected = jax.numpy.sin(
271 -jax_dynamics.soft_atan2(vy, vx))
272
273 self.assertAlmostEqual(
274 expected,
275 jax_expected,
276 msg=f"Trying wrap {wrap} theta {theta}",
277 )
278
Austin Schuh27694fa2024-07-20 16:29:49 -0700279 self.assertAlmostEqual(
280 expected,
281 computed_angle,
justinT2107d41ed2024-07-31 21:12:31 -0700282 msg=f"Trying wrap {wrap} theta {theta}",
283 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700284
Austin Schuhb8b34be2024-07-14 16:06:19 -0700285 def test_wheel_torque(self):
286 """Tests that the per module self aligning forces have the right signs."""
Austin Schuh27694fa2024-07-20 16:29:49 -0700287 # Point all the modules in a little bit.
justinT2107d41ed2024-07-31 21:12:31 -0700288 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700289 velocity=numpy.array([[1.0], [0.0]]),
290 module_angles=[-0.001, -0.001, 0.001, 0.001],
291 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700292 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700293
294 self.assertGreater(xdot_equal[2, 0], 0.0)
295 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
296 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
297
298 self.assertGreater(xdot_equal[6, 0], 0.0)
299 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
300 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
301
302 self.assertLess(xdot_equal[10, 0], 0.0)
303 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
304 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
305
306 self.assertLess(xdot_equal[14, 0], 0.0)
307 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
308 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
309
310 # Shouldn't be spinning.
311 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
312
Austin Schuh27694fa2024-07-20 16:29:49 -0700313 # Now, make the bot want to go left by going to the other side.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700314 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700315 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700316 velocity=numpy.array([[1.0], [0.0]]),
317 module_angles=[0.01, 0.01, 0.01, 0.01],
318 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700319 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700320
Austin Schuh27694fa2024-07-20 16:29:49 -0700321 self.assertLess(xdot_left[2, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700322 self.assertLess(xdot_left[3, 0], 0.0)
323 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
324
Austin Schuh27694fa2024-07-20 16:29:49 -0700325 self.assertLess(xdot_left[6, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700326 self.assertLess(xdot_left[7, 0], 0.0)
327 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
328
Austin Schuh27694fa2024-07-20 16:29:49 -0700329 self.assertLess(xdot_left[10, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700330 self.assertLess(xdot_left[11, 0], 0.0)
331 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
332
Austin Schuh27694fa2024-07-20 16:29:49 -0700333 self.assertLess(xdot_left[14, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700334 self.assertLess(xdot_left[15, 0], 0.0)
335 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
336
337 self.assertGreater(xdot_left[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700338 self.assertGreater(xdot_left[20, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700339 # Shouldn't be spinning.
340 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
341
342 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700343 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700344 velocity=numpy.array([[1.0], [0.0]]),
345 module_angles=[-0.01, -0.01, -0.01, -0.01],
346 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700347 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700348
Austin Schuh27694fa2024-07-20 16:29:49 -0700349 self.assertGreater(xdot_right[2, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700350 self.assertLess(xdot_right[3, 0], 0.0)
351 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
352
Austin Schuh27694fa2024-07-20 16:29:49 -0700353 self.assertGreater(xdot_right[6, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700354 self.assertLess(xdot_right[7, 0], 0.0)
355 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
356
Austin Schuh27694fa2024-07-20 16:29:49 -0700357 self.assertGreater(xdot_right[10, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700358 self.assertLess(xdot_right[11, 0], 0.0)
359 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
360
Austin Schuh27694fa2024-07-20 16:29:49 -0700361 self.assertGreater(xdot_right[14, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700362 self.assertLess(xdot_right[15, 0], 0.0)
363 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
364
365 self.assertGreater(xdot_right[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700366 self.assertLess(xdot_right[20, 0], -0.05)
367 # Shouldn't be spinning.
368 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
369
370 def test_wheel_torque_backwards_nocaster(self):
371 """Tests that the per module self aligning forces have the right signs when going backwards."""
372 self.wrap(nocaster_dynamics)
373 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700374 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700375 velocity=numpy.array([[1.0], [0.0]]),
376 module_angles=[
377 numpy.pi - 0.001,
378 numpy.pi - 0.001,
379 numpy.pi + 0.001,
380 numpy.pi + 0.001,
381 ],
382 drive_wheel_velocity=-1,
383 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700384 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700385
386 self.assertGreater(xdot_equal[2, 0], 0.0, msg="Steering backwards")
387 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
388 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
389
390 self.assertGreater(xdot_equal[6, 0], 0.0, msg="Steering backwards")
391 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
392 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
393
394 self.assertLess(xdot_equal[10, 0], 0.0, msg="Steering backwards")
395 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
396 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
397
398 self.assertLess(xdot_equal[14, 0], 0.0, msg="Steering backwards")
399 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
400 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
401
402 # Shouldn't be spinning.
403 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
404
405 # Now, make the bot want to go left by going to the other side.
406 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700407 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700408 velocity=numpy.array([[1.0], [0.0]]),
409 module_angles=[numpy.pi + 0.01] * 4,
410 drive_wheel_velocity=-1,
411 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700412 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700413
414 self.assertLess(xdot_left[2, 0], -0.05)
415 self.assertGreater(xdot_left[3, 0], 0.0)
416 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
417
418 self.assertLess(xdot_left[6, 0], -0.05)
419 self.assertGreater(xdot_left[7, 0], 0.0)
420 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
421
422 self.assertLess(xdot_left[10, 0], -0.05)
423 self.assertGreater(xdot_left[11, 0], 0.0)
424 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
425
426 self.assertLess(xdot_left[14, 0], -0.05)
427 self.assertGreater(xdot_left[15, 0], 0.0)
428 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
429
430 self.assertGreater(xdot_left[19, 0], 0.0001)
431 self.assertGreater(xdot_left[20, 0], 0.05)
432 # Shouldn't be spinning.
433 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
434
435 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700436 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700437 velocity=numpy.array([[1.0], [0.0]]),
438 drive_wheel_velocity=-1,
439 module_angles=[-0.01 + numpy.pi] * 4,
440 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700441 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700442
443 self.assertGreater(xdot_right[2, 0], 0.05)
444 self.assertGreater(xdot_right[3, 0], 0.0)
445 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
446
447 self.assertGreater(xdot_right[6, 0], 0.05)
448 self.assertGreater(xdot_right[7, 0], 0.0)
449 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
450
451 self.assertGreater(xdot_right[10, 0], 0.05)
452 self.assertGreater(xdot_right[11, 0], 0.0)
453 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
454
455 self.assertGreater(xdot_right[14, 0], 0.05)
456 self.assertGreater(xdot_right[15, 0], 0.0)
457 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
458
459 self.assertGreater(xdot_right[19, 0], 0.0001)
460 self.assertLess(xdot_right[20, 0], -0.05)
461 # Shouldn't be spinning.
462 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
463
464 def test_wheel_torque_backwards_caster(self):
465 """Tests that the per module self aligning forces have the right signs when going backwards with a lot of caster."""
466 self.wrap(bigcaster_dynamics)
467 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700468 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700469 velocity=numpy.array([[1.0], [0.0]]),
470 module_angles=[
471 numpy.pi - 0.001,
472 numpy.pi - 0.001,
473 numpy.pi + 0.001,
474 numpy.pi + 0.001,
475 ],
476 drive_wheel_velocity=-1,
477 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700478 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700479
480 self.assertLess(xdot_equal[2, 0], 0.0, msg="Steering backwards")
481 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
482 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
483
484 self.assertLess(xdot_equal[6, 0], 0.0, msg="Steering backwards")
485 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
486 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
487
488 self.assertGreater(xdot_equal[10, 0], 0.0, msg="Steering backwards")
489 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
490 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
491
492 self.assertGreater(xdot_equal[14, 0], 0.0, msg="Steering backwards")
493 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
494 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
495
496 # Shouldn't be spinning.
497 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
498
499 # Now, make the bot want to go left by going to the other side.
500 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700501 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700502 velocity=numpy.array([[1.0], [0.0]]),
503 module_angles=[numpy.pi + 0.01] * 4,
504 drive_wheel_velocity=-1,
505 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700506 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700507
508 self.assertGreater(xdot_left[2, 0], -0.05)
509 self.assertGreater(xdot_left[3, 0], 0.0)
510 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
511
512 self.assertGreater(xdot_left[6, 0], -0.05)
513 self.assertGreater(xdot_left[7, 0], 0.0)
514 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
515
516 self.assertGreater(xdot_left[10, 0], -0.05)
517 self.assertGreater(xdot_left[11, 0], 0.0)
518 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
519
520 self.assertGreater(xdot_left[14, 0], -0.05)
521 self.assertGreater(xdot_left[15, 0], 0.0)
522 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
523
524 self.assertGreater(xdot_left[19, 0], 0.0001)
525 self.assertGreater(xdot_left[20, 0], 0.05)
526 # Shouldn't be spinning.
527 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
528
529 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700530 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700531 velocity=numpy.array([[1.0], [0.0]]),
532 drive_wheel_velocity=-1,
533 module_angles=[-0.01 + numpy.pi] * 4,
534 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700535 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700536
537 self.assertLess(xdot_right[2, 0], 0.05)
538 self.assertGreater(xdot_right[3, 0], 0.0)
539 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
540
541 self.assertLess(xdot_right[6, 0], 0.05)
542 self.assertGreater(xdot_right[7, 0], 0.0)
543 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
544
545 self.assertLess(xdot_right[10, 0], 0.05)
546 self.assertGreater(xdot_right[11, 0], 0.0)
547 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
548
549 self.assertLess(xdot_right[14, 0], 0.05)
550 self.assertGreater(xdot_right[15, 0], 0.0)
551 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
552
553 self.assertGreater(xdot_right[19, 0], 0.0001)
554 self.assertLess(xdot_right[20, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700555 # Shouldn't be spinning.
556 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
557
Austin Schuh2a1abec2024-07-10 20:31:16 -0700558 def test_wheel_forces(self):
559 """Tests that the per module forces have the right signs."""
560 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700561 wheel_force = self.wheel_force[i]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700562
justinT2107d41ed2024-07-31 21:12:31 -0700563 X = utils.state_vector()
Austin Schuhb8b34be2024-07-14 16:06:19 -0700564 robot_equal = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700565 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700566 self.assertEqual(robot_equal[0, 0], 0.0)
567 self.assertEqual(robot_equal[1, 0], 0.0)
568 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
569 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700570
571 # Robot is moving faster than the wheels, it should decelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700572 X = utils.state_vector(dx=0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700573 robot_faster = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700574 xdot_faster = self.swerve_full_dynamics(X,
575 self.I,
576 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700577 self.assertLess(robot_faster[0, 0], -0.1)
578 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700579 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700580
581 # Robot is now going slower than the wheels. It should accelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700582 X = utils.state_vector(dx=-0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700583 robot_slower = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700584 xdot_slower = self.swerve_full_dynamics(X,
585 self.I,
586 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700587 self.assertGreater(robot_slower[0, 0], 0.1)
588 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700589 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700590
591 # Positive lateral velocity -> negative force.
justinT2107d41ed2024-07-31 21:12:31 -0700592 robot_left = wheel_force(utils.state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700593 self.assertEqual(robot_left[0, 0], 0.0)
594 self.assertLess(robot_left[1, 0], -0.1)
595
596 # Negative lateral velocity -> positive force.
justinT2107d41ed2024-07-31 21:12:31 -0700597 robot_right = wheel_force(utils.state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700598 self.assertEqual(robot_right[0, 0], 0.0)
599 self.assertGreater(robot_right[1, 0], 0.1)
600
Austin Schuh3233d7f2024-07-27 17:51:52 -0700601 def test_steer_accel(self):
602 """Tests that applying a steer torque accelerates the steer reasonably."""
603
604 for velocity in [
605 numpy.array([[0.0], [0.0]]),
606 numpy.array([[1.0], [0.0]]),
607 numpy.array([[2.0], [0.0]])
608 ]:
609 module_angles = [0.0] * 4
610
611 X = utils.state_vector(
612 velocity=velocity,
613 omega=0.0,
614 module_angles=module_angles,
615 )
616 steer_I = numpy.array([[1.0], [0.0]] * 4)
617 xdot = self.swerve_full_dynamics(X, steer_I)
618
619 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS0, 0],
620 1.4,
621 places=0)
622 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS1, 0],
623 1.4,
624 places=0)
625 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS2, 0],
626 1.4,
627 places=0)
628 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS3, 0],
629 1.4,
630 places=0)
631
632 def test_drive_accel(self):
633 """Tests that applying a drive torque accelerates the drive reasonably."""
634
635 for velocity in [
636 numpy.array([[0.01], [0.0]]),
637 numpy.array([[1.0], [0.0]]),
638 numpy.array([[2.0], [0.0]])
639 ]:
640 module_angles = [0.0] * 4
641
642 X = utils.state_vector(
643 velocity=velocity,
644 omega=0.0,
645 module_angles=module_angles,
646 )
647 steer_I = numpy.array([[0.0], [100.0]] * 4)
648 # 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.
649 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
650
651 self.assertGreater(xdot[dynamics.STATE_OMEGAD0, 0], 100.0)
652 self.assertGreater(xdot[dynamics.STATE_OMEGAD1, 0], 100.0)
653 self.assertGreater(xdot[dynamics.STATE_OMEGAD2, 0], 100.0)
654 self.assertGreater(xdot[dynamics.STATE_OMEGAD3, 0], 100.0)
655
656 X = utils.state_vector(
657 velocity=velocity,
658 omega=0.0,
659 module_angles=module_angles,
660 )
661 steer_I = numpy.array([[0.0], [-100.0]] * 4)
662 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
663
664 self.assertLess(xdot[dynamics.STATE_OMEGAD0, 0], -100.0)
665 self.assertLess(xdot[dynamics.STATE_OMEGAD1, 0], -100.0)
666 self.assertLess(xdot[dynamics.STATE_OMEGAD2, 0], -100.0)
667 self.assertLess(xdot[dynamics.STATE_OMEGAD3, 0], -100.0)
668
Austin Schuh98fbbe82024-08-18 23:07:26 -0700669 def test_steer_coupling(self):
670 """Tests that the steer coupling factor cancels out steer coupling torque."""
671 steer_I = numpy.array(
672 [[dynamics.STEER_CURRENT_COUPLING_FACTOR * 10.0], [10.0]] * 4)
673
674 X = utils.state_vector(
675 velocity=numpy.array([[0.0], [0.0]]),
676 omega=0.0,
677 )
678 X_velocity = self.to_velocity_state(X)
679 Xdot = self.velocity_swerve_physics(X_velocity, steer_I)
680
681 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS0, 0], 0.0)
682 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS1, 0], 0.0)
683 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS2, 0], 0.0)
684 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS3, 0], 0.0)
685
justinT21d18f79f2024-09-22 19:43:05 -0700686 def test_constant_torque(self):
687 """Tests that the robot exerts the same amount of torque no matter the orientation"""
688 steer_I = numpy.reshape(numpy.array([(i % 2) * 20 for i in range(8)]),
689 (8, 1))
690
691 X = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
692 omega=0.0,
693 module_angles=[
694 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
695 -numpy.pi / 4.0, numpy.pi / 4.0
696 ],
697 drive_wheel_velocity=1.0)
698 Xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
699
700 X_rot = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
701 omega=0.0,
702 theta=numpy.pi,
703 module_angles=[
704 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
705 -numpy.pi / 4.0, numpy.pi / 4.0
706 ],
707 drive_wheel_velocity=1.0)
708 Xdot_rot = self.swerve_full_dynamics(X_rot, steer_I, skip_compare=True)
709
710 self.assertGreater(Xdot[dynamics.STATE_OMEGA, 0], 0.0)
Austin Schuh5dac2292024-10-19 13:56:58 -0700711 self.assertAlmostEqual(Xdot[dynamics.STATE_OMEGA, 0],
712 Xdot_rot[dynamics.STATE_OMEGA, 0])
713
714 def test_cost_equality(self):
715 """Tests that the casadi and jax cost functions match."""
716 mpc = casadi_velocity_mpc_lib.MPC(jit=False)
717 cost = mpc.make_cost()
718
719 for i in range(10):
720 X = numpy.random.uniform(size=(dynamics.NUM_VELOCITY_STATES, ))
721 U = numpy.random.uniform(low=-10, high=10, size=(8, ))
722 R = numpy.random.uniform(low=-1, high=1, size=(3, ))
723
724 J = numpy.array(cost(X, U, R))[0, 0]
725 jax_J = jax_dynamics.mpc_cost(self.coefficients, X, U, R)
726
727 self.assertAlmostEqual(J, jax_J)
728
729 R = jax.numpy.array([0.0, 0.0, 1.0])
730
731 # Now try spinning in place and make sure the cost doesn't change.
732 # This tells us if we got our rotations right.
733 steer_I = numpy.array([(i % 2) * 20 for i in range(8)])
734
735 X = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
736 omega=0.0,
737 module_angles=[
738 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
739 -numpy.pi / 4.0, numpy.pi / 4.0
740 ],
741 drive_wheel_velocity=1.0)
742
743 jax_J_orig = jax_dynamics.mpc_cost(self.coefficients,
744 self.to_velocity_state(X)[:, 0],
745 steer_I, R)
746
747 X_rotated = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
748 omega=0.0,
749 theta=numpy.pi,
750 module_angles=[
751 3 * numpy.pi / 4.0,
752 -3 * numpy.pi / 4.0,
753 -numpy.pi / 4.0, numpy.pi / 4.0
754 ],
755 drive_wheel_velocity=1.0)
756 jax_J_rotated = jax_dynamics.mpc_cost(
757 self.coefficients,
758 self.to_velocity_state(X_rotated)[:, 0], steer_I, R)
759
760 self.assertAlmostEqual(jax_J_orig, jax_J_rotated)
justinT21d18f79f2024-09-22 19:43:05 -0700761
James Kuszmaulef14ab42024-09-14 15:05:24 -0700762 def test_cpp_consistency(self):
763 """Tests that the C++ physics are consistent with the Python physics."""
764 # TODO(james): Currently the physics only match at X = 0 and U = 0.
765 # Fix this.
766 # Maybe due to different atan2 implementations?
767 # TODO(james): Fold this into the general comparisons for JAX versus
768 # casadi once the physics actually match.
769 for current in [0]:
770 print(f"Current: {current}")
771 steer_I = numpy.zeros((8, 1)) + current
772 for state_values in [0.0]:
773 print(f"States all set to: {state_values}")
774 X = numpy.zeros((dynamics.NUM_STATES, 1)) + state_values
775 Xdot_py = self.swerve_full_dynamics(X,
776 steer_I,
777 skip_compare=True)
778 Xdot_cpp = numpy.array(
779 cpp_dynamics(X.flatten().tolist(),
780 steer_I.flatten().tolist())).reshape((25, 1))
781 for index in range(dynamics.NUM_STATES):
782 self.assertAlmostEqual(Xdot_py[index, 0], Xdot_cpp[index,
783 0])
784
Austin Schuh2a1abec2024-07-10 20:31:16 -0700785
justinT2107d41ed2024-07-31 21:12:31 -0700786if __name__ == "__main__":
Austin Schuh2a1abec2024-07-10 20:31:16 -0700787 unittest.main()