blob: a45bf4670bfe2ec67c79134f40ebb5e9e92b145d [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
Austin Schuhb8b34be2024-07-14 16:06:19 -070022
23
Austin Schuh6927bc32024-07-14 17:24:56 -070024class TestSwervePhysics(unittest.TestCase):
Austin Schuh2a1abec2024-07-10 20:31:16 -070025 I = numpy.zeros((8, 1))
Austin Schuh76534f32024-09-02 13:52:45 -070026 coefficients = jax_dynamics.Coefficients()
Austin Schuh2a1abec2024-07-10 20:31:16 -070027
Austin Schuh6ea789e2024-07-27 13:45:53 -070028 def to_velocity_state(self, X):
29 return dynamics.to_velocity_state(X)
30
31 def swerve_full_dynamics(self, X, U, skip_compare=False):
32 X_velocity = self.to_velocity_state(X)
33 Xdot = self.position_swerve_full_dynamics(X, U)
Austin Schuh76534f32024-09-02 13:52:45 -070034
Austin Schuh6ea789e2024-07-27 13:45:53 -070035 if not skip_compare:
Austin Schuh76534f32024-09-02 13:52:45 -070036 Xdot_velocity = self.to_velocity_state(Xdot)
Austin Schuh6ea789e2024-07-27 13:45:53 -070037 velocity_physics = self.velocity_swerve_physics(X_velocity, U)
Austin Schuh76534f32024-09-02 13:52:45 -070038
Austin Schuh6ea789e2024-07-27 13:45:53 -070039 self.assertLess(
Austin Schuh76534f32024-09-02 13:52:45 -070040 numpy.linalg.norm(Xdot_velocity - velocity_physics),
Austin Schuh6ea789e2024-07-27 13:45:53 -070041 2e-2,
42 msg=
Austin Schuh76534f32024-09-02 13:52:45 -070043 f'Norm failed, full physics -> {X_velocity.T}, velocity physics -> {velocity_physics}, difference -> {velocity_physics - X_velocity}',
Austin Schuh6ea789e2024-07-27 13:45:53 -070044 )
45
Austin Schuh76534f32024-09-02 13:52:45 -070046 self.validate_dynamics_equality(X, U)
47
Austin Schuh6ea789e2024-07-27 13:45:53 -070048 return Xdot
49
Austin Schuh76534f32024-09-02 13:52:45 -070050 def validate_dynamics_equality(self, X, U):
51 """Tests that both the JAX code and casadi code produce identical answers.
52
53 Note:
54 If the symengine code has been updated, you likely need to update the JAX
55 by hand. We had trouble code generating it with good performance.
56 """
57 X_velocity = self.to_velocity_state(X)
58
59 Xdot = self.position_swerve_full_dynamics(X, U)
60 Xdot_jax = jax_dynamics.full_dynamics(self.coefficients, X[:, 0], U[:,
61 0])
justinT21a4fa3e22024-10-12 17:09:50 -070062 self.assertEqual(Xdot.shape[0], Xdot_jax.shape[0])
Austin Schuh76534f32024-09-02 13:52:45 -070063
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
justinT21a4fa3e22024-10-12 17:09:50 -070075 self.assertEqual(velocity_physics.shape[0],
76 velocity_physics_jax.shape[0])
77
Austin Schuh76534f32024-09-02 13:52:45 -070078 self.assertLess(
79 numpy.linalg.norm(velocity_physics[:, 0] - velocity_physics_jax),
80 2e-8,
81 msg=
82 f'Xdot: {velocity_physics[:, 0]}, Xdot_jax: {velocity_physics_jax}, diff: {(velocity_physics[:, 0] - velocity_physics_jax)}',
83 )
84
85 def wrap_and_validate(self, function, i):
86 """Wraps a function, and validates JAX and casadi agree.
87
88 We want to do it every time we check any intermediate, since the tests
89 are designed to test all the corner cases, but they don't all do it
90 through the main dynamics function above.
91 """
92 wrapped_fn = utils.wrap_module(function, i)
93
94 def do(X, U):
95 self.validate_dynamics_equality(X, U)
96 return wrapped_fn(X, U)
97
98 return do
99
Austin Schuh27694fa2024-07-20 16:29:49 -0700100 def wrap(self, python_module):
Austin Schuh76534f32024-09-02 13:52:45 -0700101 # Only update on change to avoid re-jiting things.
102 if self.coefficients.caster != python_module.CASTER:
103 self.coefficients = self.coefficients._replace(
104 caster=python_module.CASTER)
105
Austin Schuh6ea789e2024-07-27 13:45:53 -0700106 self.position_swerve_full_dynamics = utils.wrap(
107 python_module.swerve_full_dynamics)
108
109 evaluated_fn = python_module.velocity_swerve_physics(
110 casadi.SX.sym("X", dynamics.NUM_VELOCITY_STATES, 1),
111 casadi.SX.sym("U", 8, 1))
112 self.velocity_swerve_physics = lambda X, U: numpy.array(
113 evaluated_fn(X, U))
114
Austin Schuhb8b34be2024-07-14 16:06:19 -0700115 self.contact_patch_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700116 self.wrap_and_validate(python_module.contact_patch_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700117 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700118 ]
119 self.wheel_ground_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700120 self.wrap_and_validate(python_module.wheel_ground_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700121 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700122 ]
123 self.wheel_slip_velocity = [
Austin Schuh76534f32024-09-02 13:52:45 -0700124 self.wrap_and_validate(python_module.wheel_slip_velocity, i)
Austin Schuh27694fa2024-07-20 16:29:49 -0700125 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700126 ]
justinT2107d41ed2024-07-31 21:12:31 -0700127 self.wheel_force = [
Austin Schuh76534f32024-09-02 13:52:45 -0700128 self.wrap_and_validate(python_module.wheel_force, i)
justinT2107d41ed2024-07-31 21:12:31 -0700129 for i in range(4)
130 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700131 self.module_angular_accel = [
132 self.wrap_and_validate(python_module.module_angular_accel, i)
133 for i in range(4)
134 ]
135 self.F = [self.wrap_and_validate(python_module.F, i) for i in range(4)]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700136 self.mounting_location = [
Austin Schuh76534f32024-09-02 13:52:45 -0700137 self.wrap_and_validate(python_module.mounting_location, i)
justinT2107d41ed2024-07-31 21:12:31 -0700138 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700139 ]
140
141 self.slip_angle = [
Austin Schuh76534f32024-09-02 13:52:45 -0700142 self.wrap_and_validate(python_module.slip_angle, i)
143 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700144 ]
145 self.slip_ratio = [
Austin Schuh76534f32024-09-02 13:52:45 -0700146 self.wrap_and_validate(python_module.slip_ratio, i)
147 for i in range(4)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700148 ]
Austin Schuh76534f32024-09-02 13:52:45 -0700149 self.Ms = [
150 self.wrap_and_validate(python_module.Ms, i) for i in range(4)
151 ]
Austin Schuh27694fa2024-07-20 16:29:49 -0700152
153 def setUp(self):
154 self.wrap(dynamics)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700155
156 def test_contact_patch_velocity(self):
157 """Tests that the contact patch velocity makes sense."""
158 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700159 contact_patch_velocity = self.contact_patch_velocity[i]
160 wheel_ground_velocity = self.wheel_ground_velocity[i]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700161
162 # No angular velocity should result in just linear motion.
163 for velocity in [
164 numpy.array([[1.5], [0.0]]),
165 numpy.array([[0.0], [1.0]]),
166 numpy.array([[-1.5], [0.0]]),
167 numpy.array([[0.0], [-1.0]]),
168 numpy.array([[2.0], [-1.7]]),
169 ]:
170 for theta in [0.0, 1.0, -1.0, 100.0]:
Austin Schuhb8b34be2024-07-14 16:06:19 -0700171 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700172 utils.state_vector(velocity=velocity, theta=theta),
173 self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700174
175 assert_array_equal(patch_velocity, velocity)
176
177 # Now, test that spinning the robot results in module velocities.
178 # We are assuming that each module is on a square robot.
179 module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0
180 for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]:
181 for omega in [0.65, -0.1]:
182 # Point each module to the center to make the math easier.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700183 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700184 utils.state_vector(
185 velocity=numpy.array([[0.0], [0.0]]),
186 theta=theta,
187 omega=omega,
188 module_angle=module_center_of_mass_angle,
189 ),
190 self.I,
191 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700192
193 assert_array_almost_equal(
194 patch_velocity,
Austin Schuh6927bc32024-07-14 17:24:56 -0700195 (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) -
justinT2107d41ed2024-07-31 21:12:31 -0700196 dynamics.CASTER) * omega * numpy.array([
197 [-numpy.sin(theta + module_center_of_mass_angle)],
198 [numpy.cos(theta + module_center_of_mass_angle)],
199 ]),
200 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700201
202 # Point the wheel along +x, rotate it by theta, then spin it.
203 # Confirm the velocities come out right.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700204 patch_velocity = contact_patch_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700205 utils.state_vector(
Austin Schuhb8b34be2024-07-14 16:06:19 -0700206 velocity=numpy.array([[0.0], [0.0]]),
207 theta=-module_center_of_mass_angle,
208 module_omega=omega,
justinT2107d41ed2024-07-31 21:12:31 -0700209 module_angle=(theta + module_center_of_mass_angle),
210 ),
211 self.I,
212 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700213
214 assert_array_almost_equal(
justinT2107d41ed2024-07-31 21:12:31 -0700215 patch_velocity,
216 -dynamics.CASTER * omega *
217 numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]]),
218 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700219
220 # Now, test that the rotation back to wheel coordinates works.
221 # The easiest way to do this is to point the wheel in a direction,
222 # move in that direction, and confirm that there is no lateral velocity.
223 for robot_angle in [0.0, 1.0, -5.0]:
224 for module_angle in [0.0, 1.0, -5.0]:
225 wheel_patch_velocity = numpy.array(
226 wheel_ground_velocity(
justinT2107d41ed2024-07-31 21:12:31 -0700227 utils.state_vector(
228 velocity=numpy.array([
229 [numpy.cos(robot_angle + module_angle)],
230 [numpy.sin(robot_angle + module_angle)],
231 ]),
232 theta=robot_angle,
233 module_angle=module_angle,
234 ),
235 self.I,
236 ))
Austin Schuh2a1abec2024-07-10 20:31:16 -0700237
238 assert_array_almost_equal(wheel_patch_velocity,
239 numpy.array([[1], [0]]))
240
241 def test_slip_angle(self):
242 """Tests that the slip_angle calculation works."""
243 velocity = numpy.array([[1.5], [0.0]])
244
245 for i in range(4):
Austin Schuh27694fa2024-07-20 16:29:49 -0700246 for wrap in range(-1, 2):
Austin Schuh2a1abec2024-07-10 20:31:16 -0700247 for theta in [0.0, 0.6, -0.4]:
248 module_angle = numpy.pi * wrap + theta
249
Austin Schuhbd75c482024-08-18 00:03:51 -0700250 # We have redefined the angle to be the softened sin of the angle.
Austin Schuh27694fa2024-07-20 16:29:49 -0700251 # That way, when the module flips directions, the slip angle also flips
252 # directions to keep it stable.
justinT2107d41ed2024-07-31 21:12:31 -0700253 computed_angle = self.slip_angle[i](
254 utils.state_vector(velocity=velocity,
Austin Schuhbd75c482024-08-18 00:03:51 -0700255 module_angle=module_angle),
justinT2107d41ed2024-07-31 21:12:31 -0700256 self.I,
257 )[0, 0]
Austin Schuh2a1abec2024-07-10 20:31:16 -0700258
Austin Schuhbd75c482024-08-18 00:03:51 -0700259 # Compute out the expected value directly to confirm we got it right.
260 loggain = 20.0
261 vy = 1.5 * numpy.sin(-module_angle)
262 vx = 1.5 * numpy.cos(-module_angle)
263 expected = numpy.sin(-numpy.arctan2(
264 vy,
265 scipy.special.logsumexp([1.0, abs(vx) * loggain]) /
266 loggain))
Austin Schuh27694fa2024-07-20 16:29:49 -0700267
Austin Schuh76534f32024-09-02 13:52:45 -0700268 jax_expected = jax.numpy.sin(
269 -jax_dynamics.soft_atan2(vy, vx))
270
271 self.assertAlmostEqual(
272 expected,
273 jax_expected,
274 msg=f"Trying wrap {wrap} theta {theta}",
275 )
276
Austin Schuh27694fa2024-07-20 16:29:49 -0700277 self.assertAlmostEqual(
278 expected,
279 computed_angle,
justinT2107d41ed2024-07-31 21:12:31 -0700280 msg=f"Trying wrap {wrap} theta {theta}",
281 )
Austin Schuh2a1abec2024-07-10 20:31:16 -0700282
Austin Schuhb8b34be2024-07-14 16:06:19 -0700283 def test_wheel_torque(self):
284 """Tests that the per module self aligning forces have the right signs."""
Austin Schuh27694fa2024-07-20 16:29:49 -0700285 # Point all the modules in a little bit.
justinT2107d41ed2024-07-31 21:12:31 -0700286 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700287 velocity=numpy.array([[1.0], [0.0]]),
288 module_angles=[-0.001, -0.001, 0.001, 0.001],
289 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700290 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700291
292 self.assertGreater(xdot_equal[2, 0], 0.0)
293 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
294 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
295
296 self.assertGreater(xdot_equal[6, 0], 0.0)
297 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
298 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
299
300 self.assertLess(xdot_equal[10, 0], 0.0)
301 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
302 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
303
304 self.assertLess(xdot_equal[14, 0], 0.0)
305 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
306 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
307
308 # Shouldn't be spinning.
309 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
310
Austin Schuh27694fa2024-07-20 16:29:49 -0700311 # Now, make the bot want to go left by going to the other side.
Austin Schuhb8b34be2024-07-14 16:06:19 -0700312 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700313 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700314 velocity=numpy.array([[1.0], [0.0]]),
315 module_angles=[0.01, 0.01, 0.01, 0.01],
316 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700317 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700318
Austin Schuh27694fa2024-07-20 16:29:49 -0700319 self.assertLess(xdot_left[2, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700320 self.assertLess(xdot_left[3, 0], 0.0)
321 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
322
Austin Schuh27694fa2024-07-20 16:29:49 -0700323 self.assertLess(xdot_left[6, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700324 self.assertLess(xdot_left[7, 0], 0.0)
325 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
326
Austin Schuh27694fa2024-07-20 16:29:49 -0700327 self.assertLess(xdot_left[10, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700328 self.assertLess(xdot_left[11, 0], 0.0)
329 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
330
Austin Schuh27694fa2024-07-20 16:29:49 -0700331 self.assertLess(xdot_left[14, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700332 self.assertLess(xdot_left[15, 0], 0.0)
333 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
334
335 self.assertGreater(xdot_left[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700336 self.assertGreater(xdot_left[20, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700337 # Shouldn't be spinning.
338 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
339
340 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700341 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700342 velocity=numpy.array([[1.0], [0.0]]),
343 module_angles=[-0.01, -0.01, -0.01, -0.01],
344 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700345 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700346
Austin Schuh27694fa2024-07-20 16:29:49 -0700347 self.assertGreater(xdot_right[2, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700348 self.assertLess(xdot_right[3, 0], 0.0)
349 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
350
Austin Schuh27694fa2024-07-20 16:29:49 -0700351 self.assertGreater(xdot_right[6, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700352 self.assertLess(xdot_right[7, 0], 0.0)
353 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
354
Austin Schuh27694fa2024-07-20 16:29:49 -0700355 self.assertGreater(xdot_right[10, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700356 self.assertLess(xdot_right[11, 0], 0.0)
357 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
358
Austin Schuh27694fa2024-07-20 16:29:49 -0700359 self.assertGreater(xdot_right[14, 0], 0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700360 self.assertLess(xdot_right[15, 0], 0.0)
361 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
362
363 self.assertGreater(xdot_right[19, 0], 0.0001)
Austin Schuh27694fa2024-07-20 16:29:49 -0700364 self.assertLess(xdot_right[20, 0], -0.05)
365 # Shouldn't be spinning.
366 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
367
368 def test_wheel_torque_backwards_nocaster(self):
369 """Tests that the per module self aligning forces have the right signs when going backwards."""
370 self.wrap(nocaster_dynamics)
371 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700372 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700373 velocity=numpy.array([[1.0], [0.0]]),
374 module_angles=[
375 numpy.pi - 0.001,
376 numpy.pi - 0.001,
377 numpy.pi + 0.001,
378 numpy.pi + 0.001,
379 ],
380 drive_wheel_velocity=-1,
381 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700382 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700383
384 self.assertGreater(xdot_equal[2, 0], 0.0, msg="Steering backwards")
385 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
386 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
387
388 self.assertGreater(xdot_equal[6, 0], 0.0, msg="Steering backwards")
389 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
390 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
391
392 self.assertLess(xdot_equal[10, 0], 0.0, msg="Steering backwards")
393 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
394 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
395
396 self.assertLess(xdot_equal[14, 0], 0.0, msg="Steering backwards")
397 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
398 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
399
400 # Shouldn't be spinning.
401 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
402
403 # Now, make the bot want to go left by going to the other side.
404 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700405 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700406 velocity=numpy.array([[1.0], [0.0]]),
407 module_angles=[numpy.pi + 0.01] * 4,
408 drive_wheel_velocity=-1,
409 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700410 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700411
412 self.assertLess(xdot_left[2, 0], -0.05)
413 self.assertGreater(xdot_left[3, 0], 0.0)
414 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
415
416 self.assertLess(xdot_left[6, 0], -0.05)
417 self.assertGreater(xdot_left[7, 0], 0.0)
418 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
419
420 self.assertLess(xdot_left[10, 0], -0.05)
421 self.assertGreater(xdot_left[11, 0], 0.0)
422 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
423
424 self.assertLess(xdot_left[14, 0], -0.05)
425 self.assertGreater(xdot_left[15, 0], 0.0)
426 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
427
428 self.assertGreater(xdot_left[19, 0], 0.0001)
429 self.assertGreater(xdot_left[20, 0], 0.05)
430 # Shouldn't be spinning.
431 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
432
433 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700434 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700435 velocity=numpy.array([[1.0], [0.0]]),
436 drive_wheel_velocity=-1,
437 module_angles=[-0.01 + numpy.pi] * 4,
438 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700439 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700440
441 self.assertGreater(xdot_right[2, 0], 0.05)
442 self.assertGreater(xdot_right[3, 0], 0.0)
443 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
444
445 self.assertGreater(xdot_right[6, 0], 0.05)
446 self.assertGreater(xdot_right[7, 0], 0.0)
447 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
448
449 self.assertGreater(xdot_right[10, 0], 0.05)
450 self.assertGreater(xdot_right[11, 0], 0.0)
451 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
452
453 self.assertGreater(xdot_right[14, 0], 0.05)
454 self.assertGreater(xdot_right[15, 0], 0.0)
455 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
456
457 self.assertGreater(xdot_right[19, 0], 0.0001)
458 self.assertLess(xdot_right[20, 0], -0.05)
459 # Shouldn't be spinning.
460 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
461
462 def test_wheel_torque_backwards_caster(self):
463 """Tests that the per module self aligning forces have the right signs when going backwards with a lot of caster."""
464 self.wrap(bigcaster_dynamics)
465 # Point all the modules in a little bit, going backwards.
justinT2107d41ed2024-07-31 21:12:31 -0700466 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700467 velocity=numpy.array([[1.0], [0.0]]),
468 module_angles=[
469 numpy.pi - 0.001,
470 numpy.pi - 0.001,
471 numpy.pi + 0.001,
472 numpy.pi + 0.001,
473 ],
474 drive_wheel_velocity=-1,
475 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700476 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700477
478 self.assertLess(xdot_equal[2, 0], 0.0, msg="Steering backwards")
479 self.assertAlmostEqual(xdot_equal[3, 0], 0.0, places=1)
480 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
481
482 self.assertLess(xdot_equal[6, 0], 0.0, msg="Steering backwards")
483 self.assertAlmostEqual(xdot_equal[7, 0], 0.0, places=1)
484 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
485
486 self.assertGreater(xdot_equal[10, 0], 0.0, msg="Steering backwards")
487 self.assertAlmostEqual(xdot_equal[11, 0], 0.0, places=1)
488 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
489
490 self.assertGreater(xdot_equal[14, 0], 0.0, msg="Steering backwards")
491 self.assertAlmostEqual(xdot_equal[15, 0], 0.0, places=1)
492 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
493
494 # Shouldn't be spinning.
495 self.assertAlmostEqual(xdot_equal[21, 0], 0.0, places=2)
496
497 # Now, make the bot want to go left by going to the other side.
498 # The wheels will be going too fast based on our calcs, so they should be decelerating.
justinT2107d41ed2024-07-31 21:12:31 -0700499 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700500 velocity=numpy.array([[1.0], [0.0]]),
501 module_angles=[numpy.pi + 0.01] * 4,
502 drive_wheel_velocity=-1,
503 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700504 xdot_left = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700505
506 self.assertGreater(xdot_left[2, 0], -0.05)
507 self.assertGreater(xdot_left[3, 0], 0.0)
508 self.assertGreater(self.Ms[0](X, self.I)[0, 0], 0.0)
509
510 self.assertGreater(xdot_left[6, 0], -0.05)
511 self.assertGreater(xdot_left[7, 0], 0.0)
512 self.assertGreater(self.Ms[1](X, self.I)[0, 0], 0.0)
513
514 self.assertGreater(xdot_left[10, 0], -0.05)
515 self.assertGreater(xdot_left[11, 0], 0.0)
516 self.assertGreater(self.Ms[2](X, self.I)[0, 0], 0.0)
517
518 self.assertGreater(xdot_left[14, 0], -0.05)
519 self.assertGreater(xdot_left[15, 0], 0.0)
520 self.assertGreater(self.Ms[3](X, self.I)[0, 0], 0.0)
521
522 self.assertGreater(xdot_left[19, 0], 0.0001)
523 self.assertGreater(xdot_left[20, 0], 0.05)
524 # Shouldn't be spinning.
525 self.assertAlmostEqual(xdot_left[21, 0], 0.0)
526
527 # And now do it to the right too.
justinT2107d41ed2024-07-31 21:12:31 -0700528 X = utils.state_vector(
Austin Schuh27694fa2024-07-20 16:29:49 -0700529 velocity=numpy.array([[1.0], [0.0]]),
530 drive_wheel_velocity=-1,
531 module_angles=[-0.01 + numpy.pi] * 4,
532 )
Austin Schuh6ea789e2024-07-27 13:45:53 -0700533 xdot_right = self.swerve_full_dynamics(X, self.I)
Austin Schuh27694fa2024-07-20 16:29:49 -0700534
535 self.assertLess(xdot_right[2, 0], 0.05)
536 self.assertGreater(xdot_right[3, 0], 0.0)
537 self.assertLess(self.Ms[0](X, self.I)[0, 0], 0.0)
538
539 self.assertLess(xdot_right[6, 0], 0.05)
540 self.assertGreater(xdot_right[7, 0], 0.0)
541 self.assertLess(self.Ms[1](X, self.I)[0, 0], 0.0)
542
543 self.assertLess(xdot_right[10, 0], 0.05)
544 self.assertGreater(xdot_right[11, 0], 0.0)
545 self.assertLess(self.Ms[2](X, self.I)[0, 0], 0.0)
546
547 self.assertLess(xdot_right[14, 0], 0.05)
548 self.assertGreater(xdot_right[15, 0], 0.0)
549 self.assertLess(self.Ms[3](X, self.I)[0, 0], 0.0)
550
551 self.assertGreater(xdot_right[19, 0], 0.0001)
552 self.assertLess(xdot_right[20, 0], -0.05)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700553 # Shouldn't be spinning.
554 self.assertAlmostEqual(xdot_right[21, 0], 0.0)
555
Austin Schuh2a1abec2024-07-10 20:31:16 -0700556 def test_wheel_forces(self):
557 """Tests that the per module forces have the right signs."""
558 for i in range(4):
justinT2107d41ed2024-07-31 21:12:31 -0700559 wheel_force = self.wheel_force[i]
Austin Schuhb8b34be2024-07-14 16:06:19 -0700560
justinT2107d41ed2024-07-31 21:12:31 -0700561 X = utils.state_vector()
Austin Schuhb8b34be2024-07-14 16:06:19 -0700562 robot_equal = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700563 xdot_equal = self.swerve_full_dynamics(X, self.I)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700564 self.assertEqual(robot_equal[0, 0], 0.0)
565 self.assertEqual(robot_equal[1, 0], 0.0)
566 self.assertEqual(xdot_equal[2 + 4 * i], 0.0)
567 self.assertEqual(xdot_equal[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700568
569 # Robot is moving faster than the wheels, it should decelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700570 X = utils.state_vector(dx=0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700571 robot_faster = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700572 xdot_faster = self.swerve_full_dynamics(X,
573 self.I,
574 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700575 self.assertLess(robot_faster[0, 0], -0.1)
576 self.assertEqual(robot_faster[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700577 self.assertGreater(xdot_faster[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700578
579 # Robot is now going slower than the wheels. It should accelerate.
justinT2107d41ed2024-07-31 21:12:31 -0700580 X = utils.state_vector(dx=-0.01)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700581 robot_slower = wheel_force(X, self.I)
Austin Schuh6ea789e2024-07-27 13:45:53 -0700582 xdot_slower = self.swerve_full_dynamics(X,
583 self.I,
584 skip_compare=True)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700585 self.assertGreater(robot_slower[0, 0], 0.1)
586 self.assertEqual(robot_slower[1, 0], 0.0)
Austin Schuhb8b34be2024-07-14 16:06:19 -0700587 self.assertLess(xdot_slower[3 + 4 * i], 0.0)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700588
589 # Positive lateral velocity -> negative force.
justinT2107d41ed2024-07-31 21:12:31 -0700590 robot_left = wheel_force(utils.state_vector(dy=0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700591 self.assertEqual(robot_left[0, 0], 0.0)
592 self.assertLess(robot_left[1, 0], -0.1)
593
594 # Negative lateral velocity -> positive force.
justinT2107d41ed2024-07-31 21:12:31 -0700595 robot_right = wheel_force(utils.state_vector(dy=-0.01), self.I)
Austin Schuh2a1abec2024-07-10 20:31:16 -0700596 self.assertEqual(robot_right[0, 0], 0.0)
597 self.assertGreater(robot_right[1, 0], 0.1)
598
Austin Schuh3233d7f2024-07-27 17:51:52 -0700599 def test_steer_accel(self):
600 """Tests that applying a steer torque accelerates the steer reasonably."""
601
602 for velocity in [
603 numpy.array([[0.0], [0.0]]),
604 numpy.array([[1.0], [0.0]]),
605 numpy.array([[2.0], [0.0]])
606 ]:
607 module_angles = [0.0] * 4
608
609 X = utils.state_vector(
610 velocity=velocity,
611 omega=0.0,
612 module_angles=module_angles,
613 )
614 steer_I = numpy.array([[1.0], [0.0]] * 4)
615 xdot = self.swerve_full_dynamics(X, steer_I)
616
617 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS0, 0],
618 1.4,
619 places=0)
620 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS1, 0],
621 1.4,
622 places=0)
623 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS2, 0],
624 1.4,
625 places=0)
626 self.assertAlmostEqual(xdot[dynamics.STATE_OMEGAS3, 0],
627 1.4,
628 places=0)
629
630 def test_drive_accel(self):
631 """Tests that applying a drive torque accelerates the drive reasonably."""
632
633 for velocity in [
634 numpy.array([[0.01], [0.0]]),
635 numpy.array([[1.0], [0.0]]),
636 numpy.array([[2.0], [0.0]])
637 ]:
638 module_angles = [0.0] * 4
639
640 X = utils.state_vector(
641 velocity=velocity,
642 omega=0.0,
643 module_angles=module_angles,
644 )
645 steer_I = numpy.array([[0.0], [100.0]] * 4)
646 # 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.
647 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
648
649 self.assertGreater(xdot[dynamics.STATE_OMEGAD0, 0], 100.0)
650 self.assertGreater(xdot[dynamics.STATE_OMEGAD1, 0], 100.0)
651 self.assertGreater(xdot[dynamics.STATE_OMEGAD2, 0], 100.0)
652 self.assertGreater(xdot[dynamics.STATE_OMEGAD3, 0], 100.0)
653
654 X = utils.state_vector(
655 velocity=velocity,
656 omega=0.0,
657 module_angles=module_angles,
658 )
659 steer_I = numpy.array([[0.0], [-100.0]] * 4)
660 xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
661
662 self.assertLess(xdot[dynamics.STATE_OMEGAD0, 0], -100.0)
663 self.assertLess(xdot[dynamics.STATE_OMEGAD1, 0], -100.0)
664 self.assertLess(xdot[dynamics.STATE_OMEGAD2, 0], -100.0)
665 self.assertLess(xdot[dynamics.STATE_OMEGAD3, 0], -100.0)
666
Austin Schuh98fbbe82024-08-18 23:07:26 -0700667 def test_steer_coupling(self):
668 """Tests that the steer coupling factor cancels out steer coupling torque."""
669 steer_I = numpy.array(
670 [[dynamics.STEER_CURRENT_COUPLING_FACTOR * 10.0], [10.0]] * 4)
671
672 X = utils.state_vector(
673 velocity=numpy.array([[0.0], [0.0]]),
674 omega=0.0,
675 )
676 X_velocity = self.to_velocity_state(X)
677 Xdot = self.velocity_swerve_physics(X_velocity, steer_I)
678
679 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS0, 0], 0.0)
680 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS1, 0], 0.0)
681 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS2, 0], 0.0)
682 self.assertAlmostEqual(Xdot[dynamics.VELOCITY_STATE_OMEGAS3, 0], 0.0)
683
justinT21d18f79f2024-09-22 19:43:05 -0700684 def test_constant_torque(self):
685 """Tests that the robot exerts the same amount of torque no matter the orientation"""
686 steer_I = numpy.reshape(numpy.array([(i % 2) * 20 for i in range(8)]),
687 (8, 1))
688
689 X = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
690 omega=0.0,
691 module_angles=[
692 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
693 -numpy.pi / 4.0, numpy.pi / 4.0
694 ],
695 drive_wheel_velocity=1.0)
696 Xdot = self.swerve_full_dynamics(X, steer_I, skip_compare=True)
697
698 X_rot = utils.state_vector(velocity=numpy.array([[0.0], [0.0]]),
699 omega=0.0,
700 theta=numpy.pi,
701 module_angles=[
702 3 * numpy.pi / 4.0, -3 * numpy.pi / 4.0,
703 -numpy.pi / 4.0, numpy.pi / 4.0
704 ],
705 drive_wheel_velocity=1.0)
706 Xdot_rot = self.swerve_full_dynamics(X_rot, steer_I, skip_compare=True)
707
708 self.assertGreater(Xdot[dynamics.STATE_OMEGA, 0], 0.0)
709 self.assertAlmostEquals(Xdot[dynamics.STATE_OMEGA, 0],
710 Xdot_rot[dynamics.STATE_OMEGA, 0])
711
Austin Schuh2a1abec2024-07-10 20:31:16 -0700712
justinT2107d41ed2024-07-31 21:12:31 -0700713if __name__ == "__main__":
Austin Schuh2a1abec2024-07-10 20:31:16 -0700714 unittest.main()