blob: 1da2a314aebd31f3adf7b1448eb3e0705f36aa06 [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])
justinT21a4fa3e22024-10-12 17:09:50 -070063 self.assertEqual(Xdot.shape[0], Xdot_jax.shape[0])
Austin Schuh76534f32024-09-02 13:52:45 -070064
65 self.assertLess(
66 numpy.linalg.norm(Xdot[:, 0] - Xdot_jax),
67 2e-8,
68 msg=
69 f'Xdot: {Xdot[:, 0]}, Xdot_jax: {Xdot_jax}, diff: {(Xdot[:, 0] - Xdot_jax)}',
70 )
71
72 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
73 velocity_physics_jax = jax_dynamics.velocity_dynamics(
74 self.coefficients, X_velocity[:, 0], U[:, 0])
75
justinT21a4fa3e22024-10-12 17:09:50 -070076 self.assertEqual(velocity_physics.shape[0],
77 velocity_physics_jax.shape[0])
78
Austin Schuh76534f32024-09-02 13:52:45 -070079 self.assertLess(
80 numpy.linalg.norm(velocity_physics[:, 0] - velocity_physics_jax),
81 2e-8,
82 msg=
83 f'Xdot: {velocity_physics[:, 0]}, Xdot_jax: {velocity_physics_jax}, diff: {(velocity_physics[:, 0] - velocity_physics_jax)}',
84 )
85
86 def wrap_and_validate(self, function, i):
87 """Wraps a function, and validates JAX and casadi agree.
88
89 We want to do it every time we check any intermediate, since the tests
90 are designed to test all the corner cases, but they don't all do it
91 through the main dynamics function above.
92 """
93 wrapped_fn = utils.wrap_module(function, i)
94
95 def do(X, U):
96 self.validate_dynamics_equality(X, U)
97 return wrapped_fn(X, U)
98
99 return do
100
Austin Schuh27694fa2024-07-20 16:29:49 -0700101 def wrap(self, python_module):
Austin Schuh76534f32024-09-02 13:52:45 -0700102 # Only update on change to avoid re-jiting things.
103 if self.coefficients.caster != python_module.CASTER:
104 self.coefficients = self.coefficients._replace(
105 caster=python_module.CASTER)
106
Austin Schuh6ea789e2024-07-27 13:45:53 -0700107 self.position_swerve_full_dynamics = utils.wrap(
108 python_module.swerve_full_dynamics)
109
110 evaluated_fn = python_module.velocity_swerve_physics(
111 casadi.SX.sym("X", dynamics.NUM_VELOCITY_STATES, 1),
112 casadi.SX.sym("U", 8, 1))
113 self.velocity_swerve_physics = lambda X, U: numpy.array(
114 evaluated_fn(X, U))
115
Austin Schuhb8b34be2024-07-14 16:06:19 -0700116 self.contact_patch_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700117 self.wrap_and_validate(python_module.contact_patch_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_ground_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700121 self.wrap_and_validate(python_module.wheel_ground_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700122 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700123 ]
124 self.wheel_slip_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700125 self.wrap_and_validate(python_module.wheel_slip_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700126 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700127 ]
justinT2107d41ed2024-07-31 21:12:31 -0700128 self.wheel_force = [
Austin Schuh76534f32024-09-02 13:52:45 -0700129 self.wrap_and_validate(python_module.wheel_force, i)
justinT2107d41ed2024-07-31 21:12:31 -0700130 for i in range(4)
131 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700132 self.module_angular_accel = [
133 self.wrap_and_validate(python_module.module_angular_accel, i)
134 for i in range(4)
135 ]
136 self.F = [self.wrap_and_validate(python_module.F, i) for i in range(4)]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700137 self.mounting_location = [
Austin Schuh76534f32024-09-02 13:52:45 -0700138 self.wrap_and_validate(python_module.mounting_location, i)
justinT2107d41ed2024-07-31 21:12:31 -0700139 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700140 ]
141
142 self.slip_angle = [
Austin Schuh76534f32024-09-02 13:52:45 -0700143 self.wrap_and_validate(python_module.slip_angle, i)
144 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700145 ]
146 self.slip_ratio = [
Austin Schuh76534f32024-09-02 13:52:45 -0700147 self.wrap_and_validate(python_module.slip_ratio, i)
148 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700149 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700150 self.Ms = [
151 self.wrap_and_validate(python_module.Ms, i) for i in range(4)
152 ]
Austin Schuh27694fa2024-07-20 16:29:49 -0700153
154 def setUp(self):
155 self.wrap(dynamics)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700156
157 def test_contact_patch_velocity(self):
158 """Tests that the contact patch velocity makes sense."""
159 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700160 contact_patch_velocity = self.contact_patch_velocity[i]
161 wheel_ground_velocity = self.wheel_ground_velocity[i]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700162
163 # No angular velocity should result in just linear motion.
164 for velocity in [
165 numpy.array([[1.5], [0.0]]),
166 numpy.array([[0.0], [1.0]]),
167 numpy.array([[-1.5], [0.0]]),
168 numpy.array([[0.0], [-1.0]]),
169 numpy.array([[2.0], [-1.7]]),
170 ]:
171 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -0700172 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700173 utils.state_vector(velocity=velocity, theta=theta),
174 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700175
176 assert_array_equal(patch_velocity, velocity)
177
178 # Now, test that spinning the robot results in module velocities.
179 # We are assuming that each module is on a square robot.
180 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
181 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
182 for omega in [0.65, -0.1]:
183 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700184 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700185 utils.state_vector(
186 velocity=numpy.array([[0.0], [0.0]]),
187 theta=theta,
188 omega=omega,
189 module_angle=module_center_of_mass_angle,
190 ),
191 self.I,
192 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700193
194 assert_array_almost_equal(
195 patch_velocity,
Austin Schuh6927bc32024-07-14 17:24:56 -0700196 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
justinT2107d41ed2024-07-31 21:12:31 -0700197 dynamics.CASTER) * omega * numpy.array([
198 [-numpy.sin(theta + module_center_of_mass_angle)],
199 [numpy.cos(theta + module_center_of_mass_angle)],
200 ]),
201 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700202
203 # Point the wheel along +x, rotate it by theta, then spin it.
204 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700205 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700206 utils.state_vector(
Austin Schuhb8b34be2024-07-14 16:06:19 -0700207 velocity=numpy.array([[0.0], [0.0]]),
208 theta=-module_center_of_mass_angle,
209 module_omega=omega,
justinT2107d41ed2024-07-31 21:12:31 -0700210 module_angle=(theta + module_center_of_mass_angle),
211 ),
212 self.I,
213 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700214
215 assert_array_almost_equal(
justinT2107d41ed2024-07-31 21:12:31 -0700216 patch_velocity,
217 -dynamics.CASTER * omega *
218 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]),
219 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700220
221 # Now, test that the rotation back to wheel coordinates works.
222 # The easiest way to do this is to point the wheel in a direction,
223 # move in that direction, and confirm that there is no lateral velocity.
224 for robot_angle in [0.0, 1.0, -5.0]:
225 for module_angle in [0.0, 1.0, -5.0]:
226 wheel_patch_velocity = numpy.array(
227 wheel_ground_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700228 utils.state_vector(
229 velocity=numpy.array([
230 [numpy.cos(robot_angle + module_angle)],
231 [numpy.sin(robot_angle + module_angle)],
232 ]),
233 theta=robot_angle,
234 module_angle=module_angle,
235 ),
236 self.I,
237 ))
Austin Schuh2a1abec2024-07-10 20:31:16 -0700238
239 assert_array_almost_equal(wheel_patch_velocity,
240 numpy.array([[1], [0]]))
241
242 def test_slip_angle(self):
243 """Tests that the slip_angle calculation works."""
244 velocity = numpy.array([[1.5], [0.0]])
245
246 for i in range(4):
Austin Schuh27694fa2024-07-20 16:29:49 -0700247 for wrap in range(-1, 2):
Austin Schuh2a1abec2024-07-10 20:31:16 -0700248 for theta in [0.0, 0.6, -0.4]:
249 module_angle = numpy.pi * wrap + theta
250
Austin Schuhbd75c482024-08-18 00:03:51 -0700251 # We have redefined the angle to be the softened sin of the angle.
Austin Schuh27694fa2024-07-20 16:29:49 -0700252 # That way, when the module flips directions, the slip angle also flips
253 # directions to keep it stable.
justinT2107d41ed2024-07-31 21:12:31 -0700254 computed_angle = self.slip_angle[i](
255 utils.state_vector(velocity=velocity,
Austin Schuhbd75c482024-08-18 00:03:51 -0700256 module_angle=module_angle),
justinT2107d41ed2024-07-31 21:12:31 -0700257 self.I,
258 )[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700259
Austin Schuhbd75c482024-08-18 00:03:51 -0700260 # Compute out the expected value directly to confirm we got it right.
261 loggain = 20.0
262 vy = 1.5 * numpy.sin(-module_angle)
263 vx = 1.5 * numpy.cos(-module_angle)
264 expected = numpy.sin(-numpy.arctan2(
265 vy,
266 scipy.special.logsumexp([1.0, abs(vx) * loggain]) /
267 loggain))
Austin Schuh27694fa2024-07-20 16:29:49 -0700268
Austin Schuh76534f32024-09-02 13:52:45 -0700269 jax_expected = jax.numpy.sin(
270 -jax_dynamics.soft_atan2(vy, vx))
271
272 self.assertAlmostEqual(
273 expected,
274 jax_expected,
275 msg=f"Trying wrap {wrap} theta {theta}",
276 )
277
Austin Schuh27694fa2024-07-20 16:29:49 -0700278 self.assertAlmostEqual(
279 expected,
280 computed_angle,
justinT2107d41ed2024-07-31 21:12:31 -0700281 msg=f"Trying wrap {wrap} theta {theta}",
282 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700283
Austin Schuhb8b34be2024-07-14 16:06:19 -0700284 def test_wheel_torque(self):
285 """Tests that the per module self aligning forces have the right signs."""
Austin Schuh27694fa2024-07-20 16:29:49 -0700286 # Point all the modules in a little bit.
justinT2107d41ed2024-07-31 21:12:31 -0700287 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700288 velocity=numpy.array([[1.0], [0.0]]),
289 module_angles=[-0.001, -0.001, 0.001, 0.001],
290 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700291 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700292
293 self.assertGreater(xdot_equal[2, 0], 0.0)
294 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
295 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
296
297 self.assertGreater(xdot_equal[6, 0], 0.0)
298 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
299 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
300
301 self.assertLess(xdot_equal[10, 0], 0.0)
302 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
303 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
304
305 self.assertLess(xdot_equal[14, 0], 0.0)
306 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
307 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
308
309 # Shouldn't be spinning.
310 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
311
Austin Schuh27694fa2024-07-20 16:29:49 -0700312 # Now, make the bot want to go left by going to the other side.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700313 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700314 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700315 velocity=numpy.array([[1.0], [0.0]]),
316 module_angles=[0.01, 0.01, 0.01, 0.01],
317 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700318 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700319
Austin Schuh27694fa2024-07-20 16:29:49 -0700320 self.assertLess(xdot_left[2, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700321 self.assertLess(xdot_left[3, 0], 0.0)
322 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
323
Austin Schuh27694fa2024-07-20 16:29:49 -0700324 self.assertLess(xdot_left[6, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700325 self.assertLess(xdot_left[7, 0], 0.0)
326 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
327
Austin Schuh27694fa2024-07-20 16:29:49 -0700328 self.assertLess(xdot_left[10, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700329 self.assertLess(xdot_left[11, 0], 0.0)
330 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
331
Austin Schuh27694fa2024-07-20 16:29:49 -0700332 self.assertLess(xdot_left[14, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700333 self.assertLess(xdot_left[15, 0], 0.0)
334 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
335
336 self.assertGreater(xdot_left[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700337 self.assertGreater(xdot_left[20, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700338 # Shouldn't be spinning.
339 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
340
341 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700342 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700343 velocity=numpy.array([[1.0], [0.0]]),
344 module_angles=[-0.01, -0.01, -0.01, -0.01],
345 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700346 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700347
Austin Schuh27694fa2024-07-20 16:29:49 -0700348 self.assertGreater(xdot_right[2, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700349 self.assertLess(xdot_right[3, 0], 0.0)
350 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
351
Austin Schuh27694fa2024-07-20 16:29:49 -0700352 self.assertGreater(xdot_right[6, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700353 self.assertLess(xdot_right[7, 0], 0.0)
354 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
355
Austin Schuh27694fa2024-07-20 16:29:49 -0700356 self.assertGreater(xdot_right[10, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700357 self.assertLess(xdot_right[11, 0], 0.0)
358 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
359
Austin Schuh27694fa2024-07-20 16:29:49 -0700360 self.assertGreater(xdot_right[14, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700361 self.assertLess(xdot_right[15, 0], 0.0)
362 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
363
364 self.assertGreater(xdot_right[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700365 self.assertLess(xdot_right[20, 0], -0.05)
366 # Shouldn't be spinning.
367 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
368
369 def test_wheel_torque_backwards_nocaster(self):
370 """Tests that the per module self aligning forces have the right signs when going backwards."""
371 self.wrap(nocaster_dynamics)
372 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700373 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700374 velocity=numpy.array([[1.0], [0.0]]),
375 module_angles=[
376 numpy.pi - 0.001,
377 numpy.pi - 0.001,
378 numpy.pi + 0.001,
379 numpy.pi + 0.001,
380 ],
381 drive_wheel_velocity=-1,
382 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700383 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700384
385 self.assertGreater(xdot_equal[2, 0], 0.0, msg="Steering backwards")
386 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
387 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
388
389 self.assertGreater(xdot_equal[6, 0], 0.0, msg="Steering backwards")
390 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
391 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
392
393 self.assertLess(xdot_equal[10, 0], 0.0, msg="Steering backwards")
394 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
395 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
396
397 self.assertLess(xdot_equal[14, 0], 0.0, msg="Steering backwards")
398 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
399 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
400
401 # Shouldn't be spinning.
402 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
403
404 # Now, make the bot want to go left by going to the other side.
405 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700406 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700407 velocity=numpy.array([[1.0], [0.0]]),
408 module_angles=[numpy.pi + 0.01] * 4,
409 drive_wheel_velocity=-1,
410 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700411 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700412
413 self.assertLess(xdot_left[2, 0], -0.05)
414 self.assertGreater(xdot_left[3, 0], 0.0)
415 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
416
417 self.assertLess(xdot_left[6, 0], -0.05)
418 self.assertGreater(xdot_left[7, 0], 0.0)
419 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
420
421 self.assertLess(xdot_left[10, 0], -0.05)
422 self.assertGreater(xdot_left[11, 0], 0.0)
423 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
424
425 self.assertLess(xdot_left[14, 0], -0.05)
426 self.assertGreater(xdot_left[15, 0], 0.0)
427 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
428
429 self.assertGreater(xdot_left[19, 0], 0.0001)
430 self.assertGreater(xdot_left[20, 0], 0.05)
431 # Shouldn't be spinning.
432 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
433
434 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700435 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700436 velocity=numpy.array([[1.0], [0.0]]),
437 drive_wheel_velocity=-1,
438 module_angles=[-0.01 + numpy.pi] * 4,
439 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700440 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700441
442 self.assertGreater(xdot_right[2, 0], 0.05)
443 self.assertGreater(xdot_right[3, 0], 0.0)
444 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
445
446 self.assertGreater(xdot_right[6, 0], 0.05)
447 self.assertGreater(xdot_right[7, 0], 0.0)
448 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
449
450 self.assertGreater(xdot_right[10, 0], 0.05)
451 self.assertGreater(xdot_right[11, 0], 0.0)
452 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
453
454 self.assertGreater(xdot_right[14, 0], 0.05)
455 self.assertGreater(xdot_right[15, 0], 0.0)
456 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
457
458 self.assertGreater(xdot_right[19, 0], 0.0001)
459 self.assertLess(xdot_right[20, 0], -0.05)
460 # Shouldn't be spinning.
461 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
462
463 def test_wheel_torque_backwards_caster(self):
464 """Tests that the per module self aligning forces have the right signs when going backwards with a lot of caster."""
465 self.wrap(bigcaster_dynamics)
466 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700467 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700468 velocity=numpy.array([[1.0], [0.0]]),
469 module_angles=[
470 numpy.pi - 0.001,
471 numpy.pi - 0.001,
472 numpy.pi + 0.001,
473 numpy.pi + 0.001,
474 ],
475 drive_wheel_velocity=-1,
476 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700477 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700478
479 self.assertLess(xdot_equal[2, 0], 0.0, msg="Steering backwards")
480 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
481 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
482
483 self.assertLess(xdot_equal[6, 0], 0.0, msg="Steering backwards")
484 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
485 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
486
487 self.assertGreater(xdot_equal[10, 0], 0.0, msg="Steering backwards")
488 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
489 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
490
491 self.assertGreater(xdot_equal[14, 0], 0.0, msg="Steering backwards")
492 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
493 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
494
495 # Shouldn't be spinning.
496 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
497
498 # Now, make the bot want to go left by going to the other side.
499 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700500 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700501 velocity=numpy.array([[1.0], [0.0]]),
502 module_angles=[numpy.pi + 0.01] * 4,
503 drive_wheel_velocity=-1,
504 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700505 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700506
507 self.assertGreater(xdot_left[2, 0], -0.05)
508 self.assertGreater(xdot_left[3, 0], 0.0)
509 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
510
511 self.assertGreater(xdot_left[6, 0], -0.05)
512 self.assertGreater(xdot_left[7, 0], 0.0)
513 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
514
515 self.assertGreater(xdot_left[10, 0], -0.05)
516 self.assertGreater(xdot_left[11, 0], 0.0)
517 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
518
519 self.assertGreater(xdot_left[14, 0], -0.05)
520 self.assertGreater(xdot_left[15, 0], 0.0)
521 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
522
523 self.assertGreater(xdot_left[19, 0], 0.0001)
524 self.assertGreater(xdot_left[20, 0], 0.05)
525 # Shouldn't be spinning.
526 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
527
528 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700529 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700530 velocity=numpy.array([[1.0], [0.0]]),
531 drive_wheel_velocity=-1,
532 module_angles=[-0.01 + numpy.pi] * 4,
533 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700534 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700535
536 self.assertLess(xdot_right[2, 0], 0.05)
537 self.assertGreater(xdot_right[3, 0], 0.0)
538 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
539
540 self.assertLess(xdot_right[6, 0], 0.05)
541 self.assertGreater(xdot_right[7, 0], 0.0)
542 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
543
544 self.assertLess(xdot_right[10, 0], 0.05)
545 self.assertGreater(xdot_right[11, 0], 0.0)
546 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
547
548 self.assertLess(xdot_right[14, 0], 0.05)
549 self.assertGreater(xdot_right[15, 0], 0.0)
550 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
551
552 self.assertGreater(xdot_right[19, 0], 0.0001)
553 self.assertLess(xdot_right[20, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700554 # Shouldn't be spinning.
555 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
556
Austin Schuh2a1abec2024-07-10 20:31:16 -0700557 def test_wheel_forces(self):
558 """Tests that the per module forces have the right signs."""
559 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700560 wheel_force = self.wheel_force[i]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700561
justinT2107d41ed2024-07-31 21:12:31 -0700562 X = utils.state_vector()
Austin Schuhb8b34be2024-07-14 16:06:19 -0700563 robot_equal = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700564 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700565 self.assertEqual(robot_equal[0, 0], 0.0)
566 self.assertEqual(robot_equal[1, 0], 0.0)
567 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
568 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700569
570 # Robot is moving faster than the wheels, it should decelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700571 X = utils.state_vector(dx=0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700572 robot_faster = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700573 xdot_faster = self.swerve_full_dynamics(X,
574 self.I,
575 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700576 self.assertLess(robot_faster[0, 0], -0.1)
577 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700578 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700579
580 # Robot is now going slower than the wheels. It should accelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700581 X = utils.state_vector(dx=-0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700582 robot_slower = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700583 xdot_slower = self.swerve_full_dynamics(X,
584 self.I,
585 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700586 self.assertGreater(robot_slower[0, 0], 0.1)
587 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700588 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700589
590 # Positive lateral velocity -> negative force.
justinT2107d41ed2024-07-31 21:12:31 -0700591 robot_left = wheel_force(utils.state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700592 self.assertEqual(robot_left[0, 0], 0.0)
593 self.assertLess(robot_left[1, 0], -0.1)
594
595 # Negative lateral velocity -> positive force.
justinT2107d41ed2024-07-31 21:12:31 -0700596 robot_right = wheel_force(utils.state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700597 self.assertEqual(robot_right[0, 0], 0.0)
598 self.assertGreater(robot_right[1, 0], 0.1)
599
Austin Schuh3233d7f2024-07-27 17:51:52 -0700600 def test_steer_accel(self):
601 """Tests that applying a steer torque accelerates the steer reasonably."""
602
603 for velocity in [
604 numpy.array([[0.0], [0.0]]),
605 numpy.array([[1.0], [0.0]]),
606 numpy.array([[2.0], [0.0]])
607 ]:
608 module_angles = [0.0] * 4
609
610 X = utils.state_vector(
611 velocity=velocity,
612 omega=0.0,
613 module_angles=module_angles,
614 )
615 steer_I = numpy.array([[1.0], [0.0]] * 4)
616 xdot = self.swerve_full_dynamics(X, steer_I)
617
618 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS0, 0],
619 1.4,
620 places=0)
621 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS1, 0],
622 1.4,
623 places=0)
624 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS2, 0],
625 1.4,
626 places=0)
627 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS3, 0],
628 1.4,
629 places=0)
630
631 def test_drive_accel(self):
632 """Tests that applying a drive torque accelerates the drive reasonably."""
633
634 for velocity in [
635 numpy.array([[0.01], [0.0]]),
636 numpy.array([[1.0], [0.0]]),
637 numpy.array([[2.0], [0.0]])
638 ]:
639 module_angles = [0.0] * 4
640
641 X = utils.state_vector(
642 velocity=velocity,
643 omega=0.0,
644 module_angles=module_angles,
645 )
646 steer_I = numpy.array([[0.0], [100.0]] * 4)
647 # 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.
648 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
649
650 self.assertGreater(xdot[dynamics.STATE_OMEGAD0, 0], 100.0)
651 self.assertGreater(xdot[dynamics.STATE_OMEGAD1, 0], 100.0)
652 self.assertGreater(xdot[dynamics.STATE_OMEGAD2, 0], 100.0)
653 self.assertGreater(xdot[dynamics.STATE_OMEGAD3, 0], 100.0)
654
655 X = utils.state_vector(
656 velocity=velocity,
657 omega=0.0,
658 module_angles=module_angles,
659 )
660 steer_I = numpy.array([[0.0], [-100.0]] * 4)
661 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
662
663 self.assertLess(xdot[dynamics.STATE_OMEGAD0, 0], -100.0)
664 self.assertLess(xdot[dynamics.STATE_OMEGAD1, 0], -100.0)
665 self.assertLess(xdot[dynamics.STATE_OMEGAD2, 0], -100.0)
666 self.assertLess(xdot[dynamics.STATE_OMEGAD3, 0], -100.0)
667
Austin Schuh98fbbe82024-08-18 23:07:26 -0700668 def test_steer_coupling(self):
669 """Tests that the steer coupling factor cancels out steer coupling torque."""
670 steer_I = numpy.array(
671 [[dynamics.STEER_CURRENT_COUPLING_FACTOR * 10.0], [10.0]] * 4)
672
673 X = utils.state_vector(
674 velocity=numpy.array([[0.0], [0.0]]),
675 omega=0.0,
676 )
677 X_velocity = self.to_velocity_state(X)
678 Xdot = self.velocity_swerve_physics(X_velocity, steer_I)
679
680 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS0, 0], 0.0)
681 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS1, 0], 0.0)
682 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS2, 0], 0.0)
683 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS3, 0], 0.0)
684
justinT21d18f79f2024-09-22 19:43:05 -0700685 def test_constant_torque(self):
686 """Tests that the robot exerts the same amount of torque no matter the orientation"""
687 steer_I = numpy.reshape(numpy.array([(i % 2) * 20 for i in range(8)]),
688 (8, 1))
689
690 X = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
691 omega=0.0,
692 module_angles=[
693 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
694 -numpy.pi / 4.0, numpy.pi / 4.0
695 ],
696 drive_wheel_velocity=1.0)
697 Xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
698
699 X_rot = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
700 omega=0.0,
701 theta=numpy.pi,
702 module_angles=[
703 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
704 -numpy.pi / 4.0, numpy.pi / 4.0
705 ],
706 drive_wheel_velocity=1.0)
707 Xdot_rot = self.swerve_full_dynamics(X_rot, steer_I, skip_compare=True)
708
709 self.assertGreater(Xdot[dynamics.STATE_OMEGA, 0], 0.0)
710 self.assertAlmostEquals(Xdot[dynamics.STATE_OMEGA, 0],
711 Xdot_rot[dynamics.STATE_OMEGA, 0])
712
James Kuszmaulef14ab42024-09-14 15:05:24 -0700713 def test_cpp_consistency(self):
714 """Tests that the C++ physics are consistent with the Python physics."""
715 # TODO(james): Currently the physics only match at X = 0 and U = 0.
716 # Fix this.
717 # Maybe due to different atan2 implementations?
718 # TODO(james): Fold this into the general comparisons for JAX versus
719 # casadi once the physics actually match.
720 for current in [0]:
721 print(f"Current: {current}")
722 steer_I = numpy.zeros((8, 1)) + current
723 for state_values in [0.0]:
724 print(f"States all set to: {state_values}")
725 X = numpy.zeros((dynamics.NUM_STATES, 1)) + state_values
726 Xdot_py = self.swerve_full_dynamics(X,
727 steer_I,
728 skip_compare=True)
729 Xdot_cpp = numpy.array(
730 cpp_dynamics(X.flatten().tolist(),
731 steer_I.flatten().tolist())).reshape((25, 1))
732 for index in range(dynamics.NUM_STATES):
733 self.assertAlmostEqual(Xdot_py[index, 0], Xdot_cpp[index,
734 0])
735
Austin Schuh2a1abec2024-07-10 20:31:16 -0700736
justinT2107d41ed2024-07-31 21:12:31 -0700737if __name__ == "__main__":
Austin Schuh2a1abec2024-07-10 20:31:16 -0700738 unittest.main()