Files
YAGSL/swervelib/math/SwervePoseEstimator2.java

460 lines
18 KiB
Java
Raw Normal View History

2023-04-08 13:14:18 -05:00
package swervelib.math;
import edu.wpi.first.math.MathUtil;
import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.Nat;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.geometry.Twist2d;
import edu.wpi.first.math.interpolation.Interpolatable;
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
import edu.wpi.first.math.kinematics.SwerveDriveKinematics;
import edu.wpi.first.math.kinematics.SwerveModulePosition;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N3;
import edu.wpi.first.util.WPIUtilJNI;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
/**
* Clone of {@link SwerveDrivePoseEstimator} which takes into account gyroscope pitch and roll to achieve a more
* accurate estimation, originally made by Team 1466.
*/
public class SwervePoseEstimator2 // extends SwerveDrivePoseEstimator
{
/**
* Swerve drive kinematics.
*/
private final SwerveDriveKinematics m_kinematics;
/**
* Enhanced swerve drive odometry.
*/
private final SwerveDriveOdometry2 m_odometry;
/**
* Matrix quotient.
*/
private final Matrix<N3, N1> m_q = new Matrix<>(Nat.N3(), Nat.N1());
/**
* Number of swerve modules.
*/
private final int m_numModules;
/**
* Interpolation buffer.
*/
private final TimeInterpolatableBuffer<InterpolationRecord> m_poseBuffer = TimeInterpolatableBuffer.createBuffer(1.5);
/**
* Vision standard deviations.
*/
private final Matrix<N3, N3> m_visionK = new Matrix<>(Nat.N3(), Nat.N3());
/**
* Constructs a SwerveDrivePoseEstimator with default standard deviations for the model and vision measurements.
*
* <p>The default standard deviations of the model states are 0.1 meters for x, 0.1 meters for y,
* and 0.1 radians for heading. The default standard deviations of the vision measurements are 0.9 meters for x, 0.9
* meters for y, and 0.9 radians for heading.
*
* @param kinematics A correctly-configured kinematics object for your drivetrain.
* @param gyroAngle The current gyro angle.
* @param modulePositions The current distance measurements and rotations of the swerve modules.
* @param initialPoseMeters The starting pose estimate.
*/
public SwervePoseEstimator2(
SwerveDriveKinematics kinematics,
Rotation2d gyroAngle,
SwerveModulePosition[] modulePositions,
Pose2d initialPoseMeters)
{
this(
kinematics,
gyroAngle,
modulePositions,
initialPoseMeters,
VecBuilder.fill(0.1, 0.1, 0.1),
VecBuilder.fill(0.9, 0.9, 0.9));
}
/**
* Constructs a SwerveDrivePoseEstimator.
*
* @param kinematics A correctly-configured kinematics object for your drivetrain.
* @param gyroAngle The current gyro angle.
* @param modulePositions The current distance and rotation measurements of the swerve modules.
* @param initialPoseMeters The starting pose estimate.
* @param stateStdDevs Standard deviations of the pose estimate (x position in meters, y position in
* meters, and heading in radians). Increase these numbers to trust your state
* estimate less.
* @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position in meters, y
* position in meters, and heading in radians). Increase these numbers to trust the
* vision pose measurement less.
*/
public SwervePoseEstimator2(
SwerveDriveKinematics kinematics,
Rotation2d gyroAngle,
SwerveModulePosition[] modulePositions,
Pose2d initialPoseMeters,
Matrix<N3, N1> stateStdDevs,
Matrix<N3, N1> visionMeasurementStdDevs)
{
// super(kinematics, gyroAngle, modulePositions, initialPoseMeters);
m_kinematics = kinematics;
m_odometry =
new SwerveDriveOdometry2(kinematics, gyroAngle, modulePositions, initialPoseMeters);
for (int i = 0; i < 3; ++i)
{
m_q.set(i, 0, stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0));
}
m_numModules = modulePositions.length;
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
}
/**
* Sets the pose estimator's trust of global measurements. This might be used to change trust in vision measurements
* after the autonomous period, or to change trust as distance to a vision target increases.
*
* @param visionMeasurementStdDevs Standard deviations of the vision measurements. Increase these numbers to trust
* global measurements from vision less. This matrix is in the form [x, y, theta],
* with units in meters and radians.
*/
public void setVisionMeasurementStdDevs(Matrix<N3, N1> visionMeasurementStdDevs)
{
// super.setVisionMeasurementStdDevs(visionMeasurementStdDevs);
var r = new double[3];
for (int i = 0; i < 3; ++i)
{
r[i] = visionMeasurementStdDevs.get(i, 0) * visionMeasurementStdDevs.get(i, 0);
}
// Solve for closed form Kalman gain for continuous Kalman filter with A = 0
// and C = I. See wpimath/algorithms.md.
for (int row = 0; row < 3; ++row)
{
if (m_q.get(row, 0) == 0.0)
{
m_visionK.set(row, row, 0.0);
} else
{
m_visionK.set(
row, row, m_q.get(row, 0) / (m_q.get(row, 0) + Math.sqrt(m_q.get(row, 0) * r[row])));
}
}
}
/**
* Resets the robot's position on the field.
*
* <p>The gyroscope angle does not need to be reset in the user's robot code. The library
* automatically takes care of offsetting the gyro angle.
*
* @param gyroAngle The angle reported by the gyroscope.
* @param modulePositions The current distance measurements and rotations of the swerve modules.
* @param poseMeters The position on the field that your robot is at.
*/
public void resetPosition(
Rotation2d gyroAngle, SwerveModulePosition[] modulePositions, Pose2d poseMeters)
{
// super.resetPosition(gyroAngle, modulePositions, poseMeters);
// Reset state estimate and error covariance
m_odometry.resetPosition(gyroAngle, modulePositions, poseMeters);
m_poseBuffer.clear();
}
/**
* Gets the estimated robot pose.
*
* @return The estimated robot pose in meters.
*/
public Pose2d getEstimatedPosition()
{
return m_odometry.getPoseMeters();
}
/**
* Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate while still accounting
* for measurement noise.
*
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* SwerveDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the current pose estimate.
*
* @param visionRobotPoseMeters The pose of the robot as measured by the vision camera.
* @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you don't use your
* own time source by calling
* {@link SwerveDrivePoseEstimator#updateWithTime(double, Rotation2d,
* SwerveModulePosition[])} then you must use a timestamp with an epoch since FPGA
* startup (i.e., the epoch of this timestamp is the same epoch as
* {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}.) This means that you should
* use {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} as your time source or sync
* the epochs.
*/
public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds)
{
// super.addVisionMeasurement(visionRobotPoseMeters, timestampSeconds);
// Step 1: Get the pose odometry measured at the moment the vision measurement was made.
var sample = m_poseBuffer.getSample(timestampSeconds);
if (sample.isEmpty())
{
return;
}
// Step 2: Measure the twist between the odometry pose and the vision pose.
var twist = sample.get().poseMeters.log(visionRobotPoseMeters);
// Step 3: We should not trust the twist entirely, so instead we scale this twist by a Kalman
// gain matrix representing how much we trust vision measurements compared to our current pose.
var k_times_twist = m_visionK.times(VecBuilder.fill(twist.dx, twist.dy, twist.dtheta));
// Step 4: Convert back to Twist2d.
var scaledTwist =
new Twist2d(k_times_twist.get(0, 0), k_times_twist.get(1, 0), k_times_twist.get(2, 0));
// Step 5: Reset Odometry to state at sample with vision adjustment.
m_odometry.resetPosition(
sample.get().gyroAngle,
sample.get().modulePositions,
sample.get().poseMeters.exp(scaledTwist));
// Step 6: Replay odometry inputs between sample time and latest recorded sample to update the
// pose buffer and correct odometry.
for (Map.Entry<Double, InterpolationRecord> entry :
m_poseBuffer.getInternalBuffer().tailMap(timestampSeconds).entrySet())
{
updateWithTime(
entry.getKey(),
entry.getValue().gyroAngle,
entry.getValue().gyroPitch,
entry.getValue().gyroRoll,
entry.getValue().modulePositions);
}
}
/**
* Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate while still accounting
* for measurement noise.
*
* <p>This method can be called as infrequently as you want, as long as you are calling {@link
* SwerveDrivePoseEstimator#update} every loop.
*
* <p>To promote stability of the pose estimate and make it robust to bad vision data, we
* recommend only adding vision measurements that are already within one meter or so of the current pose estimate.
*
* <p>Note that the vision measurement standard deviations passed into this method will continue
* to apply to future measurements until a subsequent call to
* {@link SwerveDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method.
*
* @param visionRobotPoseMeters The pose of the robot as measured by the vision camera.
* @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you don't use your
* own time source by calling
* {@link SwerveDrivePoseEstimator#updateWithTime(double, Rotation2d,
* SwerveModulePosition[])}, then you must use a timestamp with an epoch since FPGA
* startup (i.e., the epoch of this timestamp is the same epoch as
* {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}). This means that you should
* use {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} as your time source in
* this case.
* @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position in meters, y
* position in meters, and heading in radians). Increase these numbers to trust the
* vision pose measurement less.
*/
public void addVisionMeasurement(
Pose2d visionRobotPoseMeters,
double timestampSeconds,
Matrix<N3, N1> visionMeasurementStdDevs)
{
setVisionMeasurementStdDevs(visionMeasurementStdDevs);
addVisionMeasurement(visionRobotPoseMeters, timestampSeconds);
}
/**
* Updates the pose estimator with wheel encoder and gyro information. This should be called every loop.
*
* @param gyroAngle The current gyro angle.
* @param gyroPitch The current gyro pitch.
* @param gyroRoll The current gyro roll.
* @param modulePositions The current distance measurements and rotations of the swerve modules.
* @return The estimated pose of the robot in meters.
*/
public Pose2d update(
Rotation2d gyroAngle,
Rotation2d gyroPitch,
Rotation2d gyroRoll,
SwerveModulePosition[] modulePositions)
{
return updateWithTime(
WPIUtilJNI.now() * 1.0e-6, gyroAngle, gyroPitch, gyroRoll, modulePositions);
}
/**
* Updates the pose estimator with wheel encoder and gyro information. This should be called every loop.
*
* @param currentTimeSeconds Time at which this method was called, in seconds.
* @param gyroAngle The current gyro angle.
* @param gyroPitch The current gyro pitch.
* @param gyroRoll The current gyro roll.
* @param modulePositions The current distance measurements and rotations of the swerve modules.
* @return The estimated pose of the robot in meters.
*/
public Pose2d updateWithTime(
double currentTimeSeconds,
Rotation2d gyroAngle,
Rotation2d gyroPitch,
Rotation2d gyroRoll,
SwerveModulePosition[] modulePositions)
{
// super.updateWithTime(currentTimeSeconds, gyroAngle, modulePositions);
if (modulePositions.length != m_numModules)
{
throw new IllegalArgumentException(
"Number of modules is not consistent with number of wheel locations provided in "
+ "constructor");
}
var internalModulePositions = new SwerveModulePosition[m_numModules];
for (int i = 0; i < m_numModules; i++)
{
internalModulePositions[i] =
new SwerveModulePosition(modulePositions[i].distanceMeters, modulePositions[i].angle);
}
m_odometry.update(gyroAngle, gyroPitch, gyroRoll, internalModulePositions);
m_poseBuffer.addSample(
currentTimeSeconds,
new InterpolationRecord(
getEstimatedPosition(), gyroAngle, gyroPitch, gyroRoll, internalModulePositions));
return getEstimatedPosition();
}
/**
* Represents an odometry record. The record contains the inputs provided as well as the pose that was observed based
* on these inputs, as well as the previous record and its inputs.
*/
private class InterpolationRecord implements Interpolatable<InterpolationRecord>
{
// The pose observed given the current sensor inputs and the previous pose.
private final Pose2d poseMeters;
// The current gyro angle.
private final Rotation2d gyroAngle;
// The current gyro pitch.
private final Rotation2d gyroPitch;
// The current gyro roll.
private final Rotation2d gyroRoll;
// The distances and rotations measured at each module.
private final SwerveModulePosition[] modulePositions;
/**
* Constructs an Interpolation Record with the specified parameters.
*
* @param poseMeters The pose observed given the current sensor inputs and the previous pose.
* @param gyro The current gyro angle.
* @param gyroPitch The current gyro pitch.
* @param gyroRoll The current gyro roll.
* @param modulePositions The distances and rotations measured at each wheel.
*/
private InterpolationRecord(
Pose2d poseMeters,
Rotation2d gyro,
Rotation2d gyroPitch,
Rotation2d gyroRoll,
SwerveModulePosition[] modulePositions)
{
this.poseMeters = poseMeters;
this.gyroAngle = gyro;
this.gyroPitch = gyroPitch;
this.gyroRoll = gyroRoll;
this.modulePositions = modulePositions;
}
/**
* Return the interpolated record. This object is assumed to be the starting position, or lower bound.
*
* @param endValue The upper bound, or end.
* @param t How far between the lower and upper bound we are. This should be bounded in [0, 1].
* @return The interpolated value.
*/
@Override
public InterpolationRecord interpolate(InterpolationRecord endValue, double t)
{
if (t < 0)
{
return this;
} else if (t >= 1)
{
return endValue;
} else
{
// Find the new wheel distances.
var modulePositions = new SwerveModulePosition[m_numModules];
// Find the distance travelled between this measurement and the interpolated measurement.
var moduleDeltas = new SwerveModulePosition[m_numModules];
for (int i = 0; i < m_numModules; i++)
{
double ds =
MathUtil.interpolate(
this.modulePositions[i].distanceMeters,
endValue.modulePositions[i].distanceMeters,
t);
Rotation2d theta =
this.modulePositions[i].angle.interpolate(endValue.modulePositions[i].angle, t);
modulePositions[i] = new SwerveModulePosition(ds, theta);
moduleDeltas[i] =
new SwerveModulePosition(ds - this.modulePositions[i].distanceMeters, theta);
}
// Find the new gyro angle.
var gyro_lerp = gyroAngle.interpolate(endValue.gyroAngle, t);
var gyroPitch_lerp = gyroPitch.interpolate(endValue.gyroPitch, t);
var gyroRoll_lerp = gyroRoll.interpolate(endValue.gyroRoll, t);
// Create a twist to represent this change based on the interpolated sensor inputs.
Twist2d twist = m_kinematics.toTwist2d(moduleDeltas);
twist.dtheta = gyro_lerp.minus(gyroAngle).getRadians();
return new InterpolationRecord(
poseMeters.exp(twist), gyro_lerp, gyroPitch_lerp, gyroRoll_lerp, modulePositions);
}
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (!(obj instanceof InterpolationRecord))
{
return false;
}
InterpolationRecord record = (InterpolationRecord) obj;
return Objects.equals(gyroAngle, record.gyroAngle)
&& Arrays.equals(modulePositions, record.modulePositions)
&& Objects.equals(poseMeters, record.poseMeters);
}
@Override
public int hashCode()
{
return Objects.hash(gyroAngle, Arrays.hashCode(modulePositions), poseMeters);
}
}
}