// Copyright (c) FIRST and other WPILib contributors. // Open Source Software; you can modify and/or share it under the terms of // the WPILib BSD license file in the root directory of this project. package edu.wpi.first.wpilibj.simulation; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.Nat; import edu.wpi.first.math.StateSpaceUtil; import edu.wpi.first.math.VecBuilder; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N2; import edu.wpi.first.math.numbers.N7; import edu.wpi.first.math.system.LinearSystem; import edu.wpi.first.math.system.NumericalIntegration; import edu.wpi.first.math.system.plant.DCMotor; import edu.wpi.first.math.system.plant.LinearSystemId; import edu.wpi.first.math.util.Units; import edu.wpi.first.wpilibj.RobotController; /** * This class simulates the state of the drivetrain. In simulationPeriodic, users should first set * inputs from motors with {@link #setInputs(double, double)}, call {@link #update(double)} to * update the simulation, and set estimated encoder and gyro positions, as well as estimated * odometry pose. Teams can use {@link edu.wpi.first.wpilibj.smartdashboard.Field2d} to visualize * their robot on the Sim GUI's field. * *

Our state-space system is: * *

x = [[x, y, theta, vel_l, vel_r, dist_l, dist_r]]ᵀ in the field coordinate system (dist_* are * wheel distances.) * *

u = [[voltage_l, voltage_r]]ᵀ This is typically the control input of the last timestep from a * LTVDiffDriveController. * *

y = x */ public class DifferentialDrivetrainSim { private final DCMotor m_motor; private final double m_originalGearing; private final Matrix m_measurementStdDevs; private double m_currentGearing; private final double m_wheelRadius; private Matrix m_u; private Matrix m_x; private Matrix m_y; private final double m_rb; private final LinearSystem m_plant; /** * Creates a simulated differential drivetrain. * * @param driveMotor A {@link DCMotor} representing the left side of the drivetrain. * @param gearing The gearing ratio between motor and wheel, as output over input. This must be * the same ratio as the ratio used to identify or create the drivetrainPlant. * @param j The moment of inertia of the drivetrain about its center in kg-m². * @param mass The mass of the drivebase in kg. * @param wheelRadius The radius of the wheels on the drivetrain in meters. * @param trackwidth The robot's trackwidth, or distance between left and right wheels, in meters. * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading, * left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is * desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05 * m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting * point. */ public DifferentialDrivetrainSim( DCMotor driveMotor, double gearing, double j, double mass, double wheelRadius, double trackwidth, Matrix measurementStdDevs) { this( LinearSystemId.createDrivetrainVelocitySystem( driveMotor, mass, wheelRadius, trackwidth / 2.0, j, gearing), driveMotor, gearing, trackwidth, wheelRadius, measurementStdDevs); } /** * Creates a simulated differential drivetrain. * * @param plant The {@link LinearSystem} representing the robot's drivetrain. This system can be * created with {@link * edu.wpi.first.math.system.plant.LinearSystemId#createDrivetrainVelocitySystem(DCMotor, * double, double, double, double, double)} or {@link * edu.wpi.first.math.system.plant.LinearSystemId#identifyDrivetrainSystem(double, double, * double, double)}. * @param driveMotor A {@link DCMotor} representing the drivetrain. * @param gearing The gearingRatio ratio of the robot, as output over input. This must be the same * ratio as the ratio used to identify or create the drivetrainPlant. * @param trackwidth The distance between the two sides of the drivetrain in meters. Can be found * with SysId. * @param wheelRadius The radius of the wheels on the drivetrain, in meters. * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading, * left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is * desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05 * m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting * point. */ public DifferentialDrivetrainSim( LinearSystem plant, DCMotor driveMotor, double gearing, double trackwidth, double wheelRadius, Matrix measurementStdDevs) { this.m_plant = plant; this.m_rb = trackwidth / 2.0; this.m_motor = driveMotor; this.m_originalGearing = gearing; this.m_measurementStdDevs = measurementStdDevs; m_wheelRadius = wheelRadius; m_currentGearing = m_originalGearing; m_x = new Matrix<>(Nat.N7(), Nat.N1()); m_u = VecBuilder.fill(0, 0); m_y = new Matrix<>(Nat.N7(), Nat.N1()); } /** * Sets the applied voltage to the drivetrain. Note that positive voltage must make that side of * the drivetrain travel forward (+X). * * @param leftVoltageVolts The left voltage. * @param rightVoltageVolts The right voltage. */ public void setInputs(double leftVoltageVolts, double rightVoltageVolts) { m_u = clampInput(VecBuilder.fill(leftVoltageVolts, rightVoltageVolts)); } /** * Update the drivetrain states with the current time difference. * * @param dt the time difference in seconds */ public void update(double dt) { m_x = NumericalIntegration.rkdp(this::getDynamics, m_x, m_u, dt); m_y = m_x; if (m_measurementStdDevs != null) { m_y = m_y.plus(StateSpaceUtil.makeWhiteNoiseVector(m_measurementStdDevs)); } } /** Returns the full simulated state of the drivetrain. */ Matrix getState() { return m_x; } /** * Get one of the drivetrain states. * * @param state the state to get * @return the state */ double getState(State state) { return m_x.get(state.value, 0); } private double getOutput(State output) { return m_y.get(output.value, 0); } /** * Returns the direction the robot is pointing. * *

Note that this angle is counterclockwise-positive, while most gyros are clockwise positive. * * @return The direction the robot is pointing. */ public Rotation2d getHeading() { return new Rotation2d(getOutput(State.kHeading)); } /** * Returns the current pose. * * @return The current pose. */ public Pose2d getPose() { return new Pose2d(getOutput(State.kX), getOutput(State.kY), getHeading()); } /** * Get the right encoder position. * * @return The encoder position in meters. */ public double getRightPosition() { return getOutput(State.kRightPosition); } /** * Get the right encoder velocity. * * @return The encoder velocity in meters per second. */ public double getRightVelocity() { return getOutput(State.kRightVelocity); } /** * Get the left encoder position. * * @return The encoder position in meters. */ public double getLeftPosition() { return getOutput(State.kLeftPosition); } /** * Get the left encoder velocity. * * @return The encoder velocity in meters per second. */ public double getLeftVelocity() { return getOutput(State.kLeftVelocity); } /** * Get the current draw of the left side of the drivetrain. * * @return the drivetrain's left side current draw, in amps */ public double getLeftCurrentDraw() { return m_motor.getCurrent( getState(State.kLeftVelocity) * m_currentGearing / m_wheelRadius, m_u.get(0, 0)) * Math.signum(m_u.get(0, 0)); } /** * Get the current draw of the right side of the drivetrain. * * @return the drivetrain's right side current draw, in amps */ public double getRightCurrentDraw() { return m_motor.getCurrent( getState(State.kRightVelocity) * m_currentGearing / m_wheelRadius, m_u.get(1, 0)) * Math.signum(m_u.get(1, 0)); } /** * Get the current draw of the drivetrain. * * @return the current draw, in amps */ public double getCurrentDraw() { return getLeftCurrentDraw() + getRightCurrentDraw(); } /** * Get the drivetrain gearing. * * @return the gearing ration */ public double getCurrentGearing() { return m_currentGearing; } /** * Sets the gearing reduction on the drivetrain. This is commonly used for shifting drivetrains. * * @param newGearRatio The new gear ratio, as output over input. */ public void setCurrentGearing(double newGearRatio) { this.m_currentGearing = newGearRatio; } /** * Sets the system state. * * @param state The state. */ public void setState(Matrix state) { m_x = state; } /** * Sets the system pose. * * @param pose The pose. */ public void setPose(Pose2d pose) { m_x.set(State.kX.value, 0, pose.getX()); m_x.set(State.kY.value, 0, pose.getY()); m_x.set(State.kHeading.value, 0, pose.getRotation().getRadians()); m_x.set(State.kLeftPosition.value, 0, 0); m_x.set(State.kRightPosition.value, 0, 0); } /** * The differential drive dynamics function. * * @param x The state. * @param u The input. * @return The state derivative with respect to time. */ protected Matrix getDynamics(Matrix x, Matrix u) { // Because G can be factored out of B, we can divide by the old ratio and multiply // by the new ratio to get a new drivetrain model. var B = new Matrix<>(Nat.N4(), Nat.N2()); B.assignBlock(0, 0, m_plant.getB().times(this.m_currentGearing / this.m_originalGearing)); // Because G² can be factored out of A, we can divide by the old ratio squared and multiply // by the new ratio squared to get a new drivetrain model. var A = new Matrix<>(Nat.N4(), Nat.N4()); A.assignBlock( 0, 0, m_plant .getA() .times( (this.m_currentGearing * this.m_currentGearing) / (this.m_originalGearing * this.m_originalGearing))); A.assignBlock(2, 0, Matrix.eye(Nat.N2())); var v = (x.get(State.kLeftVelocity.value, 0) + x.get(State.kRightVelocity.value, 0)) / 2.0; var xdot = new Matrix<>(Nat.N7(), Nat.N1()); xdot.set(0, 0, v * Math.cos(x.get(State.kHeading.value, 0))); xdot.set(1, 0, v * Math.sin(x.get(State.kHeading.value, 0))); xdot.set( 2, 0, (x.get(State.kRightVelocity.value, 0) - x.get(State.kLeftVelocity.value, 0)) / (2.0 * m_rb)); xdot.assignBlock(3, 0, A.times(x.block(Nat.N4(), Nat.N1(), 3, 0)).plus(B.times(u))); return xdot; } /** * Clamp the input vector such that no element exceeds the battery voltage. If any does, the * relative magnitudes of the input will be maintained. * * @param u The input vector. * @return The normalized input. */ protected Matrix clampInput(Matrix u) { return StateSpaceUtil.desaturateInputVector(u, RobotController.getBatteryVoltage()); } /** Represents the different states of the drivetrain. */ enum State { kX(0), kY(1), kHeading(2), kLeftVelocity(3), kRightVelocity(4), kLeftPosition(5), kRightPosition(6); public final int value; State(int i) { this.value = i; } } /** * Represents a gearing option of the Toughbox mini. 12.75:1 -- 14:50 and 14:50 10.71:1 -- 14:50 * 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 */ public enum KitbotGearing { /** Gear ratio of 12.75:1. */ k12p75(12.75), /** Gear ratio of 10.71:1. */ k10p71(10.71), /** Gear ratio of 8.45:1. */ k8p45(8.45), /** Gear ratio of 7.31:1. */ k7p31(7.31), /** Gear ratio of 5.95:1. */ k5p95(5.95); /** KitbotGearing value. */ public final double value; KitbotGearing(double i) { this.value = i; } } /** Represents common motor layouts of the kit drivetrain. */ public enum KitbotMotor { /** One CIM motor per drive side. */ kSingleCIMPerSide(DCMotor.getCIM(1)), /** Two CIM motors per drive side. */ kDualCIMPerSide(DCMotor.getCIM(2)), /** One Mini CIM motor per drive side. */ kSingleMiniCIMPerSide(DCMotor.getMiniCIM(1)), /** Two Mini CIM motors per drive side. */ kDualMiniCIMPerSide(DCMotor.getMiniCIM(2)), /** One Falcon 500 motor per drive side. */ kSingleFalcon500PerSide(DCMotor.getFalcon500(1)), /** Two Falcon 500 motors per drive side. */ kDoubleFalcon500PerSide(DCMotor.getFalcon500(2)), /** One NEO motor per drive side. */ kSingleNEOPerSide(DCMotor.getNEO(1)), /** Two NEO motors per drive side. */ kDoubleNEOPerSide(DCMotor.getNEO(2)); /** KitbotMotor value. */ public final DCMotor value; KitbotMotor(DCMotor i) { this.value = i; } } /** Represents common wheel sizes of the kit drivetrain. */ public enum KitbotWheelSize { /** Six inch diameter wheels. */ kSixInch(Units.inchesToMeters(6)), /** Eight inch diameter wheels. */ kEightInch(Units.inchesToMeters(8)), /** Ten inch diameter wheels. */ kTenInch(Units.inchesToMeters(10)); /** KitbotWheelSize value. */ public final double value; KitbotWheelSize(double i) { this.value = i; } } /** * Create a sim for the standard FRC kitbot. * * @param motor The motors installed in the bot. * @param gearing The gearing reduction used. * @param wheelSize The wheel size. * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading, * left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is * desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05 * m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting * point. * @return A sim for the standard FRC kitbot. */ public static DifferentialDrivetrainSim createKitbotSim( KitbotMotor motor, KitbotGearing gearing, KitbotWheelSize wheelSize, Matrix measurementStdDevs) { // MOI estimation -- note that I = mr² for point masses var batteryMoi = 12.5 / 2.2 * Math.pow(Units.inchesToMeters(10), 2); var gearboxMoi = (2.8 /* CIM motor */ * 2 / 2.2 + 2.0 /* Toughbox Mini- ish */) * Math.pow(Units.inchesToMeters(26.0 / 2.0), 2); return createKitbotSim(motor, gearing, wheelSize, batteryMoi + gearboxMoi, measurementStdDevs); } /** * Create a sim for the standard FRC kitbot. * * @param motor The motors installed in the bot. * @param gearing The gearing reduction used. * @param wheelSize The wheel size. * @param j The moment of inertia of the drivebase in kg-m². This can be calculated using SysId. * @param measurementStdDevs Standard deviations for measurements, in the form [x, y, heading, * left velocity, right velocity, left distance, right distance]ᵀ. Can be null if no noise is * desired. Gyro standard deviations of 0.0001 radians, velocity standard deviations of 0.05 * m/s, and position measurement standard deviations of 0.005 meters are a reasonable starting * point. * @return A sim for the standard FRC kitbot. */ public static DifferentialDrivetrainSim createKitbotSim( KitbotMotor motor, KitbotGearing gearing, KitbotWheelSize wheelSize, double j, Matrix measurementStdDevs) { return new DifferentialDrivetrainSim( motor.value, gearing.value, j, Units.lbsToKilograms(60), wheelSize.value / 2.0, Units.inchesToMeters(26), measurementStdDevs); } }