Austin Schuh | 2a1abec | 2024-07-10 20:31:16 -0700 | [diff] [blame^] | 1 | #!/usr/bin/python3 |
| 2 | |
| 3 | import numpy |
| 4 | import sys, os |
| 5 | import casadi |
| 6 | from numpy.testing import assert_array_equal, assert_array_almost_equal |
| 7 | import unittest |
| 8 | |
| 9 | from frc971.control_loops.swerve import dynamics |
| 10 | |
| 11 | |
| 12 | def state_vector(velocity=numpy.array([[1.0], [0.0]]), |
| 13 | dx=0.0, |
| 14 | dy=0.0, |
| 15 | theta=0.0, |
| 16 | omega=0.0, |
| 17 | module_omega=0.0, |
| 18 | module_angle=0.0): |
| 19 | """Returns the state vector with the requested state.""" |
| 20 | X_initial = numpy.zeros((25, 1)) |
| 21 | # All the wheels are spinning at the speed needed to hit the velocity in m/s |
| 22 | X_initial[2, 0] = module_omega |
| 23 | X_initial[3, 0] = numpy.linalg.norm(velocity) / (dynamics.WHEEL_RADIUS) |
| 24 | |
| 25 | X_initial[6, 0] = module_omega |
| 26 | X_initial[7, 0] = numpy.linalg.norm(velocity) / (dynamics.WHEEL_RADIUS) |
| 27 | |
| 28 | X_initial[10, 0] = module_omega |
| 29 | X_initial[11, 0] = numpy.linalg.norm(velocity) / (dynamics.WHEEL_RADIUS) |
| 30 | |
| 31 | X_initial[14, 0] = module_omega |
| 32 | X_initial[15, 0] = numpy.linalg.norm(velocity) / (dynamics.WHEEL_RADIUS) |
| 33 | |
| 34 | X_initial[0, 0] = module_angle |
| 35 | X_initial[4, 0] = module_angle |
| 36 | X_initial[8, 0] = module_angle |
| 37 | X_initial[12, 0] = module_angle |
| 38 | |
| 39 | X_initial[18, 0] = theta |
| 40 | |
| 41 | X_initial[19, 0] = velocity[0, 0] + dx |
| 42 | X_initial[20, 0] = velocity[1, 0] + dy |
| 43 | X_initial[21, 0] = omega |
| 44 | |
| 45 | return X_initial |
| 46 | |
| 47 | |
| 48 | class TestHPolytope(unittest.TestCase): |
| 49 | I = numpy.zeros((8, 1)) |
| 50 | |
| 51 | def setUp(self): |
| 52 | pass |
| 53 | |
| 54 | def test_contact_patch_velocity(self): |
| 55 | """Tests that the contact patch velocity makes sense.""" |
| 56 | for i in range(4): |
| 57 | contact_patch_velocity = dynamics.contact_patch_velocity( |
| 58 | i, casadi.SX.sym("X", 25, 1), casadi.SX.sym("U", 8, 1)) |
| 59 | wheel_ground_velocity = dynamics.wheel_ground_velocity( |
| 60 | i, casadi.SX.sym("X", 25, 1), casadi.SX.sym("U", 8, 1)) |
| 61 | |
| 62 | # No angular velocity should result in just linear motion. |
| 63 | for velocity in [ |
| 64 | numpy.array([[1.5], [0.0]]), |
| 65 | numpy.array([[0.0], [1.0]]), |
| 66 | numpy.array([[-1.5], [0.0]]), |
| 67 | numpy.array([[0.0], [-1.0]]), |
| 68 | numpy.array([[2.0], [-1.7]]), |
| 69 | ]: |
| 70 | for theta in [0.0, 1.0, -1.0, 100.0]: |
| 71 | patch_velocity = numpy.array( |
| 72 | contact_patch_velocity( |
| 73 | state_vector(velocity=velocity, theta=theta), |
| 74 | self.I)) |
| 75 | |
| 76 | assert_array_equal(patch_velocity, velocity) |
| 77 | |
| 78 | # Now, test that spinning the robot results in module velocities. |
| 79 | # We are assuming that each module is on a square robot. |
| 80 | module_center_of_mass_angle = i * numpy.pi / 2.0 + numpy.pi / 4.0 |
| 81 | for theta in [-module_center_of_mass_angle, 0.0, 1.0, -1.0, 100.0]: |
| 82 | for omega in [0.65, -0.1]: |
| 83 | # Point each module to the center to make the math easier. |
| 84 | patch_velocity = numpy.array( |
| 85 | contact_patch_velocity( |
| 86 | state_vector( |
| 87 | velocity=numpy.array([[0.0], [0.0]]), |
| 88 | theta=theta, |
| 89 | omega=omega, |
| 90 | module_angle=module_center_of_mass_angle), |
| 91 | self.I)) |
| 92 | |
| 93 | assert_array_almost_equal( |
| 94 | patch_velocity, |
| 95 | (dynamics.ROBOT_WIDTH / numpy.sqrt(2.0) + |
| 96 | dynamics.CASTER) * omega * numpy.array([[ |
| 97 | -numpy.sin(theta + module_center_of_mass_angle) |
| 98 | ], [numpy.cos(theta + module_center_of_mass_angle)]])) |
| 99 | |
| 100 | # Point the wheel along +x, rotate it by theta, then spin it. |
| 101 | # Confirm the velocities come out right. |
| 102 | patch_velocity = numpy.array( |
| 103 | contact_patch_velocity( |
| 104 | state_vector( |
| 105 | velocity=numpy.array([[0.0], [0.0]]), |
| 106 | theta=-module_center_of_mass_angle, |
| 107 | module_omega=omega, |
| 108 | module_angle=(theta + |
| 109 | module_center_of_mass_angle)), |
| 110 | self.I)) |
| 111 | |
| 112 | assert_array_almost_equal( |
| 113 | patch_velocity, |
| 114 | dynamics.CASTER * omega * |
| 115 | numpy.array([[-numpy.sin(theta)], [numpy.cos(theta)]])) |
| 116 | |
| 117 | # Now, test that the rotation back to wheel coordinates works. |
| 118 | # The easiest way to do this is to point the wheel in a direction, |
| 119 | # move in that direction, and confirm that there is no lateral velocity. |
| 120 | for robot_angle in [0.0, 1.0, -5.0]: |
| 121 | for module_angle in [0.0, 1.0, -5.0]: |
| 122 | wheel_patch_velocity = numpy.array( |
| 123 | wheel_ground_velocity( |
| 124 | state_vector(velocity=numpy.array( |
| 125 | [[numpy.cos(robot_angle + module_angle)], |
| 126 | [numpy.sin(robot_angle + module_angle)]]), |
| 127 | theta=robot_angle, |
| 128 | module_angle=module_angle), self.I)) |
| 129 | |
| 130 | assert_array_almost_equal(wheel_patch_velocity, |
| 131 | numpy.array([[1], [0]])) |
| 132 | |
| 133 | def test_slip_angle(self): |
| 134 | """Tests that the slip_angle calculation works.""" |
| 135 | velocity = numpy.array([[1.5], [0.0]]) |
| 136 | |
| 137 | for i in range(4): |
| 138 | x = casadi.SX.sym("x") |
| 139 | y = casadi.SX.sym("y") |
| 140 | half_atan2 = casadi.Function('half_atan2', [y, x], |
| 141 | [dynamics.half_atan2(y, x)]) |
| 142 | |
| 143 | slip_angle = dynamics.slip_angle(i, casadi.SX.sym("X", 25, 1), |
| 144 | casadi.SX.sym("U", 8, 1)) |
| 145 | |
| 146 | for wrap in range(-3, 3): |
| 147 | for theta in [0.0, 0.6, -0.4]: |
| 148 | module_angle = numpy.pi * wrap + theta |
| 149 | |
| 150 | self.assertAlmostEqual( |
| 151 | theta, |
| 152 | half_atan2(numpy.sin(module_angle), |
| 153 | numpy.cos(module_angle))) |
| 154 | |
| 155 | computed_angle = slip_angle( |
| 156 | state_vector(velocity=velocity, |
| 157 | module_angle=numpy.pi * wrap + theta), |
| 158 | self.I) |
| 159 | |
| 160 | self.assertAlmostEqual(theta, computed_angle) |
| 161 | |
| 162 | def test_wheel_forces(self): |
| 163 | """Tests that the per module forces have the right signs.""" |
| 164 | for i in range(4): |
| 165 | wheel_force = dynamics.wheel_force(i, casadi.SX.sym("X", 25, 1), |
| 166 | casadi.SX.sym("U", 8, 1)) |
| 167 | |
| 168 | # Robot is moving faster than the wheels, it should decelerate. |
| 169 | robot_faster = numpy.array( |
| 170 | wheel_force(state_vector(dx=0.01), self.I)) |
| 171 | self.assertLess(robot_faster[0, 0], -0.1) |
| 172 | self.assertEqual(robot_faster[1, 0], 0.0) |
| 173 | |
| 174 | # Robot is now going slower than the wheels. It should accelerate. |
| 175 | robot_slower = numpy.array( |
| 176 | wheel_force(state_vector(dx=-0.01), self.I)) |
| 177 | self.assertGreater(robot_slower[0, 0], 0.1) |
| 178 | self.assertEqual(robot_slower[1, 0], 0.0) |
| 179 | |
| 180 | # Positive lateral velocity -> negative force. |
| 181 | robot_left = numpy.array(wheel_force(state_vector(dy=0.01), |
| 182 | self.I)) |
| 183 | self.assertEqual(robot_left[0, 0], 0.0) |
| 184 | self.assertLess(robot_left[1, 0], -0.1) |
| 185 | |
| 186 | # Negative lateral velocity -> positive force. |
| 187 | robot_right = numpy.array( |
| 188 | wheel_force(state_vector(dy=-0.01), self.I)) |
| 189 | self.assertEqual(robot_right[0, 0], 0.0) |
| 190 | self.assertGreater(robot_right[1, 0], 0.1) |
| 191 | |
| 192 | |
| 193 | if __name__ == '__main__': |
| 194 | unittest.main() |