blob: 31b96c9128a870c2f55824d1465a92d0456ad120 [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 Schuh76534f32024-09-02 13:52:45 -070021from frc971.control_loops.swerve import jax_dynamics
James Kuszmaulef14ab42024-09-14 15:05:24 -070022from frc971.control_loops.swerve.cpp_dynamics import swerve_dynamics as cpp_dynamics
Austin Schuhb8b34be2024-07-14 16:06:19 -070023
24
Austin Schuh6927bc32024-07-14 17:24:56 -070025class TestSwervePhysics(unittest.TestCase):
Austin Schuh2a1abec2024-07-10 20:31:16 -070026 I = numpy.zeros((8, 1))
Austin Schuh76534f32024-09-02 13:52:45 -070027 coefficients = jax_dynamics.Coefficients()
Austin Schuh2a1abec2024-07-10 20:31:16 -070028
Austin Schuh6ea789e2024-07-27 13:45:53 -070029 def to_velocity_state(self, X):
30 return dynamics.to_velocity_state(X)
31
32 def swerve_full_dynamics(self, X, U, skip_compare=False):
33 X_velocity = self.to_velocity_state(X)
34 Xdot = self.position_swerve_full_dynamics(X, U)
Austin Schuh76534f32024-09-02 13:52:45 -070035
Austin Schuh6ea789e2024-07-27 13:45:53 -070036 if not skip_compare:
Austin Schuh76534f32024-09-02 13:52:45 -070037 Xdot_velocity = self.to_velocity_state(Xdot)
Austin Schuh6ea789e2024-07-27 13:45:53 -070038 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
Austin Schuh76534f32024-09-02 13:52:45 -070039
Austin Schuh6ea789e2024-07-27 13:45:53 -070040 self.assertLess(
Austin Schuh76534f32024-09-02 13:52:45 -070041 numpy.linalg.norm(Xdot_velocity - velocity_physics),
Austin Schuh6ea789e2024-07-27 13:45:53 -070042 2e-2,
43 msg=
Austin Schuh76534f32024-09-02 13:52:45 -070044 f'Norm failed, full physics -> {X_velocity.T}, velocity physics -> {velocity_physics}, difference -> {velocity_physics - X_velocity}',
Austin Schuh6ea789e2024-07-27 13:45:53 -070045 )
46
Austin Schuh76534f32024-09-02 13:52:45 -070047 self.validate_dynamics_equality(X, U)
48
Austin Schuh6ea789e2024-07-27 13:45:53 -070049 return Xdot
50
Austin Schuh76534f32024-09-02 13:52:45 -070051 def validate_dynamics_equality(self, X, U):
52 """Tests that both the JAX code and casadi code produce identical answers.
53
54 Note:
55 If the symengine code has been updated, you likely need to update the JAX
56 by hand. We had trouble code generating it with good performance.
57 """
58 X_velocity = self.to_velocity_state(X)
59
60 Xdot = self.position_swerve_full_dynamics(X, U)
61 Xdot_jax = jax_dynamics.full_dynamics(self.coefficients, X[:, 0], U[:,
62 0])
63
64 self.assertLess(
65 numpy.linalg.norm(Xdot[:, 0] - Xdot_jax),
66 2e-8,
67 msg=
68 f'Xdot: {Xdot[:, 0]}, Xdot_jax: {Xdot_jax}, diff: {(Xdot[:, 0] - Xdot_jax)}',
69 )
70
71 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
72 velocity_physics_jax = jax_dynamics.velocity_dynamics(
73 self.coefficients, X_velocity[:, 0], U[:, 0])
74
75 self.assertLess(
76 numpy.linalg.norm(velocity_physics[:, 0] - velocity_physics_jax),
77 2e-8,
78 msg=
79 f'Xdot: {velocity_physics[:, 0]}, Xdot_jax: {velocity_physics_jax}, diff: {(velocity_physics[:, 0] - velocity_physics_jax)}',
80 )
81
82 def wrap_and_validate(self, function, i):
83 """Wraps a function, and validates JAX and casadi agree.
84
85 We want to do it every time we check any intermediate, since the tests
86 are designed to test all the corner cases, but they don't all do it
87 through the main dynamics function above.
88 """
89 wrapped_fn = utils.wrap_module(function, i)
90
91 def do(X, U):
92 self.validate_dynamics_equality(X, U)
93 return wrapped_fn(X, U)
94
95 return do
96
Austin Schuh27694fa2024-07-20 16:29:49 -070097 def wrap(self, python_module):
Austin Schuh76534f32024-09-02 13:52:45 -070098 # Only update on change to avoid re-jiting things.
99 if self.coefficients.caster != python_module.CASTER:
100 self.coefficients = self.coefficients._replace(
101 caster=python_module.CASTER)
102
Austin Schuh6ea789e2024-07-27 13:45:53 -0700103 self.position_swerve_full_dynamics = utils.wrap(
104 python_module.swerve_full_dynamics)
105
106 evaluated_fn = python_module.velocity_swerve_physics(
107 casadi.SX.sym("X", dynamics.NUM_VELOCITY_STATES, 1),
108 casadi.SX.sym("U", 8, 1))
109 self.velocity_swerve_physics = lambda X, U: numpy.array(
110 evaluated_fn(X, U))
111
Austin Schuhb8b34be2024-07-14 16:06:19 -0700112 self.contact_patch_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700113 self.wrap_and_validate(python_module.contact_patch_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700114 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700115 ]
116 self.wheel_ground_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700117 self.wrap_and_validate(python_module.wheel_ground_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700118 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700119 ]
120 self.wheel_slip_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700121 self.wrap_and_validate(python_module.wheel_slip_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700122 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700123 ]
justinT2107d41ed2024-07-31 21:12:31 -0700124 self.wheel_force = [
Austin Schuh76534f32024-09-02 13:52:45 -0700125 self.wrap_and_validate(python_module.wheel_force, i)
justinT2107d41ed2024-07-31 21:12:31 -0700126 for i in range(4)
127 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700128 self.module_angular_accel = [
129 self.wrap_and_validate(python_module.module_angular_accel, i)
130 for i in range(4)
131 ]
132 self.F = [self.wrap_and_validate(python_module.F, i) for i in range(4)]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700133 self.mounting_location = [
Austin Schuh76534f32024-09-02 13:52:45 -0700134 self.wrap_and_validate(python_module.mounting_location, i)
justinT2107d41ed2024-07-31 21:12:31 -0700135 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700136 ]
137
138 self.slip_angle = [
Austin Schuh76534f32024-09-02 13:52:45 -0700139 self.wrap_and_validate(python_module.slip_angle, i)
140 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700141 ]
142 self.slip_ratio = [
Austin Schuh76534f32024-09-02 13:52:45 -0700143 self.wrap_and_validate(python_module.slip_ratio, i)
144 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700145 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700146 self.Ms = [
147 self.wrap_and_validate(python_module.Ms, i) for i in range(4)
148 ]
Austin Schuh27694fa2024-07-20 16:29:49 -0700149
150 def setUp(self):
151 self.wrap(dynamics)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700152
153 def test_contact_patch_velocity(self):
154 """Tests that the contact patch velocity makes sense."""
155 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700156 contact_patch_velocity = self.contact_patch_velocity[i]
157 wheel_ground_velocity = self.wheel_ground_velocity[i]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700158
159 # No angular velocity should result in just linear motion.
160 for velocity in [
161 numpy.array([[1.5], [0.0]]),
162 numpy.array([[0.0], [1.0]]),
163 numpy.array([[-1.5], [0.0]]),
164 numpy.array([[0.0], [-1.0]]),
165 numpy.array([[2.0], [-1.7]]),
166 ]:
167 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -0700168 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700169 utils.state_vector(velocity=velocity, theta=theta),
170 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700171
172 assert_array_equal(patch_velocity, velocity)
173
174 # Now, test that spinning the robot results in module velocities.
175 # We are assuming that each module is on a square robot.
176 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
177 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
178 for omega in [0.65, -0.1]:
179 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700180 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700181 utils.state_vector(
182 velocity=numpy.array([[0.0], [0.0]]),
183 theta=theta,
184 omega=omega,
185 module_angle=module_center_of_mass_angle,
186 ),
187 self.I,
188 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700189
190 assert_array_almost_equal(
191 patch_velocity,
Austin Schuh6927bc32024-07-14 17:24:56 -0700192 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
justinT2107d41ed2024-07-31 21:12:31 -0700193 dynamics.CASTER) * omega * numpy.array([
194 [-numpy.sin(theta + module_center_of_mass_angle)],
195 [numpy.cos(theta + module_center_of_mass_angle)],
196 ]),
197 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700198
199 # Point the wheel along +x, rotate it by theta, then spin it.
200 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700201 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700202 utils.state_vector(
Austin Schuhb8b34be2024-07-14 16:06:19 -0700203 velocity=numpy.array([[0.0], [0.0]]),
204 theta=-module_center_of_mass_angle,
205 module_omega=omega,
justinT2107d41ed2024-07-31 21:12:31 -0700206 module_angle=(theta + module_center_of_mass_angle),
207 ),
208 self.I,
209 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700210
211 assert_array_almost_equal(
justinT2107d41ed2024-07-31 21:12:31 -0700212 patch_velocity,
213 -dynamics.CASTER * omega *
214 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]),
215 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700216
217 # Now, test that the rotation back to wheel coordinates works.
218 # The easiest way to do this is to point the wheel in a direction,
219 # move in that direction, and confirm that there is no lateral velocity.
220 for robot_angle in [0.0, 1.0, -5.0]:
221 for module_angle in [0.0, 1.0, -5.0]:
222 wheel_patch_velocity = numpy.array(
223 wheel_ground_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700224 utils.state_vector(
225 velocity=numpy.array([
226 [numpy.cos(robot_angle + module_angle)],
227 [numpy.sin(robot_angle + module_angle)],
228 ]),
229 theta=robot_angle,
230 module_angle=module_angle,
231 ),
232 self.I,
233 ))
Austin Schuh2a1abec2024-07-10 20:31:16 -0700234
235 assert_array_almost_equal(wheel_patch_velocity,
236 numpy.array([[1], [0]]))
237
238 def test_slip_angle(self):
239 """Tests that the slip_angle calculation works."""
240 velocity = numpy.array([[1.5], [0.0]])
241
242 for i in range(4):
Austin Schuh27694fa2024-07-20 16:29:49 -0700243 for wrap in range(-1, 2):
Austin Schuh2a1abec2024-07-10 20:31:16 -0700244 for theta in [0.0, 0.6, -0.4]:
245 module_angle = numpy.pi * wrap + theta
246
Austin Schuhbd75c482024-08-18 00:03:51 -0700247 # We have redefined the angle to be the softened sin of the angle.
Austin Schuh27694fa2024-07-20 16:29:49 -0700248 # That way, when the module flips directions, the slip angle also flips
249 # directions to keep it stable.
justinT2107d41ed2024-07-31 21:12:31 -0700250 computed_angle = self.slip_angle[i](
251 utils.state_vector(velocity=velocity,
Austin Schuhbd75c482024-08-18 00:03:51 -0700252 module_angle=module_angle),
justinT2107d41ed2024-07-31 21:12:31 -0700253 self.I,
254 )[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700255
Austin Schuhbd75c482024-08-18 00:03:51 -0700256 # Compute out the expected value directly to confirm we got it right.
257 loggain = 20.0
258 vy = 1.5 * numpy.sin(-module_angle)
259 vx = 1.5 * numpy.cos(-module_angle)
260 expected = numpy.sin(-numpy.arctan2(
261 vy,
262 scipy.special.logsumexp([1.0, abs(vx) * loggain]) /
263 loggain))
Austin Schuh27694fa2024-07-20 16:29:49 -0700264
Austin Schuh76534f32024-09-02 13:52:45 -0700265 jax_expected = jax.numpy.sin(
266 -jax_dynamics.soft_atan2(vy, vx))
267
268 self.assertAlmostEqual(
269 expected,
270 jax_expected,
271 msg=f"Trying wrap {wrap} theta {theta}",
272 )
273
Austin Schuh27694fa2024-07-20 16:29:49 -0700274 self.assertAlmostEqual(
275 expected,
276 computed_angle,
justinT2107d41ed2024-07-31 21:12:31 -0700277 msg=f"Trying wrap {wrap} theta {theta}",
278 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700279
Austin Schuhb8b34be2024-07-14 16:06:19 -0700280 def test_wheel_torque(self):
281 """Tests that the per module self aligning forces have the right signs."""
Austin Schuh27694fa2024-07-20 16:29:49 -0700282 # Point all the modules in a little bit.
justinT2107d41ed2024-07-31 21:12:31 -0700283 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700284 velocity=numpy.array([[1.0], [0.0]]),
285 module_angles=[-0.001, -0.001, 0.001, 0.001],
286 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700287 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700288
289 self.assertGreater(xdot_equal[2, 0], 0.0)
290 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
291 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
292
293 self.assertGreater(xdot_equal[6, 0], 0.0)
294 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
295 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
296
297 self.assertLess(xdot_equal[10, 0], 0.0)
298 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
299 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
300
301 self.assertLess(xdot_equal[14, 0], 0.0)
302 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
303 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
304
305 # Shouldn't be spinning.
306 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
307
Austin Schuh27694fa2024-07-20 16:29:49 -0700308 # Now, make the bot want to go left by going to the other side.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700309 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700310 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700311 velocity=numpy.array([[1.0], [0.0]]),
312 module_angles=[0.01, 0.01, 0.01, 0.01],
313 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700314 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700315
Austin Schuh27694fa2024-07-20 16:29:49 -0700316 self.assertLess(xdot_left[2, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700317 self.assertLess(xdot_left[3, 0], 0.0)
318 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
319
Austin Schuh27694fa2024-07-20 16:29:49 -0700320 self.assertLess(xdot_left[6, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700321 self.assertLess(xdot_left[7, 0], 0.0)
322 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
323
Austin Schuh27694fa2024-07-20 16:29:49 -0700324 self.assertLess(xdot_left[10, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700325 self.assertLess(xdot_left[11, 0], 0.0)
326 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
327
Austin Schuh27694fa2024-07-20 16:29:49 -0700328 self.assertLess(xdot_left[14, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700329 self.assertLess(xdot_left[15, 0], 0.0)
330 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
331
332 self.assertGreater(xdot_left[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700333 self.assertGreater(xdot_left[20, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700334 # Shouldn't be spinning.
335 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
336
337 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700338 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700339 velocity=numpy.array([[1.0], [0.0]]),
340 module_angles=[-0.01, -0.01, -0.01, -0.01],
341 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700342 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700343
Austin Schuh27694fa2024-07-20 16:29:49 -0700344 self.assertGreater(xdot_right[2, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700345 self.assertLess(xdot_right[3, 0], 0.0)
346 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
347
Austin Schuh27694fa2024-07-20 16:29:49 -0700348 self.assertGreater(xdot_right[6, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700349 self.assertLess(xdot_right[7, 0], 0.0)
350 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
351
Austin Schuh27694fa2024-07-20 16:29:49 -0700352 self.assertGreater(xdot_right[10, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700353 self.assertLess(xdot_right[11, 0], 0.0)
354 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
355
Austin Schuh27694fa2024-07-20 16:29:49 -0700356 self.assertGreater(xdot_right[14, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700357 self.assertLess(xdot_right[15, 0], 0.0)
358 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
359
360 self.assertGreater(xdot_right[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700361 self.assertLess(xdot_right[20, 0], -0.05)
362 # Shouldn't be spinning.
363 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
364
365 def test_wheel_torque_backwards_nocaster(self):
366 """Tests that the per module self aligning forces have the right signs when going backwards."""
367 self.wrap(nocaster_dynamics)
368 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700369 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700370 velocity=numpy.array([[1.0], [0.0]]),
371 module_angles=[
372 numpy.pi - 0.001,
373 numpy.pi - 0.001,
374 numpy.pi + 0.001,
375 numpy.pi + 0.001,
376 ],
377 drive_wheel_velocity=-1,
378 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700379 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700380
381 self.assertGreater(xdot_equal[2, 0], 0.0, msg="Steering backwards")
382 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
383 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
384
385 self.assertGreater(xdot_equal[6, 0], 0.0, msg="Steering backwards")
386 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
387 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
388
389 self.assertLess(xdot_equal[10, 0], 0.0, msg="Steering backwards")
390 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
391 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
392
393 self.assertLess(xdot_equal[14, 0], 0.0, msg="Steering backwards")
394 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
395 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
396
397 # Shouldn't be spinning.
398 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
399
400 # Now, make the bot want to go left by going to the other side.
401 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700402 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700403 velocity=numpy.array([[1.0], [0.0]]),
404 module_angles=[numpy.pi + 0.01] * 4,
405 drive_wheel_velocity=-1,
406 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700407 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700408
409 self.assertLess(xdot_left[2, 0], -0.05)
410 self.assertGreater(xdot_left[3, 0], 0.0)
411 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
412
413 self.assertLess(xdot_left[6, 0], -0.05)
414 self.assertGreater(xdot_left[7, 0], 0.0)
415 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
416
417 self.assertLess(xdot_left[10, 0], -0.05)
418 self.assertGreater(xdot_left[11, 0], 0.0)
419 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
420
421 self.assertLess(xdot_left[14, 0], -0.05)
422 self.assertGreater(xdot_left[15, 0], 0.0)
423 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
424
425 self.assertGreater(xdot_left[19, 0], 0.0001)
426 self.assertGreater(xdot_left[20, 0], 0.05)
427 # Shouldn't be spinning.
428 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
429
430 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700431 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700432 velocity=numpy.array([[1.0], [0.0]]),
433 drive_wheel_velocity=-1,
434 module_angles=[-0.01 + numpy.pi] * 4,
435 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700436 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700437
438 self.assertGreater(xdot_right[2, 0], 0.05)
439 self.assertGreater(xdot_right[3, 0], 0.0)
440 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
441
442 self.assertGreater(xdot_right[6, 0], 0.05)
443 self.assertGreater(xdot_right[7, 0], 0.0)
444 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
445
446 self.assertGreater(xdot_right[10, 0], 0.05)
447 self.assertGreater(xdot_right[11, 0], 0.0)
448 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
449
450 self.assertGreater(xdot_right[14, 0], 0.05)
451 self.assertGreater(xdot_right[15, 0], 0.0)
452 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
453
454 self.assertGreater(xdot_right[19, 0], 0.0001)
455 self.assertLess(xdot_right[20, 0], -0.05)
456 # Shouldn't be spinning.
457 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
458
459 def test_wheel_torque_backwards_caster(self):
460 """Tests that the per module self aligning forces have the right signs when going backwards with a lot of caster."""
461 self.wrap(bigcaster_dynamics)
462 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700463 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700464 velocity=numpy.array([[1.0], [0.0]]),
465 module_angles=[
466 numpy.pi - 0.001,
467 numpy.pi - 0.001,
468 numpy.pi + 0.001,
469 numpy.pi + 0.001,
470 ],
471 drive_wheel_velocity=-1,
472 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700473 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700474
475 self.assertLess(xdot_equal[2, 0], 0.0, msg="Steering backwards")
476 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
477 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
478
479 self.assertLess(xdot_equal[6, 0], 0.0, msg="Steering backwards")
480 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
481 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
482
483 self.assertGreater(xdot_equal[10, 0], 0.0, msg="Steering backwards")
484 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
485 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
486
487 self.assertGreater(xdot_equal[14, 0], 0.0, msg="Steering backwards")
488 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
489 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
490
491 # Shouldn't be spinning.
492 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
493
494 # Now, make the bot want to go left by going to the other side.
495 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700496 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700497 velocity=numpy.array([[1.0], [0.0]]),
498 module_angles=[numpy.pi + 0.01] * 4,
499 drive_wheel_velocity=-1,
500 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700501 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700502
503 self.assertGreater(xdot_left[2, 0], -0.05)
504 self.assertGreater(xdot_left[3, 0], 0.0)
505 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
506
507 self.assertGreater(xdot_left[6, 0], -0.05)
508 self.assertGreater(xdot_left[7, 0], 0.0)
509 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
510
511 self.assertGreater(xdot_left[10, 0], -0.05)
512 self.assertGreater(xdot_left[11, 0], 0.0)
513 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
514
515 self.assertGreater(xdot_left[14, 0], -0.05)
516 self.assertGreater(xdot_left[15, 0], 0.0)
517 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
518
519 self.assertGreater(xdot_left[19, 0], 0.0001)
520 self.assertGreater(xdot_left[20, 0], 0.05)
521 # Shouldn't be spinning.
522 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
523
524 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700525 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700526 velocity=numpy.array([[1.0], [0.0]]),
527 drive_wheel_velocity=-1,
528 module_angles=[-0.01 + numpy.pi] * 4,
529 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700530 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700531
532 self.assertLess(xdot_right[2, 0], 0.05)
533 self.assertGreater(xdot_right[3, 0], 0.0)
534 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
535
536 self.assertLess(xdot_right[6, 0], 0.05)
537 self.assertGreater(xdot_right[7, 0], 0.0)
538 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
539
540 self.assertLess(xdot_right[10, 0], 0.05)
541 self.assertGreater(xdot_right[11, 0], 0.0)
542 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
543
544 self.assertLess(xdot_right[14, 0], 0.05)
545 self.assertGreater(xdot_right[15, 0], 0.0)
546 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
547
548 self.assertGreater(xdot_right[19, 0], 0.0001)
549 self.assertLess(xdot_right[20, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700550 # Shouldn't be spinning.
551 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
552
Austin Schuh2a1abec2024-07-10 20:31:16 -0700553 def test_wheel_forces(self):
554 """Tests that the per module forces have the right signs."""
555 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700556 wheel_force = self.wheel_force[i]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700557
justinT2107d41ed2024-07-31 21:12:31 -0700558 X = utils.state_vector()
Austin Schuhb8b34be2024-07-14 16:06:19 -0700559 robot_equal = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700560 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700561 self.assertEqual(robot_equal[0, 0], 0.0)
562 self.assertEqual(robot_equal[1, 0], 0.0)
563 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
564 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700565
566 # Robot is moving faster than the wheels, it should decelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700567 X = utils.state_vector(dx=0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700568 robot_faster = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700569 xdot_faster = self.swerve_full_dynamics(X,
570 self.I,
571 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700572 self.assertLess(robot_faster[0, 0], -0.1)
573 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700574 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700575
576 # Robot is now going slower than the wheels. It should accelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700577 X = utils.state_vector(dx=-0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700578 robot_slower = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700579 xdot_slower = self.swerve_full_dynamics(X,
580 self.I,
581 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700582 self.assertGreater(robot_slower[0, 0], 0.1)
583 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700584 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700585
586 # Positive lateral velocity -> negative force.
justinT2107d41ed2024-07-31 21:12:31 -0700587 robot_left = wheel_force(utils.state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700588 self.assertEqual(robot_left[0, 0], 0.0)
589 self.assertLess(robot_left[1, 0], -0.1)
590
591 # Negative lateral velocity -> positive force.
justinT2107d41ed2024-07-31 21:12:31 -0700592 robot_right = wheel_force(utils.state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700593 self.assertEqual(robot_right[0, 0], 0.0)
594 self.assertGreater(robot_right[1, 0], 0.1)
595
Austin Schuh3233d7f2024-07-27 17:51:52 -0700596 def test_steer_accel(self):
597 """Tests that applying a steer torque accelerates the steer reasonably."""
598
599 for velocity in [
600 numpy.array([[0.0], [0.0]]),
601 numpy.array([[1.0], [0.0]]),
602 numpy.array([[2.0], [0.0]])
603 ]:
604 module_angles = [0.0] * 4
605
606 X = utils.state_vector(
607 velocity=velocity,
608 omega=0.0,
609 module_angles=module_angles,
610 )
611 steer_I = numpy.array([[1.0], [0.0]] * 4)
612 xdot = self.swerve_full_dynamics(X, steer_I)
613
614 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS0, 0],
615 1.4,
616 places=0)
617 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS1, 0],
618 1.4,
619 places=0)
620 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS2, 0],
621 1.4,
622 places=0)
623 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS3, 0],
624 1.4,
625 places=0)
626
627 def test_drive_accel(self):
628 """Tests that applying a drive torque accelerates the drive reasonably."""
629
630 for velocity in [
631 numpy.array([[0.01], [0.0]]),
632 numpy.array([[1.0], [0.0]]),
633 numpy.array([[2.0], [0.0]])
634 ]:
635 module_angles = [0.0] * 4
636
637 X = utils.state_vector(
638 velocity=velocity,
639 omega=0.0,
640 module_angles=module_angles,
641 )
642 steer_I = numpy.array([[0.0], [100.0]] * 4)
643 # 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.
644 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
645
646 self.assertGreater(xdot[dynamics.STATE_OMEGAD0, 0], 100.0)
647 self.assertGreater(xdot[dynamics.STATE_OMEGAD1, 0], 100.0)
648 self.assertGreater(xdot[dynamics.STATE_OMEGAD2, 0], 100.0)
649 self.assertGreater(xdot[dynamics.STATE_OMEGAD3, 0], 100.0)
650
651 X = utils.state_vector(
652 velocity=velocity,
653 omega=0.0,
654 module_angles=module_angles,
655 )
656 steer_I = numpy.array([[0.0], [-100.0]] * 4)
657 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
658
659 self.assertLess(xdot[dynamics.STATE_OMEGAD0, 0], -100.0)
660 self.assertLess(xdot[dynamics.STATE_OMEGAD1, 0], -100.0)
661 self.assertLess(xdot[dynamics.STATE_OMEGAD2, 0], -100.0)
662 self.assertLess(xdot[dynamics.STATE_OMEGAD3, 0], -100.0)
663
Austin Schuh98fbbe82024-08-18 23:07:26 -0700664 def test_steer_coupling(self):
665 """Tests that the steer coupling factor cancels out steer coupling torque."""
666 steer_I = numpy.array(
667 [[dynamics.STEER_CURRENT_COUPLING_FACTOR * 10.0], [10.0]] * 4)
668
669 X = utils.state_vector(
670 velocity=numpy.array([[0.0], [0.0]]),
671 omega=0.0,
672 )
673 X_velocity = self.to_velocity_state(X)
674 Xdot = self.velocity_swerve_physics(X_velocity, steer_I)
675
676 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS0, 0], 0.0)
677 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS1, 0], 0.0)
678 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS2, 0], 0.0)
679 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS3, 0], 0.0)
680
justinT21d18f79f2024-09-22 19:43:05 -0700681 def test_constant_torque(self):
682 """Tests that the robot exerts the same amount of torque no matter the orientation"""
683 steer_I = numpy.reshape(numpy.array([(i % 2) * 20 for i in range(8)]),
684 (8, 1))
685
686 X = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
687 omega=0.0,
688 module_angles=[
689 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
690 -numpy.pi / 4.0, numpy.pi / 4.0
691 ],
692 drive_wheel_velocity=1.0)
693 Xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
694
695 X_rot = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
696 omega=0.0,
697 theta=numpy.pi,
698 module_angles=[
699 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
700 -numpy.pi / 4.0, numpy.pi / 4.0
701 ],
702 drive_wheel_velocity=1.0)
703 Xdot_rot = self.swerve_full_dynamics(X_rot, steer_I, skip_compare=True)
704
705 self.assertGreater(Xdot[dynamics.STATE_OMEGA, 0], 0.0)
706 self.assertAlmostEquals(Xdot[dynamics.STATE_OMEGA, 0],
707 Xdot_rot[dynamics.STATE_OMEGA, 0])
708
James Kuszmaulef14ab42024-09-14 15:05:24 -0700709 def test_cpp_consistency(self):
710 """Tests that the C++ physics are consistent with the Python physics."""
711 # TODO(james): Currently the physics only match at X = 0 and U = 0.
712 # Fix this.
713 # Maybe due to different atan2 implementations?
714 # TODO(james): Fold this into the general comparisons for JAX versus
715 # casadi once the physics actually match.
716 for current in [0]:
717 print(f"Current: {current}")
718 steer_I = numpy.zeros((8, 1)) + current
719 for state_values in [0.0]:
720 print(f"States all set to: {state_values}")
721 X = numpy.zeros((dynamics.NUM_STATES, 1)) + state_values
722 Xdot_py = self.swerve_full_dynamics(X,
723 steer_I,
724 skip_compare=True)
725 Xdot_cpp = numpy.array(
726 cpp_dynamics(X.flatten().tolist(),
727 steer_I.flatten().tolist())).reshape((25, 1))
728 for index in range(dynamics.NUM_STATES):
729 self.assertAlmostEqual(Xdot_py[index, 0], Xdot_cpp[index,
730 0])
731
Austin Schuh2a1abec2024-07-10 20:31:16 -0700732
justinT2107d41ed2024-07-31 21:12:31 -0700733if __name__ == "__main__":
Austin Schuh2a1abec2024-07-10 20:31:16 -0700734 unittest.main()