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.math.geometry;
006
007import com.fasterxml.jackson.annotation.JsonAutoDetect;
008import com.fasterxml.jackson.annotation.JsonCreator;
009import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
010import com.fasterxml.jackson.annotation.JsonProperty;
011import edu.wpi.first.math.MatBuilder;
012import edu.wpi.first.math.Matrix;
013import edu.wpi.first.math.Nat;
014import edu.wpi.first.math.VecBuilder;
015import edu.wpi.first.math.Vector;
016import edu.wpi.first.math.interpolation.Interpolatable;
017import edu.wpi.first.math.numbers.N1;
018import edu.wpi.first.math.numbers.N3;
019import java.util.Objects;
020
021/** Represents a 3D pose containing translational and rotational elements. */
022@JsonIgnoreProperties(ignoreUnknown = true)
023@JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.NONE)
024public class Pose3d implements Interpolatable<Pose3d> {
025  private final Translation3d m_translation;
026  private final Rotation3d m_rotation;
027
028  /** Constructs a pose at the origin facing toward the positive X axis. */
029  public Pose3d() {
030    m_translation = new Translation3d();
031    m_rotation = new Rotation3d();
032  }
033
034  /**
035   * Constructs a pose with the specified translation and rotation.
036   *
037   * @param translation The translational component of the pose.
038   * @param rotation The rotational component of the pose.
039   */
040  @JsonCreator
041  public Pose3d(
042      @JsonProperty(required = true, value = "translation") Translation3d translation,
043      @JsonProperty(required = true, value = "rotation") Rotation3d rotation) {
044    m_translation = translation;
045    m_rotation = rotation;
046  }
047
048  /**
049   * Constructs a pose with x, y, and z translations instead of a separate Translation3d.
050   *
051   * @param x The x component of the translational component of the pose.
052   * @param y The y component of the translational component of the pose.
053   * @param z The z component of the translational component of the pose.
054   * @param rotation The rotational component of the pose.
055   */
056  public Pose3d(double x, double y, double z, Rotation3d rotation) {
057    m_translation = new Translation3d(x, y, z);
058    m_rotation = rotation;
059  }
060
061  /**
062   * Constructs a 3D pose from a 2D pose in the X-Y plane.
063   *
064   * @param pose The 2D pose.
065   */
066  public Pose3d(Pose2d pose) {
067    m_translation = new Translation3d(pose.getX(), pose.getY(), 0.0);
068    m_rotation = new Rotation3d(0.0, 0.0, pose.getRotation().getRadians());
069  }
070
071  /**
072   * Transforms the pose by the given transformation and returns the new transformed pose.
073   *
074   * @param other The transform to transform the pose by.
075   * @return The transformed pose.
076   */
077  public Pose3d plus(Transform3d other) {
078    return transformBy(other);
079  }
080
081  /**
082   * Returns the Transform3d that maps the one pose to another.
083   *
084   * @param other The initial pose of the transformation.
085   * @return The transform that maps the other pose to the current pose.
086   */
087  public Transform3d minus(Pose3d other) {
088    final var pose = this.relativeTo(other);
089    return new Transform3d(pose.getTranslation(), pose.getRotation());
090  }
091
092  /**
093   * Returns the translation component of the transformation.
094   *
095   * @return The translational component of the pose.
096   */
097  @JsonProperty
098  public Translation3d getTranslation() {
099    return m_translation;
100  }
101
102  /**
103   * Returns the X component of the pose's translation.
104   *
105   * @return The x component of the pose's translation.
106   */
107  public double getX() {
108    return m_translation.getX();
109  }
110
111  /**
112   * Returns the Y component of the pose's translation.
113   *
114   * @return The y component of the pose's translation.
115   */
116  public double getY() {
117    return m_translation.getY();
118  }
119
120  /**
121   * Returns the Z component of the pose's translation.
122   *
123   * @return The z component of the pose's translation.
124   */
125  public double getZ() {
126    return m_translation.getZ();
127  }
128
129  /**
130   * Returns the rotational component of the transformation.
131   *
132   * @return The rotational component of the pose.
133   */
134  @JsonProperty
135  public Rotation3d getRotation() {
136    return m_rotation;
137  }
138
139  /**
140   * Multiplies the current pose by a scalar.
141   *
142   * @param scalar The scalar.
143   * @return The new scaled Pose3d.
144   */
145  public Pose3d times(double scalar) {
146    return new Pose3d(m_translation.times(scalar), m_rotation.times(scalar));
147  }
148
149  /**
150   * Divides the current pose by a scalar.
151   *
152   * @param scalar The scalar.
153   * @return The new scaled Pose3d.
154   */
155  public Pose3d div(double scalar) {
156    return times(1.0 / scalar);
157  }
158
159  /**
160   * Transforms the pose by the given transformation and returns the new pose. See + operator for
161   * the matrix multiplication performed.
162   *
163   * @param other The transform to transform the pose by.
164   * @return The transformed pose.
165   */
166  public Pose3d transformBy(Transform3d other) {
167    return new Pose3d(
168        m_translation.plus(other.getTranslation().rotateBy(m_rotation)),
169        other.getRotation().plus(m_rotation));
170  }
171
172  /**
173   * Returns the current pose relative to the given pose.
174   *
175   * <p>This function can often be used for trajectory tracking or pose stabilization algorithms to
176   * get the error between the reference and the current pose.
177   *
178   * @param other The pose that is the origin of the new coordinate frame that the current pose will
179   *     be converted into.
180   * @return The current pose relative to the new origin pose.
181   */
182  public Pose3d relativeTo(Pose3d other) {
183    var transform = new Transform3d(other, this);
184    return new Pose3d(transform.getTranslation(), transform.getRotation());
185  }
186
187  /**
188   * Obtain a new Pose3d from a (constant curvature) velocity.
189   *
190   * <p>The twist is a change in pose in the robot's coordinate frame since the previous pose
191   * update. When the user runs exp() on the previous known field-relative pose with the argument
192   * being the twist, the user will receive the new field-relative pose.
193   *
194   * <p>"Exp" represents the pose exponential, which is solving a differential equation moving the
195   * pose forward in time.
196   *
197   * @param twist The change in pose in the robot's coordinate frame since the previous pose update.
198   *     For example, if a non-holonomic robot moves forward 0.01 meters and changes angle by 0.5
199   *     degrees since the previous pose update, the twist would be Twist3d(0.01, 0.0, 0.0, new new
200   *     Rotation3d(0.0, 0.0, Units.degreesToRadians(0.5))).
201   * @return The new pose of the robot.
202   */
203  public Pose3d exp(Twist3d twist) {
204    // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf
205    final var u = VecBuilder.fill(twist.dx, twist.dy, twist.dz);
206    final var rvec = VecBuilder.fill(twist.rx, twist.ry, twist.rz);
207    final var omega = rotationVectorToMatrix(rvec);
208    final var omegaSq = omega.times(omega);
209    double theta = rvec.norm();
210    double thetaSq = theta * theta;
211
212    double A;
213    double B;
214    double C;
215    if (Math.abs(theta) < 1E-7) {
216      // Taylor Expansions around θ = 0
217      // A = 1/1! - θ²/3! + θ⁴/5!
218      // B = 1/2! - θ²/4! + θ⁴/6!
219      // C = 1/3! - θ²/5! + θ⁴/7!
220      // sources:
221      // A:
222      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0
223      // B:
224      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5B1-cos%5C%2840%29x%5C%2841%29%2CPower%5Bx%2C2%5D%5D+at+x%3D0
225      // C:
226      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5B1-Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D%2CPower%5Bx%2C2%5D%5D+at+x%3D0
227      A = 1 - thetaSq / 6 + thetaSq * thetaSq / 120;
228      B = 1 / 2.0 - thetaSq / 24 + thetaSq * thetaSq / 720;
229      C = 1 / 6.0 - thetaSq / 120 + thetaSq * thetaSq / 5040;
230    } else {
231      // A = sin(θ)/θ
232      // B = (1 - cos(θ)) / θ²
233      // C = (1 - A) / θ²
234      A = Math.sin(theta) / theta;
235      B = (1 - Math.cos(theta)) / thetaSq;
236      C = (1 - A) / thetaSq;
237    }
238
239    Matrix<N3, N3> R = Matrix.eye(Nat.N3()).plus(omega.times(A)).plus(omegaSq.times(B));
240    Matrix<N3, N3> V = Matrix.eye(Nat.N3()).plus(omega.times(B)).plus(omegaSq.times(C));
241    Matrix<N3, N1> translation_component = V.times(u);
242    final var transform =
243        new Transform3d(
244            new Translation3d(
245                translation_component.get(0, 0),
246                translation_component.get(1, 0),
247                translation_component.get(2, 0)),
248            new Rotation3d(R));
249
250    return this.plus(transform);
251  }
252
253  /**
254   * Returns a Twist3d that maps this pose to the end pose. If c is the output of {@code a.Log(b)},
255   * then {@code a.Exp(c)} would yield b.
256   *
257   * @param end The end pose for the transformation.
258   * @return The twist that maps this to end.
259   */
260  public Twist3d log(Pose3d end) {
261    // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf
262    final var transform = end.relativeTo(this);
263
264    final var rvec = transform.getRotation().getQuaternion().toRotationVector();
265
266    final var omega = rotationVectorToMatrix(rvec);
267    final var theta = rvec.norm();
268    final var thetaSq = theta * theta;
269
270    double C;
271    if (Math.abs(theta) < 1E-7) {
272      // Taylor Expansions around θ = 0
273      // A = 1/1! - θ²/3! + θ⁴/5!
274      // B = 1/2! - θ²/4! + θ⁴/6!
275      // C = 1/6 * (1/2 + θ²/5! + θ⁴/7!)
276      // sources:
277      // A:
278      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0
279      // B:
280      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5B1-cos%5C%2840%29x%5C%2841%29%2CPower%5Bx%2C2%5D%5D+at+x%3D0
281      // C:
282      // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5B1-Divide%5BDivide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D%2C2Divide%5B1-cos%5C%2840%29x%5C%2841%29%2CPower%5Bx%2C2%5D%5D%5D%2CPower%5Bx%2C2%5D%5D+at+x%3D0
283      C = 1 / 12.0 + thetaSq / 720 + thetaSq * thetaSq / 30240;
284    } else {
285      // A = sin(θ)/θ
286      // B = (1 - cos(θ)) / θ²
287      // C = (1 - A/(2*B)) / θ²
288      double A = Math.sin(theta) / theta;
289      double B = (1 - Math.cos(theta)) / thetaSq;
290      C = (1 - A / (2 * B)) / thetaSq;
291    }
292
293    final var V_inv =
294        Matrix.eye(Nat.N3()).minus(omega.times(0.5)).plus(omega.times(omega).times(C));
295
296    final var twist_translation =
297        V_inv.times(VecBuilder.fill(transform.getX(), transform.getY(), transform.getZ()));
298
299    return new Twist3d(
300        twist_translation.get(0, 0),
301        twist_translation.get(1, 0),
302        twist_translation.get(2, 0),
303        rvec.get(0, 0),
304        rvec.get(1, 0),
305        rvec.get(2, 0));
306  }
307
308  /**
309   * Returns a Pose2d representing this Pose3d projected into the X-Y plane.
310   *
311   * @return A Pose2d representing this Pose3d projected into the X-Y plane.
312   */
313  public Pose2d toPose2d() {
314    return new Pose2d(m_translation.toTranslation2d(), m_rotation.toRotation2d());
315  }
316
317  @Override
318  public String toString() {
319    return String.format("Pose3d(%s, %s)", m_translation, m_rotation);
320  }
321
322  /**
323   * Checks equality between this Pose3d and another object.
324   *
325   * @param obj The other object.
326   * @return Whether the two objects are equal or not.
327   */
328  @Override
329  public boolean equals(Object obj) {
330    if (obj instanceof Pose3d) {
331      return ((Pose3d) obj).m_translation.equals(m_translation)
332          && ((Pose3d) obj).m_rotation.equals(m_rotation);
333    }
334    return false;
335  }
336
337  @Override
338  public int hashCode() {
339    return Objects.hash(m_translation, m_rotation);
340  }
341
342  @Override
343  public Pose3d interpolate(Pose3d endValue, double t) {
344    if (t < 0) {
345      return this;
346    } else if (t >= 1) {
347      return endValue;
348    } else {
349      var twist = this.log(endValue);
350      var scaledTwist =
351          new Twist3d(
352              twist.dx * t, twist.dy * t, twist.dz * t, twist.rx * t, twist.ry * t, twist.rz * t);
353      return this.exp(scaledTwist);
354    }
355  }
356
357  /**
358   * Applies the hat operator to a rotation vector.
359   *
360   * <p>It takes a rotation vector and returns the corresponding matrix representation of the Lie
361   * algebra element (a 3x3 rotation matrix).
362   *
363   * @param rotation The rotation vector.
364   * @return The rotation vector as a 3x3 rotation matrix.
365   */
366  private Matrix<N3, N3> rotationVectorToMatrix(Vector<N3> rotation) {
367    // Given a rotation vector <a, b, c>,
368    //         [ 0 -c  b]
369    // Omega = [ c  0 -a]
370    //         [-b  a  0]
371    return new MatBuilder<>(Nat.N3(), Nat.N3())
372        .fill(
373            0.0,
374            -rotation.get(2, 0),
375            rotation.get(1, 0),
376            rotation.get(2, 0),
377            0.0,
378            -rotation.get(0, 0),
379            -rotation.get(1, 0),
380            rotation.get(0, 0),
381            0.0);
382  }
383}