James Kuszmaul | de44eb5 | 2024-10-08 23:39:24 -0700 | [diff] [blame] | 1 | #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 | |
| 11 | namespace 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. |
| 30 | template <typename Scalar = double> |
| 31 | class 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 ¶ms) : 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_ |