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 org.wpilib.math.estimator;
006
007import java.util.NavigableMap;
008import java.util.Optional;
009import java.util.TreeMap;
010import org.wpilib.math.geometry.Pose2d;
011import org.wpilib.math.geometry.Pose3d;
012import org.wpilib.math.geometry.Rotation2d;
013import org.wpilib.math.geometry.Rotation3d;
014import org.wpilib.math.geometry.Transform3d;
015import org.wpilib.math.geometry.Translation2d;
016import org.wpilib.math.geometry.Translation3d;
017import org.wpilib.math.interpolation.TimeInterpolatableBuffer;
018import org.wpilib.math.kinematics.Kinematics;
019import org.wpilib.math.kinematics.Odometry3d;
020import org.wpilib.math.linalg.Matrix;
021import org.wpilib.math.numbers.N1;
022import org.wpilib.math.numbers.N4;
023import org.wpilib.math.util.MathSharedStore;
024
025/**
026 * This class wraps {@link Odometry3d} to fuse latency-compensated vision measurements with encoder
027 * measurements. Robot code should not use this directly- Instead, use the particular type for your
028 * drivetrain (e.g., {@link DifferentialDrivePoseEstimator3d}). It is intended to be a drop-in
029 * replacement for {@link Odometry3d}; in fact, if you never call {@link
030 * PoseEstimator3d#addVisionMeasurement} and only call {@link PoseEstimator3d#update} then this will
031 * behave exactly the same as Odometry3d. It is also intended to be an easy replacement for {@link
032 * PoseEstimator}, only requiring the addition of a standard deviation for Z and appropriate
033 * conversions between 2D and 3D versions of geometry classes. (See {@link Pose3d#Pose3d(Pose2d)},
034 * {@link Rotation3d#Rotation3d(Rotation2d)}, {@link Translation3d#Translation3d(Translation2d)},
035 * and {@link Pose3d#toPose2d()}.)
036 *
037 * <p>{@link PoseEstimator3d#update} should be called every robot loop.
038 *
039 * <p>{@link PoseEstimator3d#addVisionMeasurement} can be called as infrequently as you want; if you
040 * never call it then this class will behave exactly like regular encoder odometry.
041 *
042 * @param <T> Wheel positions type.
043 */
044public class PoseEstimator3d<T> {
045  private final Odometry3d<T> m_odometry;
046
047  // Diagonal of process noise covariance matrix Q
048  private final double[] m_q = new double[] {0.0, 0.0, 0.0, 0.0};
049
050  // Diagonal of Kalman gain matrix K
051  private final double[] m_vision_k = new double[] {0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
052
053  private static final double kBufferDuration = 1.5;
054  // Maps timestamps to odometry-only pose estimates
055  private final TimeInterpolatableBuffer<Pose3d> m_odometryPoseBuffer =
056      TimeInterpolatableBuffer.createBuffer(kBufferDuration);
057  // Maps timestamps to vision updates
058  // Always contains one entry before the oldest entry in m_odometryPoseBuffer, unless there have
059  // been no vision measurements after the last reset. May contain one entry while
060  // m_odometryPoseBuffer is empty to correct for translation/rotation after a call to
061  // ResetRotation/ResetTranslation.
062  private final NavigableMap<Double, VisionUpdate> m_visionUpdates = new TreeMap<>();
063
064  private Pose3d m_poseEstimate;
065
066  /**
067   * Constructs a PoseEstimator3d.
068   *
069   * @param kinematics A correctly-configured kinematics object for your drivetrain.
070   * @param odometry A correctly-configured odometry object for your drivetrain.
071   * @param stateStdDevs Standard deviations of the pose estimate (x position in meters, y position
072   *     in meters, z position in meters, and angle in radians). Increase these numbers to trust
073   *     your state estimate less.
074   * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position
075   *     in meters, y position in meters, z position in meters, and angle in radians). Increase
076   *     these numbers to trust the vision pose measurement less.
077   */
078  public PoseEstimator3d(
079      Kinematics<T, ?, ?> kinematics,
080      Odometry3d<T> odometry,
081      Matrix<N4, N1> stateStdDevs,
082      Matrix<N4, N1> visionMeasurementStdDevs) {
083    m_odometry = odometry;
084
085    m_poseEstimate = m_odometry.getPose();
086
087    for (int i = 0; i < 4; ++i) {
088      m_q[i] = stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0);
089    }
090    setVisionMeasurementStdDevs(visionMeasurementStdDevs);
091    MathSharedStore.getMathShared().reportUsage("PoseEstimator3d", "");
092  }
093
094  /**
095   * Sets the pose estimator's trust of global measurements. This might be used to change trust in
096   * vision measurements after the autonomous period, or to change trust as distance to a vision
097   * target increases.
098   *
099   * @param visionMeasurementStdDevs Standard deviations of the vision measurements. Increase these
100   *     numbers to trust global measurements from vision less. This matrix is in the form [x, y, z,
101   *     theta]ᵀ, with units in meters and radians.
102   */
103  public final void setVisionMeasurementStdDevs(Matrix<N4, N1> visionMeasurementStdDevs) {
104    // Diagonal of measurement covariance matrix R
105    var r = new double[4];
106    for (int i = 0; i < 4; ++i) {
107      r[i] = visionMeasurementStdDevs.get(i, 0) * visionMeasurementStdDevs.get(i, 0);
108    }
109
110    // Solve for closed form Kalman gain for continuous Kalman filter with A = 0
111    // and C = I. See wpimath/algorithms.md.
112    for (int row = 0; row < 4; ++row) {
113      if (m_q[row] == 0.0) {
114        m_vision_k[row] = 0.0;
115      } else {
116        m_vision_k[row] = m_q[row] / (m_q[row] + Math.sqrt(m_q[row] * r[row]));
117      }
118    }
119    // Fill in the gains for the other components of the rotation vector
120    double angle_gain = m_vision_k[3];
121    m_vision_k[4] = angle_gain;
122    m_vision_k[5] = angle_gain;
123  }
124
125  /**
126   * Resets the robot's position on the field.
127   *
128   * <p>The gyroscope angle does not need to be reset here on the user's robot code. The library
129   * automatically takes care of offsetting the gyro angle.
130   *
131   * @param gyroAngle The angle reported by the gyroscope.
132   * @param wheelPositions The current encoder readings.
133   * @param pose The position on the field that your robot is at.
134   */
135  public void resetPosition(Rotation3d gyroAngle, T wheelPositions, Pose3d pose) {
136    // Reset state estimate and error covariance
137    m_odometry.resetPosition(gyroAngle, wheelPositions, pose);
138    m_odometryPoseBuffer.clear();
139    m_visionUpdates.clear();
140    m_poseEstimate = m_odometry.getPose();
141  }
142
143  /**
144   * Resets the robot's pose.
145   *
146   * @param pose The pose to reset to.
147   */
148  public void resetPose(Pose3d pose) {
149    m_odometry.resetPose(pose);
150    m_odometryPoseBuffer.clear();
151    m_visionUpdates.clear();
152    m_poseEstimate = m_odometry.getPose();
153  }
154
155  /**
156   * Resets the robot's translation.
157   *
158   * @param translation The pose to translation to.
159   */
160  public void resetTranslation(Translation3d translation) {
161    m_odometry.resetTranslation(translation);
162
163    final var latestVisionUpdate = m_visionUpdates.lastEntry();
164    m_odometryPoseBuffer.clear();
165    m_visionUpdates.clear();
166
167    if (latestVisionUpdate != null) {
168      // apply vision compensation to the pose rotation
169      final var visionUpdate =
170          new VisionUpdate(
171              new Pose3d(translation, latestVisionUpdate.getValue().visionPose.getRotation()),
172              new Pose3d(translation, latestVisionUpdate.getValue().odometryPose.getRotation()));
173      m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate);
174      m_poseEstimate = visionUpdate.compensate(m_odometry.getPose());
175    } else {
176      m_poseEstimate = m_odometry.getPose();
177    }
178  }
179
180  /**
181   * Resets the robot's rotation.
182   *
183   * @param rotation The rotation to reset to.
184   */
185  public void resetRotation(Rotation3d rotation) {
186    m_odometry.resetRotation(rotation);
187
188    final var latestVisionUpdate = m_visionUpdates.lastEntry();
189    m_odometryPoseBuffer.clear();
190    m_visionUpdates.clear();
191
192    if (latestVisionUpdate != null) {
193      // apply vision compensation to the pose translation
194      final var visionUpdate =
195          new VisionUpdate(
196              new Pose3d(latestVisionUpdate.getValue().visionPose.getTranslation(), rotation),
197              new Pose3d(latestVisionUpdate.getValue().odometryPose.getTranslation(), rotation));
198      m_visionUpdates.put(latestVisionUpdate.getKey(), visionUpdate);
199      m_poseEstimate = visionUpdate.compensate(m_odometry.getPose());
200    } else {
201      m_poseEstimate = m_odometry.getPose();
202    }
203  }
204
205  /**
206   * Gets the estimated robot pose.
207   *
208   * @return The estimated robot pose in meters.
209   */
210  public Pose3d getEstimatedPosition() {
211    return m_poseEstimate;
212  }
213
214  /**
215   * Return the pose at a given timestamp, if the buffer is not empty.
216   *
217   * @param timestamp The pose's timestamp in seconds.
218   * @return The pose at the given timestamp (or Optional.empty() if the buffer is empty).
219   */
220  public Optional<Pose3d> sampleAt(double timestamp) {
221    // Step 0: If there are no odometry updates to sample, skip.
222    if (m_odometryPoseBuffer.getInternalBuffer().isEmpty()) {
223      return Optional.empty();
224    }
225
226    // Step 1: Make sure timestamp matches the sample from the odometry pose buffer. (When sampling,
227    // the buffer will always use a timestamp between the first and last timestamps)
228    double oldestOdometryTimestamp = m_odometryPoseBuffer.getInternalBuffer().firstKey();
229    double newestOdometryTimestamp = m_odometryPoseBuffer.getInternalBuffer().lastKey();
230    timestamp = Math.clamp(timestamp, oldestOdometryTimestamp, newestOdometryTimestamp);
231
232    // Step 2: If there are no applicable vision updates, use the odometry-only information.
233    if (m_visionUpdates.isEmpty() || timestamp < m_visionUpdates.firstKey()) {
234      return m_odometryPoseBuffer.getSample(timestamp);
235    }
236
237    // Step 3: Get the latest vision update from before or at the timestamp to sample at.
238    double floorTimestamp = m_visionUpdates.floorKey(timestamp);
239    var visionUpdate = m_visionUpdates.get(floorTimestamp);
240
241    // Step 4: Get the pose measured by odometry at the time of the sample.
242    var odometryEstimate = m_odometryPoseBuffer.getSample(timestamp);
243
244    // Step 5: Apply the vision compensation to the odometry pose.
245    return odometryEstimate.map(odometryPose -> visionUpdate.compensate(odometryPose));
246  }
247
248  /** Removes stale vision updates that won't affect sampling. */
249  private void cleanUpVisionUpdates() {
250    // Step 0: If there are no odometry samples, skip.
251    if (m_odometryPoseBuffer.getInternalBuffer().isEmpty()) {
252      return;
253    }
254
255    // Step 1: Find the oldest timestamp that needs a vision update.
256    double oldestOdometryTimestamp = m_odometryPoseBuffer.getInternalBuffer().firstKey();
257
258    // Step 2: If there are no vision updates before that timestamp, skip.
259    if (m_visionUpdates.isEmpty() || oldestOdometryTimestamp < m_visionUpdates.firstKey()) {
260      return;
261    }
262
263    // Step 3: Find the newest vision update timestamp before or at the oldest timestamp.
264    double newestNeededVisionUpdateTimestamp = m_visionUpdates.floorKey(oldestOdometryTimestamp);
265
266    // Step 4: Remove all entries strictly before the newest timestamp we need.
267    m_visionUpdates.headMap(newestNeededVisionUpdateTimestamp, false).clear();
268  }
269
270  /**
271   * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate
272   * while still accounting for measurement noise.
273   *
274   * <p>This method can be called as infrequently as you want, as long as you are calling {@link
275   * PoseEstimator3d#update} every loop.
276   *
277   * <p>To promote stability of the pose estimate and make it robust to bad vision data, we
278   * recommend only adding vision measurements that are already within one meter or so of the
279   * current pose estimate.
280   *
281   * @param visionRobotPose The pose of the robot as measured by the vision camera.
282   * @param timestamp The timestamp of the vision measurement in seconds. Note that if you don't use
283   *     your own time source by calling {@link
284   *     PoseEstimator3d#updateWithTime(double,Rotation3d,Object)} then you must use a timestamp
285   *     with the same epoch as {@link org.wpilib.system.Timer#getMonotonicTimestamp()}.) This means
286   *     that you should use {@link org.wpilib.system.Timer#getMonotonicTimestamp()} as your time
287   *     source or sync the epochs.
288   */
289  public void addVisionMeasurement(Pose3d visionRobotPose, double timestamp) {
290    // Step 0: If this measurement is old enough to be outside the pose buffer's timespan, skip.
291    if (m_odometryPoseBuffer.getInternalBuffer().isEmpty()
292        || m_odometryPoseBuffer.getInternalBuffer().lastKey() - kBufferDuration > timestamp) {
293      return;
294    }
295
296    // Step 1: Clean up any old entries
297    cleanUpVisionUpdates();
298
299    // Step 2: Get the pose measured by odometry at the moment the vision measurement was made.
300    var odometrySample = m_odometryPoseBuffer.getSample(timestamp);
301
302    if (odometrySample.isEmpty()) {
303      return;
304    }
305
306    // Step 3: Get the vision-compensated pose estimate at the moment the vision measurement was
307    // made.
308    var visionSample = sampleAt(timestamp);
309
310    if (visionSample.isEmpty()) {
311      return;
312    }
313
314    // Step 4: Measure the transform between the old pose estimate and the vision pose.
315    var transform = visionRobotPose.minus(visionSample.get());
316
317    // Step 5: We should not trust the transform entirely, so instead we scale this transform by a
318    // Kalman gain matrix representing how much we trust vision measurements compared to our current
319    // pose. Then we convert the result back to a Transform3d.
320    var scaledTransform =
321        new Transform3d(
322            m_vision_k[0] * transform.getX(),
323            m_vision_k[1] * transform.getY(),
324            m_vision_k[2] * transform.getZ(),
325            new Rotation3d(
326                m_vision_k[3] * transform.getRotation().getX(),
327                m_vision_k[4] * transform.getRotation().getY(),
328                m_vision_k[5] * transform.getRotation().getZ()));
329
330    // Step 6: Calculate and record the vision update.
331    var visionUpdate =
332        new VisionUpdate(visionSample.get().plus(scaledTransform), odometrySample.get());
333    m_visionUpdates.put(timestamp, visionUpdate);
334
335    // Step 7: Remove later vision measurements. (Matches previous behavior)
336    m_visionUpdates.tailMap(timestamp, false).entrySet().clear();
337
338    // Step 8: Update latest pose estimate. Since we cleared all updates after this vision update,
339    // it's guaranteed to be the latest vision update.
340    m_poseEstimate = visionUpdate.compensate(m_odometry.getPose());
341  }
342
343  /**
344   * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate
345   * while still accounting for measurement noise.
346   *
347   * <p>This method can be called as infrequently as you want, as long as you are calling {@link
348   * PoseEstimator3d#update} every loop.
349   *
350   * <p>To promote stability of the pose estimate and make it robust to bad vision data, we
351   * recommend only adding vision measurements that are already within one meter or so of the
352   * current pose estimate.
353   *
354   * <p>Note that the vision measurement standard deviations passed into this method will continue
355   * to apply to future measurements until a subsequent call to {@link
356   * PoseEstimator3d#setVisionMeasurementStdDevs(Matrix)} or this method.
357   *
358   * @param visionRobotPose The pose of the robot as measured by the vision camera.
359   * @param timestamp The timestamp of the vision measurement in seconds. Note that if you don't use
360   *     your own time source by calling {@link #updateWithTime}, then you must use a timestamp with
361   *     the same epoch as {@link org.wpilib.system.Timer#getMonotonicTimestamp()}). This means that
362   *     you should use {@link org.wpilib.system.Timer#getMonotonicTimestamp()} as your time source
363   *     in this case.
364   * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position
365   *     in meters, y position in meters, z position in meters, and angle in radians). Increase
366   *     these numbers to trust the vision pose measurement less.
367   */
368  public void addVisionMeasurement(
369      Pose3d visionRobotPose, double timestamp, Matrix<N4, N1> visionMeasurementStdDevs) {
370    setVisionMeasurementStdDevs(visionMeasurementStdDevs);
371    addVisionMeasurement(visionRobotPose, timestamp);
372  }
373
374  /**
375   * Updates the pose estimator with wheel encoder and gyro information. This should be called every
376   * loop.
377   *
378   * @param gyroAngle The current gyro angle.
379   * @param wheelPositions The current encoder readings.
380   * @return The estimated pose of the robot in meters.
381   */
382  public Pose3d update(Rotation3d gyroAngle, T wheelPositions) {
383    return updateWithTime(MathSharedStore.getTimestamp(), gyroAngle, wheelPositions);
384  }
385
386  /**
387   * Updates the pose estimator with wheel encoder and gyro information. This should be called every
388   * loop.
389   *
390   * @param currentTime Time at which this method was called, in seconds.
391   * @param gyroAngle The current gyro angle.
392   * @param wheelPositions The current encoder readings.
393   * @return The estimated pose of the robot in meters.
394   */
395  public Pose3d updateWithTime(double currentTime, Rotation3d gyroAngle, T wheelPositions) {
396    var odometryEstimate = m_odometry.update(gyroAngle, wheelPositions);
397
398    m_odometryPoseBuffer.addSample(currentTime, odometryEstimate);
399
400    if (m_visionUpdates.isEmpty()) {
401      m_poseEstimate = odometryEstimate;
402    } else {
403      var visionUpdate = m_visionUpdates.get(m_visionUpdates.lastKey());
404      m_poseEstimate = visionUpdate.compensate(odometryEstimate);
405    }
406
407    return getEstimatedPosition();
408  }
409
410  /**
411   * Represents a vision update record. The record contains the vision-compensated pose estimate as
412   * well as the corresponding odometry pose estimate.
413   */
414  private static final class VisionUpdate {
415    // The vision-compensated pose estimate.
416    private final Pose3d visionPose;
417
418    // The pose estimated based solely on odometry.
419    private final Pose3d odometryPose;
420
421    /**
422     * Constructs a vision update record with the specified parameters.
423     *
424     * @param visionPose The vision-compensated pose estimate.
425     * @param odometryPose The pose estimate based solely on odometry.
426     */
427    private VisionUpdate(Pose3d visionPose, Pose3d odometryPose) {
428      this.visionPose = visionPose;
429      this.odometryPose = odometryPose;
430    }
431
432    /**
433     * Returns the vision-compensated version of the pose. Specifically, changes the pose from being
434     * relative to this record's odometry pose to being relative to this record's vision pose.
435     *
436     * @param pose The pose to compensate.
437     * @return The compensated pose.
438     */
439    public Pose3d compensate(Pose3d pose) {
440      var delta = pose.minus(this.odometryPose);
441      return this.visionPose.plus(delta);
442    }
443  }
444}