Description
If you discretize a ChassisSpeeds
, convert the speeds to module states, and then desaturate the module states, translational drift will be introduced to the resulting robot twist. This is how most users currently implement swerve kinematics.
Note
Removing the SwerveDriveKinematics.desaturateWheelSpeeds(states, maxSpeed)
does not fix the problem, it simply creates module states that are not achievable, which introduces other issues (including translational drift).
A Java example is shown below:
static ChassisSpeeds undiscretize(ChassisSpeeds speeds, double dtSeconds) {
// Construct the resulting twist for the timestep
var twist = new Twist2d(
speeds.vxMetersPerSecond * dtSeconds,
speeds.vyMetersPerSecond * dtSeconds,
speeds.omegaRadiansPerSecond * dtSeconds
);
// Find the desired pose after a timestep, relative to the current pose.
var desiredDeltaPose = Pose2d.kZero.exp(twist);
// Turn the chassis translation/rotation deltas into average velocities
return new ChassisSpeeds(
desiredDeltaPose.getX() / dtSeconds,
desiredDeltaPose.getY() / dtSeconds,
desiredDeltaPose.getRotation().getRadians() / dtSeconds
);
}
static void testDiscretizeAndDesaturate() {
var kinematics = new SwerveDriveKinematics(
new Translation2d(0.27, 0.27),
new Translation2d(0.27, -0.27),
new Translation2d(-0.27, 0.27),
new Translation2d(-0.27, -0.27)
);
double dt = 0.02;
double maxSpeed = 4.0;
// impossible chassis speeds, 0 m/s on Y
var origSpeeds = new ChassisSpeeds(4, 0, Math.PI * 2.0);
// discretize our speeds
var speeds = ChassisSpeeds.discretize(origSpeeds, dt);
// determine desaturated module states
var states = kinematics.toSwerveModuleStates(speeds);
SwerveDriveKinematics.desaturateWheelSpeeds(states, maxSpeed);
// pull out resulting chassis speeds
var desatSpeeds = kinematics.toChassisSpeeds(states);
// print effective chassis Y speeds after undoing discretization
System.out.println(undiscretize(speeds, dt).vyMetersPerSecond); // -8.67E-17 m/s [GOOD]
System.out.println(undiscretize(desatSpeeds, dt).vyMetersPerSecond); // -0.056 m/s [BAD]
}
If you instead do desaturate -> discretize -> desaturate, the error is reduced significantly (50x in my testing), but is still less than optimal:
static void testDesaturateDiscretizeDesaturate() {
var kinematics = new SwerveDriveKinematics(
new Translation2d(0.27, 0.27),
new Translation2d(0.27, -0.27),
new Translation2d(-0.27, 0.27),
new Translation2d(-0.27, -0.27)
);
double dt = 0.02;
double maxSpeed = 4.0;
// impossible chassis speeds, 0 m/s on Y
var origSpeeds = new ChassisSpeeds(4, 0, Math.PI * 2.0);
// first desaturate module states with the non-discretized speeds
var tmpStates = kinematics.toSwerveModuleStates(origSpeeds);
SwerveDriveKinematics.desaturateWheelSpeeds(tmpStates, maxSpeed);
// convert back to chassis speeds
var speeds = kinematics.toChassisSpeeds(tmpStates);
// discretize our speeds
speeds = ChassisSpeeds.discretize(speeds, dt);
// determine desaturated module states for the discretized speeds
var states = kinematics.toSwerveModuleStates(speeds);
SwerveDriveKinematics.desaturateWheelSpeeds(states, maxSpeed);
// pull out resulting chassis speeds
var desatSpeeds = kinematics.toChassisSpeeds(states);
// print effective chassis Y speeds after undoing discretization
System.out.println(undiscretize(speeds, dt).vyMetersPerSecond); // 3.53E-15 m/s [GOOD]
System.out.println(undiscretize(desatSpeeds, dt).vyMetersPerSecond); // -9.1E-4 m/s [OK]
}
I'm not sure what direction WPILib wants to take to address this issue, but at a minimum it should be documented.