001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.wpilibj.simulation;
006
007import edu.wpi.first.math.Matrix;
008import edu.wpi.first.math.Nat;
009import edu.wpi.first.math.StateSpaceUtil;
010import edu.wpi.first.math.VecBuilder;
011import edu.wpi.first.math.geometry.Pose2d;
012import edu.wpi.first.math.geometry.Rotation2d;
013import edu.wpi.first.math.numbers.N1;
014import edu.wpi.first.math.numbers.N2;
015import edu.wpi.first.math.numbers.N7;
016import edu.wpi.first.math.system.LinearSystem;
017import edu.wpi.first.math.system.NumericalIntegration;
018import edu.wpi.first.math.system.plant.DCMotor;
019import edu.wpi.first.math.system.plant.LinearSystemId;
020import edu.wpi.first.math.util.Units;
021import edu.wpi.first.wpilibj.RobotController;
022
023/**
024 * This class simulates the state of the drivetrain. In simulationPeriodic, users should first set
025 * inputs from motors with {@link #setInputs(double, double)}, call {@link #update(double)} to
026 * update the simulation, and set estimated encoder and gyro positions, as well as estimated
027 * odometry pose. Teams can use {@link edu.wpi.first.wpilibj.smartdashboard.Field2d} to visualize
028 * their robot on the Sim GUI's field.
029 *
030 * <p>Our state-space system is:
031 *
032 * <p>x = [[x, y, theta, vel_l, vel_r, dist_l, dist_r]]ᵀ in the field coordinate system (dist_* are
033 * wheel distances.)
034 *
035 * <p>u = [[voltage_l, voltage_r]]ᵀ This is typically the control input of the last timestep from a
036 * LTVDiffDriveController.
037 *
038 * <p>y = x
039 */
040public class DifferentialDrivetrainSim {
041  private final DCMotor m_motor;
042  private final double m_originalGearing;
043  private final Matrix<N7, N1> m_measurementStdDevs;
044  private double m_currentGearing;
045  private final double m_wheelRadiusMeters;
046
047  private Matrix<N2, N1> m_u;
048  private Matrix<N7, N1> m_x;
049  private Matrix<N7, N1> m_y;
050
051  private final double m_rb;
052  private final LinearSystem<N2, N2, N2> m_plant;
053
054  /**
055   * Creates a simulated differential drivetrain.
056   *
057   * @param driveMotor A {@link DCMotor} representing the left side of the drivetrain.
058   * @param gearing The gearing ratio between motor and wheel, as output over input. This must be
059   *     the same ratio as the ratio used to identify or create the drivetrainPlant.
060   * @param jKgMetersSquared The moment of inertia of the drivetrain about its center.
061   * @param massKg The mass of the drivebase.
062   * @param wheelRadiusMeters The radius of the wheels on the drivetrain.
063   * @param trackWidthMeters The robot's track width, or distance between left and right wheels.
064   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
065   *     left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is
066   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
067   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
068   *     point.
069   */
070  public DifferentialDrivetrainSim(
071      DCMotor driveMotor,
072      double gearing,
073      double jKgMetersSquared,
074      double massKg,
075      double wheelRadiusMeters,
076      double trackWidthMeters,
077      Matrix<N7, N1> measurementStdDevs) {
078    this(
079        LinearSystemId.createDrivetrainVelocitySystem(
080            driveMotor,
081            massKg,
082            wheelRadiusMeters,
083            trackWidthMeters / 2.0,
084            jKgMetersSquared,
085            gearing),
086        driveMotor,
087        gearing,
088        trackWidthMeters,
089        wheelRadiusMeters,
090        measurementStdDevs);
091  }
092
093  /**
094   * Creates a simulated differential drivetrain.
095   *
096   * @param plant The {@link LinearSystem} representing the robot's drivetrain. This system can be
097   *     created with {@link
098   *     edu.wpi.first.math.system.plant.LinearSystemId#createDrivetrainVelocitySystem(DCMotor,
099   *     double, double, double, double, double)} or {@link
100   *     edu.wpi.first.math.system.plant.LinearSystemId#identifyDrivetrainSystem(double, double,
101   *     double, double)}.
102   * @param driveMotor A {@link DCMotor} representing the drivetrain.
103   * @param gearing The gearingRatio ratio of the robot, as output over input. This must be the same
104   *     ratio as the ratio used to identify or create the drivetrainPlant.
105   * @param trackWidthMeters The distance between the two sides of the drivetrian. Can be found with
106   *     SysId.
107   * @param wheelRadiusMeters The radius of the wheels on the drivetrain, in meters.
108   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
109   *     left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is
110   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
111   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
112   *     point.
113   */
114  public DifferentialDrivetrainSim(
115      LinearSystem<N2, N2, N2> plant,
116      DCMotor driveMotor,
117      double gearing,
118      double trackWidthMeters,
119      double wheelRadiusMeters,
120      Matrix<N7, N1> measurementStdDevs) {
121    this.m_plant = plant;
122    this.m_rb = trackWidthMeters / 2.0;
123    this.m_motor = driveMotor;
124    this.m_originalGearing = gearing;
125    this.m_measurementStdDevs = measurementStdDevs;
126    m_wheelRadiusMeters = wheelRadiusMeters;
127    m_currentGearing = m_originalGearing;
128
129    m_x = new Matrix<>(Nat.N7(), Nat.N1());
130    m_u = VecBuilder.fill(0, 0);
131    m_y = new Matrix<>(Nat.N7(), Nat.N1());
132  }
133
134  /**
135   * Sets the applied voltage to the drivetrain. Note that positive voltage must make that side of
136   * the drivetrain travel forward (+X).
137   *
138   * @param leftVoltageVolts The left voltage.
139   * @param rightVoltageVolts The right voltage.
140   */
141  public void setInputs(double leftVoltageVolts, double rightVoltageVolts) {
142    m_u = clampInput(VecBuilder.fill(leftVoltageVolts, rightVoltageVolts));
143  }
144
145  /**
146   * Update the drivetrain states with the current time difference.
147   *
148   * @param dtSeconds the time difference
149   */
150  public void update(double dtSeconds) {
151    m_x = NumericalIntegration.rkdp(this::getDynamics, m_x, m_u, dtSeconds);
152    m_y = m_x;
153    if (m_measurementStdDevs != null) {
154      m_y = m_y.plus(StateSpaceUtil.makeWhiteNoiseVector(m_measurementStdDevs));
155    }
156  }
157
158  /** Returns the full simulated state of the drivetrain. */
159  Matrix<N7, N1> getState() {
160    return m_x;
161  }
162
163  /**
164   * Get one of the drivetrain states.
165   *
166   * @param state the state to get
167   * @return the state
168   */
169  double getState(State state) {
170    return m_x.get(state.value, 0);
171  }
172
173  private double getOutput(State output) {
174    return m_y.get(output.value, 0);
175  }
176
177  /**
178   * Returns the direction the robot is pointing.
179   *
180   * <p>Note that this angle is counterclockwise-positive, while most gyros are clockwise positive.
181   *
182   * @return The direction the robot is pointing.
183   */
184  public Rotation2d getHeading() {
185    return new Rotation2d(getOutput(State.kHeading));
186  }
187
188  /**
189   * Returns the current pose.
190   *
191   * @return The current pose.
192   */
193  public Pose2d getPose() {
194    return new Pose2d(getOutput(State.kX), getOutput(State.kY), getHeading());
195  }
196
197  /**
198   * Get the right encoder position in meters.
199   *
200   * @return The encoder position.
201   */
202  public double getRightPositionMeters() {
203    return getOutput(State.kRightPosition);
204  }
205
206  /**
207   * Get the right encoder velocity in meters per second.
208   *
209   * @return The encoder velocity.
210   */
211  public double getRightVelocityMetersPerSecond() {
212    return getOutput(State.kRightVelocity);
213  }
214
215  /**
216   * Get the left encoder position in meters.
217   *
218   * @return The encoder position.
219   */
220  public double getLeftPositionMeters() {
221    return getOutput(State.kLeftPosition);
222  }
223
224  /**
225   * Get the left encoder velocity in meters per second.
226   *
227   * @return The encoder velocity.
228   */
229  public double getLeftVelocityMetersPerSecond() {
230    return getOutput(State.kLeftVelocity);
231  }
232
233  /**
234   * Get the current draw of the left side of the drivetrain.
235   *
236   * @return the drivetrain's left side current draw, in amps
237   */
238  public double getLeftCurrentDrawAmps() {
239    return m_motor.getCurrent(
240            getState(State.kLeftVelocity) * m_currentGearing / m_wheelRadiusMeters, m_u.get(0, 0))
241        * Math.signum(m_u.get(0, 0));
242  }
243
244  /**
245   * Get the current draw of the right side of the drivetrain.
246   *
247   * @return the drivetrain's right side current draw, in amps
248   */
249  public double getRightCurrentDrawAmps() {
250    return m_motor.getCurrent(
251            getState(State.kRightVelocity) * m_currentGearing / m_wheelRadiusMeters, m_u.get(1, 0))
252        * Math.signum(m_u.get(1, 0));
253  }
254
255  /**
256   * Get the current draw of the drivetrain.
257   *
258   * @return the current draw, in amps
259   */
260  public double getCurrentDrawAmps() {
261    return getLeftCurrentDrawAmps() + getRightCurrentDrawAmps();
262  }
263
264  /**
265   * Get the drivetrain gearing.
266   *
267   * @return the gearing ration
268   */
269  public double getCurrentGearing() {
270    return m_currentGearing;
271  }
272
273  /**
274   * Sets the gearing reduction on the drivetrain. This is commonly used for shifting drivetrains.
275   *
276   * @param newGearRatio The new gear ratio, as output over input.
277   */
278  public void setCurrentGearing(double newGearRatio) {
279    this.m_currentGearing = newGearRatio;
280  }
281
282  /**
283   * Sets the system state.
284   *
285   * @param state The state.
286   */
287  public void setState(Matrix<N7, N1> state) {
288    m_x = state;
289  }
290
291  /**
292   * Sets the system pose.
293   *
294   * @param pose The pose.
295   */
296  public void setPose(Pose2d pose) {
297    m_x.set(State.kX.value, 0, pose.getX());
298    m_x.set(State.kY.value, 0, pose.getY());
299    m_x.set(State.kHeading.value, 0, pose.getRotation().getRadians());
300    m_x.set(State.kLeftPosition.value, 0, 0);
301    m_x.set(State.kRightPosition.value, 0, 0);
302  }
303
304  protected Matrix<N7, N1> getDynamics(Matrix<N7, N1> x, Matrix<N2, N1> u) {
305    // Because G can be factored out of B, we can divide by the old ratio and multiply
306    // by the new ratio to get a new drivetrain model.
307    var B = new Matrix<>(Nat.N4(), Nat.N2());
308    B.assignBlock(0, 0, m_plant.getB().times(this.m_currentGearing / this.m_originalGearing));
309
310    // Because G² can be factored out of A, we can divide by the old ratio squared and multiply
311    // by the new ratio squared to get a new drivetrain model.
312    var A = new Matrix<>(Nat.N4(), Nat.N4());
313    A.assignBlock(
314        0,
315        0,
316        m_plant
317            .getA()
318            .times(
319                (this.m_currentGearing * this.m_currentGearing)
320                    / (this.m_originalGearing * this.m_originalGearing)));
321
322    A.assignBlock(2, 0, Matrix.eye(Nat.N2()));
323
324    var v = (x.get(State.kLeftVelocity.value, 0) + x.get(State.kRightVelocity.value, 0)) / 2.0;
325
326    var xdot = new Matrix<>(Nat.N7(), Nat.N1());
327    xdot.set(0, 0, v * Math.cos(x.get(State.kHeading.value, 0)));
328    xdot.set(1, 0, v * Math.sin(x.get(State.kHeading.value, 0)));
329    xdot.set(
330        2,
331        0,
332        (x.get(State.kRightVelocity.value, 0) - x.get(State.kLeftVelocity.value, 0))
333            / (2.0 * m_rb));
334    xdot.assignBlock(3, 0, A.times(x.block(Nat.N4(), Nat.N1(), 3, 0)).plus(B.times(u)));
335
336    return xdot;
337  }
338
339  /**
340   * Clamp the input vector such that no element exceeds the battery voltage. If any does, the
341   * relative magnitudes of the input will be maintained.
342   *
343   * @param u The input vector.
344   * @return The normalized input.
345   */
346  protected Matrix<N2, N1> clampInput(Matrix<N2, N1> u) {
347    return StateSpaceUtil.desaturateInputVector(u, RobotController.getBatteryVoltage());
348  }
349
350  /** Represents the different states of the drivetrain. */
351  enum State {
352    kX(0),
353    kY(1),
354    kHeading(2),
355    kLeftVelocity(3),
356    kRightVelocity(4),
357    kLeftPosition(5),
358    kRightPosition(6);
359
360    public final int value;
361
362    State(int i) {
363      this.value = i;
364    }
365  }
366
367  /**
368   * Represents a gearing option of the Toughbox mini. 12.75:1 -- 14:50 and 14:50 10.71:1 -- 14:50
369   * and 16:48 8.45:1 -- 14:50 and 19:45 7.31:1 -- 14:50 and 21:43 5.95:1 -- 14:50 and 24:40
370   */
371  public enum KitbotGearing {
372    k12p75(12.75),
373    k10p71(10.71),
374    k8p45(8.45),
375    k7p31(7.31),
376    k5p95(5.95);
377
378    public final double value;
379
380    KitbotGearing(double i) {
381      this.value = i;
382    }
383  }
384
385  /** Represents common motor layouts of the kit drivetrain. */
386  public enum KitbotMotor {
387    kSingleCIMPerSide(DCMotor.getCIM(1)),
388    kDualCIMPerSide(DCMotor.getCIM(2)),
389    kSingleMiniCIMPerSide(DCMotor.getMiniCIM(1)),
390    kDualMiniCIMPerSide(DCMotor.getMiniCIM(2)),
391    kSingleFalcon500PerSide(DCMotor.getFalcon500(1)),
392    kDoubleFalcon500PerSide(DCMotor.getFalcon500(2)),
393    kSingleNEOPerSide(DCMotor.getNEO(1)),
394    kDoubleNEOPerSide(DCMotor.getNEO(2));
395
396    public final DCMotor value;
397
398    KitbotMotor(DCMotor i) {
399      this.value = i;
400    }
401  }
402
403  /** Represents common wheel sizes of the kit drivetrain. */
404  public enum KitbotWheelSize {
405    kSixInch(Units.inchesToMeters(6)),
406    kEightInch(Units.inchesToMeters(8)),
407    kTenInch(Units.inchesToMeters(10));
408
409    public final double value;
410
411    KitbotWheelSize(double i) {
412      this.value = i;
413    }
414  }
415
416  /**
417   * Create a sim for the standard FRC kitbot.
418   *
419   * @param motor The motors installed in the bot.
420   * @param gearing The gearing reduction used.
421   * @param wheelSize The wheel size.
422   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
423   *     left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is
424   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
425   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
426   *     point.
427   * @return A sim for the standard FRC kitbot.
428   */
429  public static DifferentialDrivetrainSim createKitbotSim(
430      KitbotMotor motor,
431      KitbotGearing gearing,
432      KitbotWheelSize wheelSize,
433      Matrix<N7, N1> measurementStdDevs) {
434    // MOI estimation -- note that I = mr² for point masses
435    var batteryMoi = 12.5 / 2.2 * Math.pow(Units.inchesToMeters(10), 2);
436    var gearboxMoi =
437        (2.8 /* CIM motor */ * 2 / 2.2 + 2.0 /* Toughbox Mini- ish */)
438            * Math.pow(Units.inchesToMeters(26.0 / 2.0), 2);
439
440    return createKitbotSim(motor, gearing, wheelSize, batteryMoi + gearboxMoi, measurementStdDevs);
441  }
442
443  /**
444   * Create a sim for the standard FRC kitbot.
445   *
446   * @param motor The motors installed in the bot.
447   * @param gearing The gearing reduction used.
448   * @param wheelSize The wheel size.
449   * @param jKgMetersSquared The moment of inertia of the drivebase. This can be calculated using
450   *     SysId.
451   * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading,
452   *     left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is
453   *     desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05
454   *     m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting
455   *     point.
456   * @return A sim for the standard FRC kitbot.
457   */
458  public static DifferentialDrivetrainSim createKitbotSim(
459      KitbotMotor motor,
460      KitbotGearing gearing,
461      KitbotWheelSize wheelSize,
462      double jKgMetersSquared,
463      Matrix<N7, N1> measurementStdDevs) {
464    return new DifferentialDrivetrainSim(
465        motor.value,
466        gearing.value,
467        jKgMetersSquared,
468        Units.lbsToKilograms(60),
469        wheelSize.value / 2.0,
470        Units.inchesToMeters(26),
471        measurementStdDevs);
472  }
473}