blob: 9afe63639ae1ae282ddbc7f5761c4593ffa4d0d1 [file] [log] [blame]
James Kuszmaulde44eb52024-10-08 23:39:24 -07001#ifndef FRC971_CONTROL_LOOPS_SWERVE_SIMPLIFIED_DYNAMICS_H_
2#define FRC971_CONTROL_LOOPS_SWERVE_SIMPLIFIED_DYNAMICS_H_
3#include "absl/log/check.h"
4#include "absl/log/log.h"
5
6#include "aos/util/math.h"
7#include "frc971/control_loops/swerve/auto_diff_jacobian.h"
8#include "frc971/control_loops/swerve/dynamics.h"
9#include "frc971/control_loops/swerve/motors.h"
10
11namespace frc971::control_loops::swerve {
12
13// Provides a simplified set of physics representing a swerve drivetrain.
14// Broadly speaking, these dynamics model:
15// * Standard motors on the drive and steer axes, with no coupling between
16// the motors.
17// * Assume that the steer direction of each module is only influenced by
18// the inertia of the motor rotor plus some small aligning force.
19// * Assume that torque from the drive motor is transferred directly to the
20// carpet.
21// * Assume that a lateral force on the wheel is generated proportional to the
22// slip angle of the wheel.
23//
24// This class is templated on a Scalar that is used to determine whether you
25// want to use double or single-precision floats for the calculations here.
26//
27// Several individual methods on this class are also templated on a LocalScalar
28// type. This is provided to allow those methods to be called with ceres Jets to
29// do autodifferentiation within various solvers/jacobian calculators.
30template <typename Scalar = double>
31class SimplifiedDynamics {
32 public:
33 struct ModuleParams {
34 // Module position relative to the center of mass of the robot.
35 Eigen::Matrix<Scalar, 2, 1> position;
36 // Coefficient dictating how much sideways force is generated at a given
37 // slip angle. Units are effectively Newtons / radian of slip.
38 Scalar slip_angle_coefficient;
39 // Coefficient dicating how much the steer wheel is forced into alignment
40 // by the motion of the wheel over the ground (i.e., we are assuming that
41 // if you push the robot along it will cause the wheels to eventually
42 // align with the direction of motion).
43 // In radians / sec^2 / radians of slip angle.
44 Scalar slip_angle_alignment_coefficient;
45 // Parameters for the steer and drive motors.
46 Motor steer_motor;
47 Motor drive_motor;
48 // radians of module steering = steer_ratio * radians of motor shaft
49 Scalar steer_ratio;
50 // meters of driving = drive_ratio * radians of motor shaft
51 Scalar drive_ratio;
52 };
53 struct Parameters {
54 // Mass of the robot, in kg.
55 Scalar mass;
56 // Moment of inertia of the robot about the yaw axis, in kg * m^2.
57 Scalar moment_of_inertia;
58 // Note: While this technically would support an arbitrary number of
59 // modules, the statically-sized state vectors do limit us to 4 modules
60 // currently, and it should not be counted on that other pieces of code will
61 // be able to support non-4-module swerves.
62 std::vector<ModuleParams> modules;
63 };
64 enum States {
65 // Thetas* and Omegas* are the yaw and yaw rate of the indicated modules.
66 // (note that we do not actually need to track drive speed per module,
67 // as with current control we have the ability to directly command torque
68 // to those motors; however, if we wished to account for the saturation
69 // limits on the motor, then we would need to have access to those states,
70 // although they can be fully derived from the robot vx, vy, theta, and
71 // omega).
72 kThetas0 = 0,
73 kOmegas0 = 1,
74 kThetas1 = 2,
75 kOmegas1 = 3,
76 kThetas2 = 4,
77 kOmegas2 = 5,
78 kThetas3 = 6,
79 kOmegas3 = 7,
80 // Robot yaw, in radians.
81 kTheta = 8,
82 // Robot speed in the global frame, in meters / sec.
83 kVx = 9,
84 kVy = 10,
85 // Robot yaw rate, in radians / sec.
86 kOmega = 11,
87 kNumVelocityStates = 12,
88 // Augmented states for doing position control.
89 // Robot X position in the global frame.
90 kX = 12,
91 // Robot Y position in the global frame.
92 kY = 13,
93 kNumPositionStates = 14,
94 };
95 using Inputs = InputStates::States;
96
97 template <typename ScalarT = Scalar>
98 using VelocityState = Eigen::Matrix<ScalarT, kNumVelocityStates, 1>;
99 template <typename ScalarT = Scalar>
100 using PositionState = Eigen::Matrix<ScalarT, kNumPositionStates, 1>;
101 template <typename ScalarT = Scalar>
102 using VelocityStateSquare =
103 Eigen::Matrix<ScalarT, kNumVelocityStates, kNumVelocityStates>;
104 template <typename ScalarT = Scalar>
105 using PositionStateSquare =
106 Eigen::Matrix<ScalarT, kNumPositionStates, kNumPositionStates>;
107 template <typename ScalarT = Scalar>
108 using PositionBMatrix =
109 Eigen::Matrix<ScalarT, kNumPositionStates, kNumInputs>;
110 template <typename ScalarT = Scalar>
111 using Input = Eigen::Matrix<ScalarT, kNumInputs, 1>;
112
113 SimplifiedDynamics(const Parameters &params) : params_(params) {
114 for (size_t module_index = 0; module_index < params_.modules.size();
115 ++module_index) {
116 module_dynamics_.emplace_back(params_, module_index);
117 }
118 }
119
120 // Returns the derivative of state for the given state and input.
121 template <typename LocalScalar>
122 PositionState<LocalScalar> Dynamics(const PositionState<LocalScalar> &state,
123 const Input<LocalScalar> &input) const {
124 PositionState<LocalScalar> Xdot = PositionState<LocalScalar>::Zero();
125
126 for (const ModuleDynamics &module : module_dynamics_) {
127 Xdot += module.PartialDynamics(state, input);
128 }
129
130 // And finally catch the global states:
131 Xdot(kX) = state(kVx);
132 Xdot(kY) = state(kVy);
133 Xdot(kTheta) = state(kOmega);
134
135 return Xdot;
136 }
137
138 template <typename LocalScalar>
139 VelocityState<LocalScalar> VelocityDynamics(
140 const VelocityState<LocalScalar> &state,
141 const Input<LocalScalar> &input) const {
142 PositionState<LocalScalar> input_state = PositionState<LocalScalar>::Zero();
143 input_state.template topRows<kNumVelocityStates>() = state;
144 return Dynamics(input_state, input).template topRows<kNumVelocityStates>();
145 }
146
147 std::pair<PositionStateSquare<>, PositionBMatrix<>> LinearizedDynamics(
148 const PositionState<> &state, const Input<> &input) {
149 DynamicsFunctor functor(*this);
150 Eigen::Matrix<Scalar, kNumPositionStates + kNumInputs, 1> parameters;
151 parameters.template topRows<kNumPositionStates>() = state;
152 parameters.template bottomRows<kNumInputs>() = input;
153 const Eigen::Matrix<Scalar, kNumPositionStates,
154 kNumPositionStates + kNumInputs>
155 jacobian =
156 AutoDiffJacobian<Scalar, DynamicsFunctor,
157 kNumPositionStates + kNumInputs,
158 kNumPositionStates>::Jacobian(functor, parameters);
159 return {
160 jacobian.template block<kNumPositionStates, kNumPositionStates>(0, 0),
161 jacobian.template block<kNumPositionStates, kNumInputs>(
162 0, kNumPositionStates)};
163 }
164
165 private:
166 // Wrapper to provide an operator() for the dynamisc class that allows it to
167 // be used by the auto-differentiation code.
168 class DynamicsFunctor {
169 public:
170 DynamicsFunctor(const SimplifiedDynamics &dynamics) : dynamics_(dynamics) {}
171
172 template <typename LocalScalar>
173 Eigen::Matrix<LocalScalar, kNumPositionStates, 1> operator()(
174 const Eigen::Map<const Eigen::Matrix<
175 LocalScalar, kNumPositionStates + kNumInputs, 1>>
176 input) const {
177 return dynamics_.Dynamics(
178 PositionState<LocalScalar>(
179 input.template topRows<kNumPositionStates>()),
180 Input<LocalScalar>(input.template bottomRows<kNumInputs>()));
181 }
182
183 private:
184 const SimplifiedDynamics &dynamics_;
185 };
186
187 // Represents the dynamics of an individual module.
188 class ModuleDynamics {
189 public:
190 ModuleDynamics(const Parameters &robot_params, const size_t module_index)
191 : robot_params_(robot_params), module_index_(module_index) {
192 CHECK_LT(module_index_, robot_params_.modules.size());
193 }
194
195 // This returns the portions of the derivative of state that are due to the
196 // individual module. The result from this function should be able to be
197 // naively summed with the dynamics for each other module plus some global
198 // dynamics (which take care of that e.g. xdot = vx) and give you the
199 // overall dynamics of the system.
200 template <typename LocalScalar>
201 PositionState<LocalScalar> PartialDynamics(
202 const PositionState<LocalScalar> &state,
203 const Input<LocalScalar> &input) const {
204 PositionState<LocalScalar> Xdot = PositionState<LocalScalar>::Zero();
205
206 Xdot(ThetasIdx()) = state(OmegasIdx());
207
208 // Steering dynamics for an individual module assume ~zero friction,
209 // and thus ~the only inertia is from the motor rotor itself.
210 // torque_motor = stall_torque / stall_current * current
211 // accel_motor = torque_motor / motor_inertia
212 // accel_steer = accel_motor * steer_ratio
213 const Motor &steer_motor = module_params().steer_motor;
214 const LocalScalar steer_motor_accel =
215 input(IsIdx()) *
216 static_cast<Scalar>(
217 module_params().steer_ratio * steer_motor.stall_torque /
218 (steer_motor.stall_current * steer_motor.motor_inertia));
219
220 // For the impacts of the modules on the overall robot
221 // dynamics (X, Y, and theta acceleration), we calculate the forces
222 // generated by the module and then apply them. These forces come from
223 // two effects in this model:
224 // 1. Slip angle of the module (dependent on the current robot velocity &
225 // module steer angle).
226 // 2. Drive torque from the module (dependent on the current drive
227 // current and module steer angle).
228 // We assume no torque is generated from e.g. the wheel resisting the
229 // steering motion.
230 //
231 // clang-format off
232 //
233 // For slip angle we have:
234 // wheel_velocity = R(-theta - theta_steer) * (
235 // robot_vel + omega.cross(R(theta) * module_position))
236 // slip_angle = -atan2(wheel_velocity)
237 // slip_force = slip_angle_coefficient * slip_angle
238 // slip_force_direction = theta + theta_steer + pi / 2
239 // force_x = slip_force * cos(slip_force_direction)
240 // force_y = slip_force * sin(slip_force_direction)
241 // accel_* = force_* / mass
242 // # And now calculate torque from slip angle.
243 // torque_vec = module_position.cross([slip_force * cos(theta_steer + pi / 2),
244 // slip_force * sin(theta_steer + pi / 2),
245 // 0.0])
246 // torque_vec = module_position.cross([slip_force * -sin(theta_steer),
247 // slip_force * cos(theta_steer),
248 // 0.0])
249 // robot_torque = torque_vec.z()
250 //
251 // For drive torque we have:
252 // drive_force = (drive_current * stall_torque / stall_current) / drive_ratio
253 // drive_force_direction = theta + theta_steer
254 // force_x = drive_force * cos(drive_force_direction)
255 // force_y = drive_force * sin(drive_force_direction)
256 // torque_vec = drive_force * module_position.cross([cos(theta_steer),
257 // sin(theta_steer),
258 // 0.0])
259 // torque = torque_vec.z()
260 //
261 // clang-format on
262
263 const Eigen::Matrix<Scalar, 3, 1> module_position{
264 {module_params().position.x()},
265 {module_params().position.y()},
266 {0.0}};
267 const LocalScalar theta = state(kTheta);
268 const LocalScalar theta_steer = state(ThetasIdx());
269 const Eigen::Matrix<LocalScalar, 3, 1> wheel_velocity_in_global_frame =
270 Eigen::Matrix<LocalScalar, 3, 1>(state(kVx), state(kVy),
271 static_cast<LocalScalar>(0.0)) +
272 (Eigen::Matrix<LocalScalar, 3, 1>(static_cast<LocalScalar>(0.0),
273 static_cast<LocalScalar>(0.0),
274 state(kOmega))
275 .cross(Eigen::AngleAxis<LocalScalar>(
276 theta, Eigen::Matrix<LocalScalar, 3, 1>::UnitZ()) *
277 module_position));
278 const Eigen::Matrix<LocalScalar, 3, 1> wheel_velocity_in_wheel_frame =
279 Eigen::AngleAxis<LocalScalar>(
280 -theta - theta_steer, Eigen::Matrix<LocalScalar, 3, 1>::UnitZ()) *
281 wheel_velocity_in_global_frame;
282 // The complicated dynamics use some obnoxious-looking functions to
283 // try to approximate how the slip angle behaves a low speeds to better
284 // condition the dynamics. Because I couldn't be bothered to copy those
285 // dynamics, instead just bias the slip angle to zero at low speeds.
286 const LocalScalar wheel_speed = wheel_velocity_in_wheel_frame.norm();
287 const Scalar start_speed = 0.1;
288 const LocalScalar heading_truth_proportion = -expm1(
289 /*arbitrary large number=*/static_cast<Scalar>(-100.0) *
290 (wheel_speed - start_speed));
291 const LocalScalar wheel_heading =
292 (wheel_speed < start_speed)
293 ? static_cast<LocalScalar>(0.0)
294 : heading_truth_proportion *
295 atan2(wheel_velocity_in_wheel_frame.y(),
296 wheel_velocity_in_wheel_frame.x());
297
298 // We wrap slip_angle with a sin() not because there is actually a sin()
299 // in the real math but rather because we need to smoothly and correctly
300 // handle slip angles between pi / 2 and 3 * pi / 2.
301 const LocalScalar slip_angle = sin(-wheel_heading);
302 const LocalScalar slip_force =
303 module_params().slip_angle_coefficient * slip_angle;
304 const LocalScalar slip_force_direction =
305 theta + theta_steer + static_cast<Scalar>(M_PI_2);
306 const Eigen::Matrix<LocalScalar, 3, 1> slip_force_vec =
307 slip_force * UnitYawVector<LocalScalar>(slip_force_direction);
308 const LocalScalar slip_torque =
309 module_position
310 .cross(slip_force *
311 UnitYawVector<LocalScalar>(theta_steer +
312 static_cast<Scalar>(M_PI_2)))
313 .z();
314
315 // drive torque calculations
316 const Motor &drive_motor = module_params().drive_motor;
317 const LocalScalar drive_force =
318 input(IdIdx()) * static_cast<Scalar>(drive_motor.stall_torque /
319 drive_motor.stall_current /
320 module_params().drive_ratio);
321 const Eigen::Matrix<LocalScalar, 3, 1> drive_force_vec =
322 drive_force * UnitYawVector<LocalScalar>(theta + theta_steer);
323 const LocalScalar drive_torque =
324 drive_force *
325 module_position.cross(UnitYawVector<LocalScalar>(theta_steer)).z();
326 // We add in an aligning force on the wheels primarily to help provide a
327 // bit of impetus to the controllers/solvers to discourage aggressive
328 // slip angles. If we do not include this, then the dynamics make it look
329 // like there are no losses to using extremely aggressive slip angles.
330 const LocalScalar wheel_alignment_accel =
331 -module_params().slip_angle_alignment_coefficient * slip_angle;
332
333 Xdot(OmegasIdx()) = steer_motor_accel + wheel_alignment_accel;
334 // Sum up all the forces.
335 Xdot(kVx) =
336 (slip_force_vec.x() + drive_force_vec.x()) / robot_params_.mass;
337 Xdot(kVy) =
338 (slip_force_vec.y() + drive_force_vec.y()) / robot_params_.mass;
339 Xdot(kOmega) =
340 (slip_torque + drive_torque) / robot_params_.moment_of_inertia;
341
342 return Xdot;
343 }
344
345 private:
346 template <typename LocalScalar>
347 Eigen::Matrix<LocalScalar, 3, 1> UnitYawVector(LocalScalar yaw) const {
348 return Eigen::Matrix<LocalScalar, 3, 1>{
349 {static_cast<LocalScalar>(cos(yaw))},
350 {static_cast<LocalScalar>(sin(yaw))},
351 {static_cast<LocalScalar>(0.0)}};
352 }
353 size_t ThetasIdx() const { return kThetas0 + 2 * module_index_; }
354 size_t OmegasIdx() const { return kOmegas0 + 2 * module_index_; }
355 size_t IsIdx() const { return Inputs::kIs0 + 2 * module_index_; }
356 size_t IdIdx() const { return Inputs::kId0 + 2 * module_index_; }
357
358 const ModuleParams &module_params() const {
359 return robot_params_.modules[module_index_];
360 }
361
362 const Parameters robot_params_;
363
364 const size_t module_index_;
365 };
366
367 Parameters params_;
368 std::vector<ModuleDynamics> module_dynamics_;
369};
370
371} // namespace frc971::control_loops::swerve
372#endif // FRC971_CONTROL_LOOPS_SWERVE_SIMPLIFIED_DYNAMICS_H_