diff --git a/benchmark/src/main/java/frc/robot/Main.java b/benchmark/src/main/java/frc/robot/Main.java index bb7564a6c2..862a2f58b7 100644 --- a/benchmark/src/main/java/frc/robot/Main.java +++ b/benchmark/src/main/java/frc/robot/Main.java @@ -39,7 +39,7 @@ public class Main { private static final TravelingSalesman twistTraveler = new TravelingSalesman( (pose1, pose2) -> { - var twist = pose1.log(pose2); + var twist = pose2.minus(pose1).log(); return Math.hypot(twist.dx, twist.dy); }); diff --git a/benchmark/src/main/native/cpp/Main.cpp b/benchmark/src/main/native/cpp/Main.cpp index e332345a88..1a9c3262a4 100644 --- a/benchmark/src/main/native/cpp/Main.cpp +++ b/benchmark/src/main/native/cpp/Main.cpp @@ -30,7 +30,7 @@ BENCHMARK(BM_Transform); void BM_Twist(benchmark::State& state) { frc::TravelingSalesman traveler{[](auto pose1, auto pose2) { - auto twist = pose1.Log(pose2); + auto twist = (pose2 - pose1).Log(); return units::math::hypot(twist.dx, twist.dy).value(); }}; for (auto _ : state) { diff --git a/wpimath/CMakeLists.txt b/wpimath/CMakeLists.txt index ef21f48c6c..c5be554abb 100644 --- a/wpimath/CMakeLists.txt +++ b/wpimath/CMakeLists.txt @@ -12,8 +12,9 @@ file( src/main/native/cpp/jni/EigenJNI.cpp src/main/native/cpp/jni/Ellipse2dJNI.cpp src/main/native/cpp/jni/Exceptions.cpp - src/main/native/cpp/jni/Pose3dJNI.cpp src/main/native/cpp/jni/StateSpaceUtilJNI.cpp + src/main/native/cpp/jni/Transform3dJNI.cpp + src/main/native/cpp/jni/Twist3dJNI.cpp ) # Java bindings diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java index c68b8f1d9f..09a43bf952 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java +++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator.java @@ -271,7 +271,7 @@ public class PoseEstimator { } // Step 4: Measure the twist between the old pose estimate and the vision pose. - var twist = visionSample.get().log(visionRobotPose); + var twist = visionRobotPose.minus(visionSample.get()).log(); // Step 5: 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. @@ -282,7 +282,8 @@ public class PoseEstimator { new Twist2d(k_times_twist.get(0, 0), k_times_twist.get(1, 0), k_times_twist.get(2, 0)); // Step 7: Calculate and record the vision update. - var visionUpdate = new VisionUpdate(visionSample.get().exp(scaledTwist), odometrySample.get()); + var visionUpdate = + new VisionUpdate(visionSample.get().plus(scaledTwist.exp()), odometrySample.get()); m_visionUpdates.put(timestamp, visionUpdate); // Step 8: Remove later vision measurements. (Matches previous behavior) diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator3d.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator3d.java index b3844a2bde..124ca35105 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator3d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/PoseEstimator3d.java @@ -284,7 +284,7 @@ public class PoseEstimator3d { } // Step 4: Measure the twist between the old pose estimate and the vision pose. - var twist = visionSample.get().log(visionRobotPose); + var twist = visionRobotPose.minus(visionSample.get()).log(); // Step 5: 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. @@ -303,7 +303,8 @@ public class PoseEstimator3d { k_times_twist.get(5, 0)); // Step 7: Calculate and record the vision update. - var visionUpdate = new VisionUpdate(visionSample.get().exp(scaledTwist), odometrySample.get()); + var visionUpdate = + new VisionUpdate(visionSample.get().plus(scaledTwist.exp()), odometrySample.get()); m_visionUpdates.put(timestamp, visionUpdate); // Step 8: Remove later vision measurements. (Matches previous behavior) diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose2d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose2d.java index 4a84ef6543..7ee7fc6fc7 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose2d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose2d.java @@ -249,81 +249,6 @@ public class Pose2d implements Interpolatable, ProtobufSerializable, Str return new Pose2d(m_translation.rotateAround(point, rot), m_rotation.rotateBy(rot)); } - /** - * Obtain a new Pose2d from a (constant curvature) velocity. - * - *

See Controls - * Engineering in the FIRST Robotics Competition section 10.2 "Pose exponential" for a - * derivation. - * - *

The twist is a change in pose in the robot's coordinate frame since the previous pose - * update. When the user runs exp() on the previous known field-relative pose with the argument - * being the twist, the user will receive the new field-relative pose. - * - *

"Exp" represents the pose exponential, which is solving a differential equation moving the - * pose forward in time. - * - * @param twist The change in pose in the robot's coordinate frame since the previous pose update. - * For example, if a non-holonomic robot moves forward 0.01 meters and changes angle by 0.5 - * degrees since the previous pose update, the twist would be Twist2d(0.01, 0.0, - * Units.degreesToRadians(0.5)). - * @return The new pose of the robot. - */ - public Pose2d exp(Twist2d twist) { - double dx = twist.dx; - double dy = twist.dy; - double dtheta = twist.dtheta; - - double sinTheta = Math.sin(dtheta); - double cosTheta = Math.cos(dtheta); - - double s; - double c; - if (Math.abs(dtheta) < 1E-9) { - s = 1.0 - 1.0 / 6.0 * dtheta * dtheta; - c = 0.5 * dtheta; - } else { - s = sinTheta / dtheta; - c = (1 - cosTheta) / dtheta; - } - var transform = - new Transform2d( - new Translation2d(dx * s - dy * c, dx * c + dy * s), - new Rotation2d(cosTheta, sinTheta)); - - return this.plus(transform); - } - - /** - * Returns a Twist2d that maps this pose to the end pose. If c is the output of {@code a.Log(b)}, - * then {@code a.Exp(c)} would yield b. - * - * @param end The end pose for the transformation. - * @return The twist that maps this to end. - */ - public Twist2d log(Pose2d end) { - final var transform = end.relativeTo(this); - final var dtheta = transform.getRotation().getRadians(); - final var halfDtheta = dtheta / 2.0; - - final var cosMinusOne = transform.getRotation().getCos() - 1; - - double halfThetaByTanOfHalfDtheta; - if (Math.abs(cosMinusOne) < 1E-9) { - halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; - } else { - halfThetaByTanOfHalfDtheta = -(halfDtheta * transform.getRotation().getSin()) / cosMinusOne; - } - - Translation2d translationPart = - transform - .getTranslation() - .rotateBy(new Rotation2d(halfThetaByTanOfHalfDtheta, -halfDtheta)) - .times(Math.hypot(halfThetaByTanOfHalfDtheta, halfDtheta)); - - return new Twist2d(translationPart.getX(), translationPart.getY(), dtheta); - } - /** * Returns an affine transformation matrix representation of this pose. * @@ -393,9 +318,9 @@ public class Pose2d implements Interpolatable, ProtobufSerializable, Str } else if (t >= 1) { return endValue; } else { - var twist = this.log(endValue); + var twist = endValue.minus(this).log(); var scaledTwist = new Twist2d(twist.dx * t, twist.dy * t, twist.dtheta * t); - return this.exp(scaledTwist); + return this.plus(scaledTwist.exp()); } } diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose3d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose3d.java index 20204235bc..a723d63e10 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose3d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Pose3d.java @@ -16,7 +16,6 @@ import edu.wpi.first.math.Nat; import edu.wpi.first.math.geometry.proto.Pose3dProto; import edu.wpi.first.math.geometry.struct.Pose3dStruct; import edu.wpi.first.math.interpolation.Interpolatable; -import edu.wpi.first.math.jni.Pose3dJNI; import edu.wpi.first.math.numbers.N4; import edu.wpi.first.units.measure.Distance; import edu.wpi.first.util.protobuf.ProtobufSerializable; @@ -285,82 +284,6 @@ public class Pose3d implements Interpolatable, ProtobufSerializable, Str return new Pose3d(m_translation.rotateAround(point, rot), m_rotation.rotateBy(rot)); } - /** - * Obtain a new Pose3d from a (constant curvature) velocity. - * - *

The twist is a change in pose in the robot's coordinate frame since the previous pose - * update. When the user runs exp() on the previous known field-relative pose with the argument - * being the twist, the user will receive the new field-relative pose. - * - *

"Exp" represents the pose exponential, which is solving a differential equation moving the - * pose forward in time. - * - * @param twist The change in pose in the robot's coordinate frame since the previous pose update. - * For example, if a non-holonomic robot moves forward 0.01 meters and changes angle by 0.5 - * degrees since the previous pose update, the twist would be Twist3d(0.01, 0.0, 0.0, new new - * Rotation3d(0.0, 0.0, Units.degreesToRadians(0.5))). - * @return The new pose of the robot. - */ - public Pose3d exp(Twist3d twist) { - var quaternion = this.getRotation().getQuaternion(); - double[] resultArray = - Pose3dJNI.exp( - this.getX(), - this.getY(), - this.getZ(), - quaternion.getW(), - quaternion.getX(), - quaternion.getY(), - quaternion.getZ(), - twist.dx, - twist.dy, - twist.dz, - twist.rx, - twist.ry, - twist.rz); - return new Pose3d( - resultArray[0], - resultArray[1], - resultArray[2], - new Rotation3d( - new Quaternion(resultArray[3], resultArray[4], resultArray[5], resultArray[6]))); - } - - /** - * Returns a Twist3d that maps this pose to the end pose. If c is the output of {@code a.Log(b)}, - * then {@code a.Exp(c)} would yield b. - * - * @param end The end pose for the transformation. - * @return The twist that maps this to end. - */ - public Twist3d log(Pose3d end) { - var thisQuaternion = this.getRotation().getQuaternion(); - var endQuaternion = end.getRotation().getQuaternion(); - double[] resultArray = - Pose3dJNI.log( - this.getX(), - this.getY(), - this.getZ(), - thisQuaternion.getW(), - thisQuaternion.getX(), - thisQuaternion.getY(), - thisQuaternion.getZ(), - end.getX(), - end.getY(), - end.getZ(), - endQuaternion.getW(), - endQuaternion.getX(), - endQuaternion.getY(), - endQuaternion.getZ()); - return new Twist3d( - resultArray[0], - resultArray[1], - resultArray[2], - resultArray[3], - resultArray[4], - resultArray[5]); - } - /** * Returns an affine transformation matrix representation of this pose. * @@ -445,11 +368,11 @@ public class Pose3d implements Interpolatable, ProtobufSerializable, Str } else if (t >= 1) { return endValue; } else { - var twist = this.log(endValue); + var twist = endValue.minus(this).log(); var scaledTwist = new Twist3d( twist.dx * t, twist.dy * t, twist.dz * t, twist.rx * t, twist.ry * t, twist.rz * t); - return this.exp(scaledTwist); + return this.plus(scaledTwist.exp()); } } diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform2d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform2d.java index 7a4cf8f4f9..475f037b1a 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform2d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform2d.java @@ -209,6 +209,33 @@ public class Transform2d implements ProtobufSerializable, StructSerializable { return m_rotation; } + /** + * Returns a Twist2d of the current transform (pose delta). If b is the output of {@code a.log()}, + * then {@code b.exp()} would yield a. + * + * @return The twist that maps the current transform. + */ + public Twist2d log() { + final double dtheta = m_rotation.getRadians(); + final double halfDtheta = dtheta / 2.0; + + final double cosMinusOne = m_rotation.getCos() - 1; + + double halfThetaByTanOfHalfDtheta; + if (Math.abs(cosMinusOne) < 1E-9) { + halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; + } else { + halfThetaByTanOfHalfDtheta = -(halfDtheta * m_rotation.getSin()) / cosMinusOne; + } + + Translation2d translationPart = + m_translation + .rotateBy(new Rotation2d(halfThetaByTanOfHalfDtheta, -halfDtheta)) + .times(Math.hypot(halfThetaByTanOfHalfDtheta, halfDtheta)); + + return new Twist2d(translationPart.getX(), translationPart.getY(), dtheta); + } + /** * Invert the transformation. This is useful for undoing a transformation. * diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform3d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform3d.java index d993d86100..dc893d1279 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform3d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Transform3d.java @@ -11,6 +11,7 @@ import edu.wpi.first.math.Matrix; import edu.wpi.first.math.Nat; import edu.wpi.first.math.geometry.proto.Transform3dProto; import edu.wpi.first.math.geometry.struct.Transform3dStruct; +import edu.wpi.first.math.jni.Transform3dJNI; import edu.wpi.first.math.numbers.N4; import edu.wpi.first.units.measure.Distance; import edu.wpi.first.util.protobuf.ProtobufSerializable; @@ -251,6 +252,32 @@ public class Transform3d implements ProtobufSerializable, StructSerializable { return m_rotation; } + /** + * Returns a Twist3d of the current transform (pose delta). If b is the output of {@code a.log()}, + * then {@code b.exp()} would yield a. + * + * @return The twist that maps the current transform. + */ + public Twist3d log() { + var thisQuaternion = m_rotation.getQuaternion(); + double[] resultArray = + Transform3dJNI.log( + this.getX(), + this.getY(), + this.getZ(), + thisQuaternion.getW(), + thisQuaternion.getX(), + thisQuaternion.getY(), + thisQuaternion.getZ()); + return new Twist3d( + resultArray[0], + resultArray[1], + resultArray[2], + resultArray[3], + resultArray[4], + resultArray[5]); + } + /** * Invert the transformation. This is useful for undoing a transformation. * diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist2d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist2d.java index 0867d05062..36edd569c6 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist2d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist2d.java @@ -42,6 +42,39 @@ public class Twist2d implements ProtobufSerializable, StructSerializable { this.dtheta = dtheta; } + /** + * Obtain a new Transform2d from a (constant curvature) velocity. + * + *

See Controls + * Engineering in the FIRST Robotics Competition section 10.2 "Pose exponential" for a + * derivation. + * + *

The twist is a change in pose in the robot's coordinate frame since the previous pose + * update. When the user runs exp() on the twist, the user will receive the pose delta. + * + *

"Exp" represents the pose exponential, which is solving a differential equation moving the + * pose forward in time. + * + * @return The pose delta of the robot. + */ + public Transform2d exp() { + double sinTheta = Math.sin(dtheta); + double cosTheta = Math.cos(dtheta); + + double s; + double c; + if (Math.abs(dtheta) < 1E-9) { + s = 1.0 - 1.0 / 6.0 * dtheta * dtheta; + c = 0.5 * dtheta; + } else { + s = sinTheta / dtheta; + c = (1 - cosTheta) / dtheta; + } + + return new Transform2d( + new Translation2d(dx * s - dy * c, dx * c + dy * s), new Rotation2d(cosTheta, sinTheta)); + } + @Override public String toString() { return String.format("Twist2d(dX: %.2f, dY: %.2f, dTheta: %.2f)", dx, dy, dtheta); diff --git a/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist3d.java b/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist3d.java index a03a035adf..64274dfb24 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist3d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/geometry/Twist3d.java @@ -6,6 +6,7 @@ package edu.wpi.first.math.geometry; import edu.wpi.first.math.geometry.proto.Twist3dProto; import edu.wpi.first.math.geometry.struct.Twist3dStruct; +import edu.wpi.first.math.jni.Twist3dJNI; import edu.wpi.first.util.protobuf.ProtobufSerializable; import edu.wpi.first.util.struct.StructSerializable; import java.util.Objects; @@ -57,6 +58,31 @@ public class Twist3d implements ProtobufSerializable, StructSerializable { this.rz = rz; } + /** + * Obtain a new Transform3d from a (constant curvature) velocity. + * + *

See Controls + * Engineering in the FIRST Robotics Competition section 10.2 "Pose exponential" for a + * derivation. + * + *

The twist is a change in pose in the robot's coordinate frame since the previous pose + * update. When the user runs exp() on the twist, the user will receive the pose delta. + * + *

"Exp" represents the pose exponential, which is solving a differential equation moving the + * pose forward in time. + * + * @return The pose delta of the robot. + */ + public Transform3d exp() { + double[] resultArray = Twist3dJNI.exp(dx, dy, dz, rx, ry, rz); + return new Transform3d( + resultArray[0], + resultArray[1], + resultArray[2], + new Rotation3d( + new Quaternion(resultArray[3], resultArray[4], resultArray[5], resultArray[6]))); + } + @Override public String toString() { return String.format( diff --git a/wpimath/src/main/java/edu/wpi/first/math/jni/Pose3dJNI.java b/wpimath/src/main/java/edu/wpi/first/math/jni/Pose3dJNI.java deleted file mode 100644 index f32d58e0b1..0000000000 --- a/wpimath/src/main/java/edu/wpi/first/math/jni/Pose3dJNI.java +++ /dev/null @@ -1,83 +0,0 @@ -// 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.math.jni; - -/** Pose3d JNI. */ -public final class Pose3dJNI extends WPIMathJNI { - /** - * Obtain a Pose3d from a (constant curvature) velocity. - * - *

The double array returned is of the form [dx, dy, dz, qx, qy, qz]. - * - * @param poseX The pose's translational X component. - * @param poseY The pose's translational Y component. - * @param poseZ The pose's translational Z component. - * @param poseQw The pose quaternion's W component. - * @param poseQx The pose quaternion's X component. - * @param poseQy The pose quaternion's Y component. - * @param poseQz The pose quaternion's Z component. - * @param twistDx The twist's dx value. - * @param twistDy The twist's dy value. - * @param twistDz The twist's dz value. - * @param twistRx The twist's rx value. - * @param twistRy The twist's ry value. - * @param twistRz The twist's rz value. - * @return The new pose as a double array. - */ - public static native double[] exp( - double poseX, - double poseY, - double poseZ, - double poseQw, - double poseQx, - double poseQy, - double poseQz, - double twistDx, - double twistDy, - double twistDz, - double twistRx, - double twistRy, - double twistRz); - - /** - * Returns a Twist3d that maps the starting pose to the end pose. - * - *

The double array returned is of the form [dx, dy, dz, rx, ry, rz]. - * - * @param startX The starting pose's translational X component. - * @param startY The starting pose's translational Y component. - * @param startZ The starting pose's translational Z component. - * @param startQw The starting pose quaternion's W component. - * @param startQx The starting pose quaternion's X component. - * @param startQy The starting pose quaternion's Y component. - * @param startQz The starting pose quaternion's Z component. - * @param endX The ending pose's translational X component. - * @param endY The ending pose's translational Y component. - * @param endZ The ending pose's translational Z component. - * @param endQw The ending pose quaternion's W component. - * @param endQx The ending pose quaternion's X component. - * @param endQy The ending pose quaternion's Y component. - * @param endQz The ending pose quaternion's Z component. - * @return The twist that maps start to end as a double array. - */ - public static native double[] log( - double startX, - double startY, - double startZ, - double startQw, - double startQx, - double startQy, - double startQz, - double endX, - double endY, - double endZ, - double endQw, - double endQx, - double endQy, - double endQz); - - /** Utility class. */ - private Pose3dJNI() {} -} diff --git a/wpimath/src/main/java/edu/wpi/first/math/jni/Transform3dJNI.java b/wpimath/src/main/java/edu/wpi/first/math/jni/Transform3dJNI.java new file mode 100644 index 0000000000..22fff99fe8 --- /dev/null +++ b/wpimath/src/main/java/edu/wpi/first/math/jni/Transform3dJNI.java @@ -0,0 +1,34 @@ +// 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.math.jni; + +/** Transform3d JNI. */ +public final class Transform3dJNI extends WPIMathJNI { + /** + * Returns a Twist3d that maps the Transform3d (pose delta). + * + *

The double array returned is of the form [dx, dy, dz, rx, ry, rz]. + * + * @param relX The transform's translational X component. + * @param relY The transform's translational Y component. + * @param relZ The transform's translational Z component. + * @param relQw The transform quaternion's W component. + * @param relQx The transform quaternion's X component. + * @param relQy The transform quaternion's Y component. + * @param relQz The transform quaternion's Z component. + * @return The twist that maps start to end as a double array. + */ + public static native double[] log( + double relX, + double relY, + double relZ, + double relQw, + double relQx, + double relQy, + double relQz); + + /** Utility class. */ + private Transform3dJNI() {} +} diff --git a/wpimath/src/main/java/edu/wpi/first/math/jni/Twist3dJNI.java b/wpimath/src/main/java/edu/wpi/first/math/jni/Twist3dJNI.java new file mode 100644 index 0000000000..d594c0cafc --- /dev/null +++ b/wpimath/src/main/java/edu/wpi/first/math/jni/Twist3dJNI.java @@ -0,0 +1,32 @@ +// 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.math.jni; + +/** Twist3d JNI. */ +public final class Twist3dJNI extends WPIMathJNI { + /** + * Obtain a Transform3d from a (constant curvature) velocity. + * + *

The double array returned is of the form [dx, dy, dz, qw, qx, qy, qz]. + * + * @param twistDx The twist's dx value. + * @param twistDy The twist's dy value. + * @param twistDz The twist's dz value. + * @param twistRx The twist's rx value. + * @param twistRy The twist's ry value. + * @param twistRz The twist's rz value. + * @return The new pose as a double array. + */ + public static native double[] exp( + double twistDx, + double twistDy, + double twistDz, + double twistRx, + double twistRy, + double twistRz); + + /** Utility class. */ + private Twist3dJNI() {} +} diff --git a/wpimath/src/main/java/edu/wpi/first/math/kinematics/ChassisSpeeds.java b/wpimath/src/main/java/edu/wpi/first/math/kinematics/ChassisSpeeds.java index 9de9d528f7..913a66a784 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/kinematics/ChassisSpeeds.java +++ b/wpimath/src/main/java/edu/wpi/first/math/kinematics/ChassisSpeeds.java @@ -7,8 +7,8 @@ package edu.wpi.first.math.kinematics; import static edu.wpi.first.units.Units.MetersPerSecond; import static edu.wpi.first.units.Units.RadiansPerSecond; -import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; +import edu.wpi.first.math.geometry.Transform2d; import edu.wpi.first.math.geometry.Translation2d; import edu.wpi.first.math.geometry.Twist2d; import edu.wpi.first.math.kinematics.proto.ChassisSpeedsProto; @@ -101,11 +101,11 @@ public class ChassisSpeeds implements ProtobufSerializable, StructSerializable { public ChassisSpeeds discretize(double dt) { // Construct the desired pose after a timestep, relative to the current pose. The desired pose // has decoupled translation and rotation. - var desiredDeltaPose = new Pose2d(vx * dt, vy * dt, new Rotation2d(omega * dt)); + var desiredTransform = new Transform2d(vx * dt, vy * dt, new Rotation2d(omega * dt)); // Find the chassis translation/rotation deltas in the robot frame that move the robot from its // current pose to the desired pose - var twist = Pose2d.kZero.log(desiredDeltaPose); + var twist = desiredTransform.log(); // Turn the chassis translation/rotation deltas into average velocities return new ChassisSpeeds(twist.dx / dt, twist.dy / dt, twist.dtheta / dt); diff --git a/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry.java b/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry.java index 9c99696590..c5fee7ebfc 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry.java +++ b/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry.java @@ -117,7 +117,7 @@ public class Odometry { var twist = m_kinematics.toTwist2d(m_previousWheelPositions, wheelPositions); twist.dtheta = angle.minus(m_previousAngle).getRadians(); - var newPose = m_pose.exp(twist); + var newPose = m_pose.plus(twist.exp()); m_kinematics.copyInto(wheelPositions, m_previousWheelPositions); m_previousAngle = angle; diff --git a/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry3d.java b/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry3d.java index a50a7f1d73..12211f721a 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry3d.java +++ b/wpimath/src/main/java/edu/wpi/first/math/kinematics/Odometry3d.java @@ -134,7 +134,7 @@ public class Odometry3d { angle_difference.get(1), angle_difference.get(2)); - var newPose = m_pose.exp(twist); + var newPose = m_pose.plus(twist.exp()); m_kinematics.copyInto(wheelPositions, m_previousWheelPositions); m_previousAngle = angle; diff --git a/wpimath/src/main/java/edu/wpi/first/math/spline/SplineParameterizer.java b/wpimath/src/main/java/edu/wpi/first/math/spline/SplineParameterizer.java index bc63d1a672..35d526e8d0 100644 --- a/wpimath/src/main/java/edu/wpi/first/math/spline/SplineParameterizer.java +++ b/wpimath/src/main/java/edu/wpi/first/math/spline/SplineParameterizer.java @@ -126,7 +126,7 @@ public final class SplineParameterizer { throw new MalformedSplineException(kMalformedSplineExceptionMsg); } - final var twist = start.get().pose.log(end.get().pose); + final var twist = (end.get().pose.minus(start.get().pose)).log(); if (Math.abs(twist.dy) > kMaxDy || Math.abs(twist.dx) > kMaxDx || Math.abs(twist.dtheta) > kMaxDtheta) { diff --git a/wpimath/src/main/native/cpp/jni/Pose3dJNI.cpp b/wpimath/src/main/native/cpp/jni/Pose3dJNI.cpp deleted file mode 100644 index e41e0eb703..0000000000 --- a/wpimath/src/main/native/cpp/jni/Pose3dJNI.cpp +++ /dev/null @@ -1,70 +0,0 @@ -// 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. - -#include - -#include - -#include "edu_wpi_first_math_jni_Pose3dJNI.h" -#include "frc/geometry/Pose3d.h" - -using namespace wpi::java; - -extern "C" { - -/* - * Class: edu_wpi_first_math_jni_Pose3dJNI - * Method: exp - * Signature: (DDDDDDDDDDDDD)[D - */ -JNIEXPORT jdoubleArray JNICALL -Java_edu_wpi_first_math_jni_Pose3dJNI_exp - (JNIEnv* env, jclass, jdouble poseX, jdouble poseY, jdouble poseZ, - jdouble poseQw, jdouble poseQx, jdouble poseQy, jdouble poseQz, - jdouble twistDx, jdouble twistDy, jdouble twistDz, jdouble twistRx, - jdouble twistRy, jdouble twistRz) -{ - frc::Pose3d pose{ - units::meter_t{poseX}, units::meter_t{poseY}, units::meter_t{poseZ}, - frc::Rotation3d{frc::Quaternion{poseQw, poseQx, poseQy, poseQz}}}; - frc::Twist3d twist{units::meter_t{twistDx}, units::meter_t{twistDy}, - units::meter_t{twistDz}, units::radian_t{twistRx}, - units::radian_t{twistRy}, units::radian_t{twistRz}}; - - frc::Pose3d result = pose.Exp(twist); - - const auto& resultQuaternion = result.Rotation().GetQuaternion(); - return MakeJDoubleArray( - env, {{result.X().value(), result.Y().value(), result.Z().value(), - resultQuaternion.W(), resultQuaternion.X(), resultQuaternion.Y(), - resultQuaternion.Z()}}); -} - -/* - * Class: edu_wpi_first_math_jni_Pose3dJNI - * Method: log - * Signature: (DDDDDDDDDDDDDD)[D - */ -JNIEXPORT jdoubleArray JNICALL -Java_edu_wpi_first_math_jni_Pose3dJNI_log - (JNIEnv* env, jclass, jdouble startX, jdouble startY, jdouble startZ, - jdouble startQw, jdouble startQx, jdouble startQy, jdouble startQz, - jdouble endX, jdouble endY, jdouble endZ, jdouble endQw, jdouble endQx, - jdouble endQy, jdouble endQz) -{ - frc::Pose3d startPose{ - units::meter_t{startX}, units::meter_t{startY}, units::meter_t{startZ}, - frc::Rotation3d{frc::Quaternion{startQw, startQx, startQy, startQz}}}; - frc::Pose3d endPose{ - units::meter_t{endX}, units::meter_t{endY}, units::meter_t{endZ}, - frc::Rotation3d{frc::Quaternion{endQw, endQx, endQy, endQz}}}; - - frc::Twist3d result = startPose.Log(endPose); - - return MakeJDoubleArray( - env, {{result.dx.value(), result.dy.value(), result.dz.value(), - result.rx.value(), result.ry.value(), result.rz.value()}}); -} - -} // extern "C" diff --git a/wpimath/src/main/native/cpp/jni/Transform3dJNI.cpp b/wpimath/src/main/native/cpp/jni/Transform3dJNI.cpp new file mode 100644 index 0000000000..893da08b00 --- /dev/null +++ b/wpimath/src/main/native/cpp/jni/Transform3dJNI.cpp @@ -0,0 +1,42 @@ +// 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. + +#include + +#include + +#include "edu_wpi_first_math_jni_Transform3dJNI.h" +#include "frc/geometry/Quaternion.h" +#include "frc/geometry/Rotation3d.h" +#include "frc/geometry/Transform3d.h" +#include "frc/geometry/Twist3d.h" +#include "units/angle.h" +#include "units/length.h" + +using namespace wpi::java; + +extern "C" { + +/* + * Class: edu_wpi_first_math_jni_Transform3dJNI + * Method: log + * Signature: (DDDDDDD)[D + */ +JNIEXPORT jdoubleArray JNICALL +Java_edu_wpi_first_math_jni_Transform3dJNI_log + (JNIEnv* env, jclass, jdouble relX, jdouble relY, jdouble relZ, jdouble relQw, + jdouble relQx, jdouble relQy, jdouble relQz) +{ + frc::Transform3d transform3d{ + units::meter_t{relX}, units::meter_t{relY}, units::meter_t{relZ}, + frc::Rotation3d{frc::Quaternion{relQw, relQx, relQy, relQz}}}; + + frc::Twist3d result = transform3d.Log(); + + return MakeJDoubleArray( + env, {{result.dx.value(), result.dy.value(), result.dz.value(), + result.rx.value(), result.ry.value(), result.rz.value()}}); +} + +} // extern "C" diff --git a/wpimath/src/main/native/cpp/jni/Twist3dJNI.cpp b/wpimath/src/main/native/cpp/jni/Twist3dJNI.cpp new file mode 100644 index 0000000000..4cf5afa367 --- /dev/null +++ b/wpimath/src/main/native/cpp/jni/Twist3dJNI.cpp @@ -0,0 +1,42 @@ +// 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. + +#include + +#include + +#include "edu_wpi_first_math_jni_Twist3dJNI.h" +#include "frc/geometry/Transform3d.h" +#include "frc/geometry/Twist3d.h" +#include "units/angle.h" +#include "units/length.h" + +using namespace wpi::java; + +extern "C" { + +/* + * Class: edu_wpi_first_math_jni_Twist3dJNI + * Method: exp + * Signature: (DDDDDD)[D + */ +JNIEXPORT jdoubleArray JNICALL +Java_edu_wpi_first_math_jni_Twist3dJNI_exp + (JNIEnv* env, jclass, jdouble twistDx, jdouble twistDy, jdouble twistDz, + jdouble twistRx, jdouble twistRy, jdouble twistRz) +{ + frc::Twist3d twist{units::meter_t{twistDx}, units::meter_t{twistDy}, + units::meter_t{twistDz}, units::radian_t{twistRx}, + units::radian_t{twistRy}, units::radian_t{twistRz}}; + + frc::Transform3d result = twist.Exp(); + + const auto& resultQuaternion = result.Rotation().GetQuaternion(); + return MakeJDoubleArray( + env, {{result.X().value(), result.Y().value(), result.Z().value(), + resultQuaternion.W(), resultQuaternion.X(), resultQuaternion.Y(), + resultQuaternion.Z()}}); +} + +} // extern "C" diff --git a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h index 9c3bc4d6de..46d124f7de 100644 --- a/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h +++ b/wpimath/src/main/native/include/frc/estimator/PoseEstimator.h @@ -263,7 +263,7 @@ class WPILIB_DLLEXPORT PoseEstimator { // Step 4: Measure the twist between the old pose estimate and the vision // pose. - auto twist = visionSample.value().Log(visionRobotPose); + auto twist = (visionRobotPose - visionSample.value()).Log(); // Step 5: We should not trust the twist entirely, so instead we scale this // twist by a Kalman gain matrix representing how much we trust vision @@ -278,7 +278,8 @@ class WPILIB_DLLEXPORT PoseEstimator { units::radian_t{k_times_twist(2)}}; // Step 7: Calculate and record the vision update. - VisionUpdate visionUpdate{visionSample->Exp(scaledTwist), *odometrySample}; + VisionUpdate visionUpdate{visionSample.value() + scaledTwist.Exp(), + *odometrySample}; m_visionUpdates[timestamp] = visionUpdate; // Step 8: Remove later vision measurements. (Matches previous behavior) diff --git a/wpimath/src/main/native/include/frc/estimator/PoseEstimator3d.h b/wpimath/src/main/native/include/frc/estimator/PoseEstimator3d.h index bc8bdb08a6..68e99dd4f2 100644 --- a/wpimath/src/main/native/include/frc/estimator/PoseEstimator3d.h +++ b/wpimath/src/main/native/include/frc/estimator/PoseEstimator3d.h @@ -272,7 +272,7 @@ class WPILIB_DLLEXPORT PoseEstimator3d { // Step 4: Measure the twist between the old pose estimate and the vision // pose. - auto twist = visionSample.value().Log(visionRobotPose); + auto twist = (visionRobotPose - visionSample.value()).Log(); // Step 5: We should not trust the twist entirely, so instead we scale this // twist by a Kalman gain matrix representing how much we trust vision @@ -289,7 +289,8 @@ class WPILIB_DLLEXPORT PoseEstimator3d { units::radian_t{k_times_twist(4)}, units::radian_t{k_times_twist(5)}}; // Step 7: Calculate and record the vision update. - VisionUpdate visionUpdate{visionSample->Exp(scaledTwist), *odometrySample}; + VisionUpdate visionUpdate{visionSample.value() + scaledTwist.Exp(), + *odometrySample}; m_visionUpdates[timestamp] = visionUpdate; // Step 8: Remove later vision measurements. (Matches previous behavior) diff --git a/wpimath/src/main/native/include/frc/geometry/Pose2d.h b/wpimath/src/main/native/include/frc/geometry/Pose2d.h index d488e312a7..17328e8554 100644 --- a/wpimath/src/main/native/include/frc/geometry/Pose2d.h +++ b/wpimath/src/main/native/include/frc/geometry/Pose2d.h @@ -15,7 +15,6 @@ #include "frc/geometry/Rotation2d.h" #include "frc/geometry/Translation2d.h" -#include "frc/geometry/Twist2d.h" #include "units/length.h" namespace frc { @@ -197,39 +196,6 @@ class WPILIB_DLLEXPORT Pose2d { return {m_translation.RotateAround(point, rot), m_rotation.RotateBy(rot)}; } - /** - * Obtain a new Pose2d from a (constant curvature) velocity. - * - * See https://file.tavsys.net/control/controls-engineering-in-frc.pdf section - * 10.2 "Pose exponential" for a derivation. - * - * The twist is a change in pose in the robot's coordinate frame since the - * previous pose update. When the user runs exp() on the previous known - * field-relative pose with the argument being the twist, the user will - * receive the new field-relative pose. - * - * "Exp" represents the pose exponential, which is solving a differential - * equation moving the pose forward in time. - * - * @param twist The change in pose in the robot's coordinate frame since the - * previous pose update. For example, if a non-holonomic robot moves forward - * 0.01 meters and changes angle by 0.5 degrees since the previous pose - * update, the twist would be Twist2d{0.01_m, 0_m, 0.5_deg}. - * - * @return The new pose of the robot. - */ - constexpr Pose2d Exp(const Twist2d& twist) const; - - /** - * Returns a Twist2d that maps this pose to the end pose. If c is the output - * of a.Log(b), then a.Exp(c) would yield b. - * - * @param end The end pose for the transformation. - * - * @return The twist that maps this to end. - */ - constexpr Twist2d Log(const Pose2d& end) const; - /** * Returns an affine transformation matrix representation of this pose. */ @@ -328,51 +294,4 @@ constexpr Pose2d Pose2d::RelativeTo(const Pose2d& other) const { return {transform.Translation(), transform.Rotation()}; } -constexpr Pose2d Pose2d::Exp(const Twist2d& twist) const { - const auto dx = twist.dx; - const auto dy = twist.dy; - const auto dtheta = twist.dtheta.value(); - - const auto sinTheta = gcem::sin(dtheta); - const auto cosTheta = gcem::cos(dtheta); - - double s, c; - if (gcem::abs(dtheta) < 1E-9) { - s = 1.0 - 1.0 / 6.0 * dtheta * dtheta; - c = 0.5 * dtheta; - } else { - s = sinTheta / dtheta; - c = (1 - cosTheta) / dtheta; - } - - const Transform2d transform{Translation2d{dx * s - dy * c, dx * c + dy * s}, - Rotation2d{cosTheta, sinTheta}}; - - return *this + transform; -} - -constexpr Twist2d Pose2d::Log(const Pose2d& end) const { - const auto transform = end.RelativeTo(*this); - const auto dtheta = transform.Rotation().Radians().value(); - const auto halfDtheta = dtheta / 2.0; - - const auto cosMinusOne = transform.Rotation().Cos() - 1; - - double halfThetaByTanOfHalfDtheta; - - if (gcem::abs(cosMinusOne) < 1E-9) { - halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; - } else { - halfThetaByTanOfHalfDtheta = - -(halfDtheta * transform.Rotation().Sin()) / cosMinusOne; - } - - const Translation2d translationPart = - transform.Translation().RotateBy( - {halfThetaByTanOfHalfDtheta, -halfDtheta}) * - gcem::hypot(halfThetaByTanOfHalfDtheta, halfDtheta); - - return {translationPart.X(), translationPart.Y(), units::radian_t{dtheta}}; -} - } // namespace frc diff --git a/wpimath/src/main/native/include/frc/geometry/Pose3d.h b/wpimath/src/main/native/include/frc/geometry/Pose3d.h index e2ec7c4609..ebf4de6827 100644 --- a/wpimath/src/main/native/include/frc/geometry/Pose3d.h +++ b/wpimath/src/main/native/include/frc/geometry/Pose3d.h @@ -19,7 +19,6 @@ #include "frc/geometry/Pose2d.h" #include "frc/geometry/Rotation3d.h" #include "frc/geometry/Translation3d.h" -#include "frc/geometry/Twist3d.h" namespace frc { @@ -223,37 +222,6 @@ class WPILIB_DLLEXPORT Pose3d { return {m_translation.RotateAround(point, rot), m_rotation.RotateBy(rot)}; } - /** - * Obtain a new Pose3d from a (constant curvature) velocity. - * - * The twist is a change in pose in the robot's coordinate frame since the - * previous pose update. When the user runs exp() on the previous known - * field-relative pose with the argument being the twist, the user will - * receive the new field-relative pose. - * - * "Exp" represents the pose exponential, which is solving a differential - * equation moving the pose forward in time. - * - * @param twist The change in pose in the robot's coordinate frame since the - * previous pose update. For example, if a non-holonomic robot moves forward - * 0.01 meters and changes angle by 0.5 degrees since the previous pose - * update, the twist would be Twist3d{0.01_m, 0_m, 0_m, Rotation3d{0.0, 0.0, - * 0.5_deg}}. - * - * @return The new pose of the robot. - */ - constexpr Pose3d Exp(const Twist3d& twist) const; - - /** - * Returns a Twist3d that maps this pose to the end pose. If c is the output - * of a.Log(b), then a.Exp(c) would yield b. - * - * @param end The end pose for the transformation. - * - * @return The twist that maps this to end. - */ - constexpr Twist3d Log(const Pose3d& end) const; - /** * Returns an affine transformation matrix representation of this pose. */ @@ -336,56 +304,10 @@ void from_json(const wpi::json& json, Pose3d& pose); } // namespace frc -#include "frc/geometry/proto/Pose3dProto.h" -#include "frc/geometry/struct/Pose3dStruct.h" - #include "frc/geometry/Transform3d.h" namespace frc { -namespace detail { - -/** - * Applies the hat operator to a rotation vector. - * - * It takes a rotation vector and returns the corresponding matrix - * representation of the Lie algebra element (a 3x3 rotation matrix). - * - * @param rotation The rotation vector. - * @return The rotation vector as a 3x3 rotation matrix. - */ -constexpr ct_matrix3d RotationVectorToMatrix(const ct_vector3d& rotation) { - // Given a rotation vector , - // [ 0 -c b] - // Omega = [ c 0 -a] - // [-b a 0] - return ct_matrix3d{{0.0, -rotation(2), rotation(1)}, - {rotation(2), 0.0, -rotation(0)}, - {-rotation(1), rotation(0), 0.0}}; -} - -/** - * Applies the hat operator to a rotation vector. - * - * It takes a rotation vector and returns the corresponding matrix - * representation of the Lie algebra element (a 3x3 rotation matrix). - * - * @param rotation The rotation vector. - * @return The rotation vector as a 3x3 rotation matrix. - */ -constexpr Eigen::Matrix3d RotationVectorToMatrix( - const Eigen::Vector3d& rotation) { - // Given a rotation vector , - // [ 0 -c b] - // Omega = [ c 0 -a] - // [-b a 0] - return Eigen::Matrix3d{{0.0, -rotation(2), rotation(1)}, - {rotation(2), 0.0, -rotation(0)}, - {-rotation(1), rotation(0), 0.0}}; -} - -} // namespace detail - constexpr Transform3d Pose3d::operator-(const Pose3d& other) const { const auto pose = this->RelativeTo(other); return Transform3d{pose.Translation(), pose.Rotation()}; @@ -401,121 +323,7 @@ constexpr Pose3d Pose3d::RelativeTo(const Pose3d& other) const { return {transform.Translation(), transform.Rotation()}; } -constexpr Pose3d Pose3d::Exp(const Twist3d& twist) const { - // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf - - auto impl = [this]( - const Twist3d& twist) -> Pose3d { - Vector3d u{{twist.dx.value(), twist.dy.value(), twist.dz.value()}}; - Vector3d rvec{{twist.rx.value(), twist.ry.value(), twist.rz.value()}}; - Matrix3d omega = detail::RotationVectorToMatrix(rvec); - Matrix3d omegaSq = omega * omega; - double theta = rvec.norm(); - double thetaSq = theta * theta; - - double A; - double B; - double C; - if (gcem::abs(theta) < 1E-7) { - // Taylor Expansions around θ = 0 - // A = 1/1! - θ²/3! + θ⁴/5! - // B = 1/2! - θ²/4! + θ⁴/6! - // C = 1/3! - θ²/5! + θ⁴/7! - // sources: - // A: - // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0 - // B: - // 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 - // C: - // 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 - A = 1 - thetaSq / 6 + thetaSq * thetaSq / 120; - B = 1 / 2.0 - thetaSq / 24 + thetaSq * thetaSq / 720; - C = 1 / 6.0 - thetaSq / 120 + thetaSq * thetaSq / 5040; - } else { - // A = std::sin(θ)/θ - // B = (1 - std::cos(θ)) / θ² - // C = (1 - A) / θ² - A = gcem::sin(theta) / theta; - B = (1 - gcem::cos(theta)) / thetaSq; - C = (1 - A) / thetaSq; - } - - Matrix3d R = Matrix3d::Identity() + A * omega + B * omegaSq; - Matrix3d V = Matrix3d::Identity() + B * omega + C * omegaSq; - - Vector3d translation_component = V * u; - - const Transform3d transform{ - Translation3d{units::meter_t{translation_component(0)}, - units::meter_t{translation_component(1)}, - units::meter_t{translation_component(2)}}, - Rotation3d{R}}; - - return *this + transform; - }; - - if (std::is_constant_evaluated()) { - return impl.template operator()(twist); - } else { - return impl.template operator()(twist); - } -} - -constexpr Twist3d Pose3d::Log(const Pose3d& end) const { - // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf - - auto impl = [this]( - const Pose3d& end) -> Twist3d { - const auto transform = end.RelativeTo(*this); - - Vector3d u{ - {transform.X().value(), transform.Y().value(), transform.Z().value()}}; - Vector3d rvec = transform.Rotation().ToVector(); - Matrix3d omega = detail::RotationVectorToMatrix(rvec); - Matrix3d omegaSq = omega * omega; - double theta = rvec.norm(); - double thetaSq = theta * theta; - - double C; - if (gcem::abs(theta) < 1E-7) { - // Taylor Expansions around θ = 0 - // A = 1/1! - θ²/3! + θ⁴/5! - // B = 1/2! - θ²/4! + θ⁴/6! - // C = 1/6 * (1/2 + θ²/5! + θ⁴/7!) - // sources: - // A: - // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0 - // B: - // 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 - // C: - // 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 - C = 1 / 12.0 + thetaSq / 720 + thetaSq * thetaSq / 30240; - } else { - // A = std::sin(θ)/θ - // B = (1 - std::cos(θ)) / θ² - // C = (1 - A/(2*B)) / θ² - double A = gcem::sin(theta) / theta; - double B = (1 - gcem::cos(theta)) / thetaSq; - C = (1 - A / (2 * B)) / thetaSq; - } - - Matrix3d V_inv = Matrix3d::Identity() - 0.5 * omega + C * omegaSq; - - Vector3d translation_component = V_inv * u; - - return Twist3d{units::meter_t{translation_component(0)}, - units::meter_t{translation_component(1)}, - units::meter_t{translation_component(2)}, - units::radian_t{rvec(0)}, - units::radian_t{rvec(1)}, - units::radian_t{rvec(2)}}; - }; - - if (std::is_constant_evaluated()) { - return impl.template operator()(end); - } else { - return impl.template operator()(end); - } -} - } // namespace frc + +#include "frc/geometry/proto/Pose3dProto.h" +#include "frc/geometry/struct/Pose3dStruct.h" diff --git a/wpimath/src/main/native/include/frc/geometry/Transform2d.h b/wpimath/src/main/native/include/frc/geometry/Transform2d.h index 88c00550d0..d85abb4ded 100644 --- a/wpimath/src/main/native/include/frc/geometry/Transform2d.h +++ b/wpimath/src/main/native/include/frc/geometry/Transform2d.h @@ -14,6 +14,7 @@ namespace frc { class Pose2d; +struct Twist2d; /** * Represents a transformation for a Pose2d in the pose's frame. @@ -109,6 +110,14 @@ class WPILIB_DLLEXPORT Transform2d { */ constexpr const Rotation2d& Rotation() const { return m_rotation; } + /** + * Returns a Twist2d of the current transform (pose delta). If b is the output + * of {@code a.Log()}, then {@code b.Exp()} would yield a. + * + * @return The twist that maps the current transform. + */ + constexpr Twist2d Log() const; + /** * Invert the transformation. This is useful for undoing a transformation. * @@ -163,6 +172,7 @@ class WPILIB_DLLEXPORT Transform2d { } // namespace frc #include "frc/geometry/Pose2d.h" +#include "frc/geometry/Twist2d.h" namespace frc { @@ -180,6 +190,27 @@ constexpr Transform2d Transform2d::operator+(const Transform2d& other) const { return Transform2d{Pose2d{}, Pose2d{}.TransformBy(*this).TransformBy(other)}; } +constexpr Twist2d Transform2d::Log() const { + const auto dtheta = m_rotation.Radians().value(); + const auto halfDtheta = dtheta / 2.0; + + const auto cosMinusOne = m_rotation.Cos() - 1; + + double halfThetaByTanOfHalfDtheta; + + if (gcem::abs(cosMinusOne) < 1E-9) { + halfThetaByTanOfHalfDtheta = 1.0 - 1.0 / 12.0 * dtheta * dtheta; + } else { + halfThetaByTanOfHalfDtheta = -(halfDtheta * m_rotation.Sin()) / cosMinusOne; + } + + const Translation2d translationPart = + m_translation.RotateBy({halfThetaByTanOfHalfDtheta, -halfDtheta}) * + gcem::hypot(halfThetaByTanOfHalfDtheta, halfDtheta); + + return {translationPart.X(), translationPart.Y(), units::radian_t{dtheta}}; +} + } // namespace frc #include "frc/geometry/proto/Transform2dProto.h" diff --git a/wpimath/src/main/native/include/frc/geometry/Transform3d.h b/wpimath/src/main/native/include/frc/geometry/Transform3d.h index bd6ca9c41e..b424ddf48c 100644 --- a/wpimath/src/main/native/include/frc/geometry/Transform3d.h +++ b/wpimath/src/main/native/include/frc/geometry/Transform3d.h @@ -14,6 +14,7 @@ namespace frc { class Pose3d; +struct Twist3d; /** * Represents a transformation for a Pose3d in the pose's frame. @@ -134,6 +135,14 @@ class WPILIB_DLLEXPORT Transform3d { */ constexpr const Rotation3d& Rotation() const { return m_rotation; } + /** + * Returns a Twist3d of the current transform (pose delta). If b is the output + * of {@code a.Log()}, then {@code b.Exp()} would yield a. + * + * @return The twist that maps the current transform. + */ + constexpr Twist3d Log() const; + /** * Invert the transformation. This is useful for undoing a transformation. * @@ -188,6 +197,8 @@ class WPILIB_DLLEXPORT Transform3d { } // namespace frc #include "frc/geometry/Pose3d.h" +#include "frc/geometry/Twist3d.h" +#include "frc/geometry/detail/RotationVectorToMatrix.h" namespace frc { @@ -205,6 +216,59 @@ constexpr Transform3d Transform3d::operator+(const Transform3d& other) const { return Transform3d{Pose3d{}, Pose3d{}.TransformBy(*this).TransformBy(other)}; } +constexpr Twist3d Transform3d::Log() const { + // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf + + auto impl = [this]() -> Twist3d { + Vector3d u{{m_translation.X().value(), m_translation.Y().value(), + m_translation.Z().value()}}; + Vector3d rvec = m_rotation.ToVector(); + Matrix3d omega = detail::RotationVectorToMatrix(rvec); + Matrix3d omegaSq = omega * omega; + double theta = rvec.norm(); + double thetaSq = theta * theta; + + double C; + if (gcem::abs(theta) < 1E-7) { + // Taylor Expansions around θ = 0 + // A = 1/1! - θ²/3! + θ⁴/5! + // B = 1/2! - θ²/4! + θ⁴/6! + // C = 1/6 * (1/2 + θ²/5! + θ⁴/7!) + // sources: + // A: + // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0 + // B: + // 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 + // C: + // 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 + C = 1 / 12.0 + thetaSq / 720 + thetaSq * thetaSq / 30240; + } else { + // A = std::sin(θ)/θ + // B = (1 - std::cos(θ)) / θ² + // C = (1 - A/(2*B)) / θ² + double A = gcem::sin(theta) / theta; + double B = (1 - gcem::cos(theta)) / thetaSq; + C = (1 - A / (2 * B)) / thetaSq; + } + + Matrix3d V_inv = Matrix3d::Identity() - 0.5 * omega + C * omegaSq; + + Vector3d translation_component = V_inv * u; + + return Twist3d{units::meter_t{translation_component(0)}, + units::meter_t{translation_component(1)}, + units::meter_t{translation_component(2)}, + units::radian_t{rvec(0)}, + units::radian_t{rvec(1)}, + units::radian_t{rvec(2)}}; + }; + + if (std::is_constant_evaluated()) { + return impl.template operator()(); + } + return impl.template operator()(); +} + } // namespace frc #include "frc/geometry/proto/Transform3dProto.h" diff --git a/wpimath/src/main/native/include/frc/geometry/Twist2d.h b/wpimath/src/main/native/include/frc/geometry/Twist2d.h index 40bef42e4e..043f334c2e 100644 --- a/wpimath/src/main/native/include/frc/geometry/Twist2d.h +++ b/wpimath/src/main/native/include/frc/geometry/Twist2d.h @@ -12,6 +12,8 @@ namespace frc { +class Transform2d; + /** * A change in distance along a 2D arc since the last pose update. We can use * ideas from differential calculus to create new Pose2ds from a Twist2d and @@ -35,6 +37,24 @@ struct WPILIB_DLLEXPORT Twist2d { */ units::radian_t dtheta = 0_rad; + /** + * Obtain a new Transform2d from a (constant curvature) velocity. + * + * See "https://file.tavsys.net/control/controls-engineering-in-frc.pdf" + * Controls Engineering in the FIRST Robotics Competition section 10.2 + * "Pose exponential" for a derivation. + * + * The twist is a change in pose in the robot's coordinate frame since the + * previous pose update. When the user runs Exp() on the twist, the user will + * receive the pose delta. + * + * "Exp" represents the pose exponential, which is solving a differential + * equation moving the pose forward in time. + * + * @return The pose delta of the robot. + */ + constexpr Transform2d Exp() const; + /** * Checks equality between this Twist2d and another object. * @@ -60,5 +80,29 @@ struct WPILIB_DLLEXPORT Twist2d { } // namespace frc +#include "frc/geometry/Transform2d.h" + +namespace frc { + +constexpr Transform2d Twist2d::Exp() const { + const auto theta = dtheta.value(); + const auto sinTheta = gcem::sin(theta); + const auto cosTheta = gcem::cos(theta); + + double s, c; + if (gcem::abs(theta) < 1E-9) { + s = 1.0 - 1.0 / 6.0 * theta * theta; + c = 0.5 * theta; + } else { + s = sinTheta / theta; + c = (1 - cosTheta) / theta; + } + + return Transform2d(Translation2d{dx * s - dy * c, dx * c + dy * s}, + Rotation2d{cosTheta, sinTheta}); +} + +} // namespace frc + #include "frc/geometry/proto/Twist2dProto.h" #include "frc/geometry/struct/Twist2dStruct.h" diff --git a/wpimath/src/main/native/include/frc/geometry/Twist3d.h b/wpimath/src/main/native/include/frc/geometry/Twist3d.h index 57e6caa5e2..f7a21d51f5 100644 --- a/wpimath/src/main/native/include/frc/geometry/Twist3d.h +++ b/wpimath/src/main/native/include/frc/geometry/Twist3d.h @@ -12,6 +12,8 @@ namespace frc { +class Transform3d; + /** * A change in distance along a 3D arc since the last pose update. We can use * ideas from differential calculus to create new Pose3ds from a Twist3d and @@ -50,6 +52,24 @@ struct WPILIB_DLLEXPORT Twist3d { */ units::radian_t rz = 0_rad; + /** + * Obtain a new Transform3d from a (constant curvature) velocity. + * + * See "https://file.tavsys.net/control/controls-engineering-in-frc.pdf" + * Controls Engineering in the FIRST Robotics Competition section 10.2 + * "Pose exponential" for a derivation. + * + * The twist is a change in pose in the robot's coordinate frame since the + * previous pose update. When the user runs Exp() on the twist, the user will + * receive the pose delta. + * + * "Exp" represents the pose exponential, which is solving a differential + * equation moving the pose forward in time. + * + * @return The pose delta of the robot. + */ + constexpr Transform3d Exp() const; + /** * Checks equality between this Twist3d and another object. * @@ -79,5 +99,70 @@ struct WPILIB_DLLEXPORT Twist3d { } // namespace frc +#include "frc/geometry/Transform3d.h" +#include "frc/geometry/detail/RotationVectorToMatrix.h" + +namespace frc { + +constexpr Transform3d Twist3d::Exp() const { + // Implementation from Section 3.2 of https://ethaneade.org/lie.pdf + + auto impl = [this]() -> Transform3d { + Vector3d u{{dx.value(), dy.value(), dz.value()}}; + Vector3d rvec{{rx.value(), ry.value(), rz.value()}}; + Matrix3d omega = detail::RotationVectorToMatrix(rvec); + Matrix3d omegaSq = omega * omega; + double theta = rvec.norm(); + double thetaSq = theta * theta; + + double A; + double B; + double C; + if (gcem::abs(theta) < 1E-7) { + // Taylor Expansions around θ = 0 + // A = 1/1! - θ²/3! + θ⁴/5! + // B = 1/2! - θ²/4! + θ⁴/6! + // C = 1/3! - θ²/5! + θ⁴/7! + // sources: + // A: + // https://www.wolframalpha.com/input?i2d=true&i=series+expansion+of+Divide%5Bsin%5C%2840%29x%5C%2841%29%2Cx%5D+at+x%3D0 + // B: + // 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 + // C: + // 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 + A = 1 - thetaSq / 6 + thetaSq * thetaSq / 120; + B = 1 / 2.0 - thetaSq / 24 + thetaSq * thetaSq / 720; + C = 1 / 6.0 - thetaSq / 120 + thetaSq * thetaSq / 5040; + } else { + // A = std::sin(θ)/θ + // B = (1 - std::cos(θ)) / θ² + // C = (1 - A) / θ² + A = gcem::sin(theta) / theta; + B = (1 - gcem::cos(theta)) / thetaSq; + C = (1 - A) / thetaSq; + } + + Matrix3d R = Matrix3d::Identity() + A * omega + B * omegaSq; + Matrix3d V = Matrix3d::Identity() + B * omega + C * omegaSq; + + Vector3d translation_component = V * u; + + const Transform3d transform{ + Translation3d{units::meter_t{translation_component(0)}, + units::meter_t{translation_component(1)}, + units::meter_t{translation_component(2)}}, + Rotation3d{R}}; + + return transform; + }; + + if (std::is_constant_evaluated()) { + return impl.template operator()(); + } + return impl.template operator()(); +} + +} // namespace frc + #include "frc/geometry/proto/Twist3dProto.h" #include "frc/geometry/struct/Twist3dStruct.h" diff --git a/wpimath/src/main/native/include/frc/geometry/detail/RotationVectorToMatrix.h b/wpimath/src/main/native/include/frc/geometry/detail/RotationVectorToMatrix.h new file mode 100644 index 0000000000..7d81e0f7ce --- /dev/null +++ b/wpimath/src/main/native/include/frc/geometry/detail/RotationVectorToMatrix.h @@ -0,0 +1,26 @@ +// 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. + +#pragma once + +#include + +#include "frc/ct_matrix.h" + +namespace frc::detail { + +constexpr ct_matrix3d RotationVectorToMatrix(const ct_vector3d& rotation) { + return ct_matrix3d{{0.0, -rotation(2), rotation(1)}, + {rotation(2), 0.0, -rotation(0)}, + {-rotation(1), rotation(0), 0.0}}; +} + +constexpr Eigen::Matrix3d RotationVectorToMatrix( + const Eigen::Vector3d& rotation) { + return Eigen::Matrix3d{{0.0, -rotation(2), rotation(1)}, + {rotation(2), 0.0, -rotation(0)}, + {-rotation(1), rotation(0), 0.0}}; +} + +} // namespace frc::detail diff --git a/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h b/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h index 47bcc46b14..74824c2620 100644 --- a/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h +++ b/wpimath/src/main/native/include/frc/interpolation/TimeInterpolatableBuffer.h @@ -166,9 +166,9 @@ inline TimeInterpolatableBuffer::TimeInterpolatableBuffer( } else if (t >= 1) { return end; } else { - Twist2d twist = start.Log(end); + Twist2d twist = (end - start).Log(); Twist2d scaledTwist = twist * t; - return start.Exp(scaledTwist); + return start + scaledTwist.Exp(); } }) {} diff --git a/wpimath/src/main/native/include/frc/kinematics/ChassisSpeeds.h b/wpimath/src/main/native/include/frc/kinematics/ChassisSpeeds.h index 9b520dff6e..b35e70a545 100644 --- a/wpimath/src/main/native/include/frc/kinematics/ChassisSpeeds.h +++ b/wpimath/src/main/native/include/frc/kinematics/ChassisSpeeds.h @@ -71,11 +71,11 @@ struct WPILIB_DLLEXPORT ChassisSpeeds { constexpr ChassisSpeeds Discretize(units::second_t dt) const { // Construct the desired pose after a timestep, relative to the current // pose. The desired pose has decoupled translation and rotation. - Pose2d desiredDeltaPose{vx * dt, vy * dt, omega * dt}; + Transform2d desiredTransform{vx * dt, vy * dt, omega * dt}; // Find the chassis translation/rotation deltas in the robot frame that move // the robot from its current pose to the desired pose - auto twist = Pose2d{}.Log(desiredDeltaPose); + auto twist = desiredTransform.Log(); // Turn the chassis translation/rotation deltas into average velocities return {twist.dx / dt, twist.dy / dt, twist.dtheta / dt}; diff --git a/wpimath/src/main/native/include/frc/kinematics/Odometry.h b/wpimath/src/main/native/include/frc/kinematics/Odometry.h index c73eed2248..6ac4f1c958 100644 --- a/wpimath/src/main/native/include/frc/kinematics/Odometry.h +++ b/wpimath/src/main/native/include/frc/kinematics/Odometry.h @@ -122,7 +122,7 @@ class WPILIB_DLLEXPORT Odometry { m_kinematics.ToTwist2d(m_previousWheelPositions, wheelPositions); twist.dtheta = (angle - m_previousAngle).Radians(); - auto newPose = m_pose.Exp(twist); + auto newPose = m_pose + twist.Exp(); m_previousAngle = angle; m_previousWheelPositions = wheelPositions; diff --git a/wpimath/src/main/native/include/frc/kinematics/Odometry3d.h b/wpimath/src/main/native/include/frc/kinematics/Odometry3d.h index f24d6f86f6..0c4b819bc0 100644 --- a/wpimath/src/main/native/include/frc/kinematics/Odometry3d.h +++ b/wpimath/src/main/native/include/frc/kinematics/Odometry3d.h @@ -131,7 +131,7 @@ class WPILIB_DLLEXPORT Odometry3d { units::radian_t{angle_difference(1)}, units::radian_t{angle_difference(2)}}; - auto newPose = m_pose.Exp(twist); + auto newPose = m_pose + twist.Exp(); m_previousWheelPositions = wheelPositions; m_previousAngle = angle; diff --git a/wpimath/src/main/native/include/frc/spline/SplineParameterizer.h b/wpimath/src/main/native/include/frc/spline/SplineParameterizer.h index 46bfc7148f..cbcd9de13f 100644 --- a/wpimath/src/main/native/include/frc/spline/SplineParameterizer.h +++ b/wpimath/src/main/native/include/frc/spline/SplineParameterizer.h @@ -106,7 +106,7 @@ class WPILIB_DLLEXPORT SplineParameterizer { throw MalformedSplineException(kMalformedSplineExceptionMsg); } - const auto twist = start.value().first.Log(end.value().first); + const auto twist = (end.value().first - start.value().first).Log(); if (units::math::abs(twist.dy) > kMaxDy || units::math::abs(twist.dx) > kMaxDx || diff --git a/wpimath/src/test/java/edu/wpi/first/math/controller/LTVUnicycleControllerTest.java b/wpimath/src/test/java/edu/wpi/first/math/controller/LTVUnicycleControllerTest.java index b75dcbd514..a52dc96e75 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/controller/LTVUnicycleControllerTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/controller/LTVUnicycleControllerTest.java @@ -41,7 +41,7 @@ class LTVUnicycleControllerTest { var state = trajectory.sample(kDt * i); var output = controller.calculate(robotPose, state); - robotPose = robotPose.exp(new Twist2d(output.vx * kDt, 0, output.omega * kDt)); + robotPose = robotPose.plus(new Twist2d(output.vx * kDt, 0, output.omega * kDt).exp()); } final var states = trajectory.getStates(); diff --git a/wpimath/src/test/java/edu/wpi/first/math/geometry/Pose3dTest.java b/wpimath/src/test/java/edu/wpi/first/math/geometry/Pose3dTest.java index 583e125fc0..71e61ca6c5 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/geometry/Pose3dTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/geometry/Pose3dTest.java @@ -257,8 +257,8 @@ class Pose3dTest { var start = initial_poses.get(i); var end = final_poses.get(i); - var twist = start.log(end); - var start_exp = start.exp(twist); + var twist = end.minus(start).log(); + var start_exp = start.plus(twist.exp()); assertAll( () -> assertEquals(start_exp.getX(), end.getX(), eps), @@ -314,7 +314,7 @@ class Pose3dTest { var start = initial_poses.get(i); var end = final_poses.get(i); - var twist = start.log(end); + var twist = end.minus(start).log(); assertAll( () -> assertFalse(((Double) twist.dx).isNaN()), () -> assertFalse(((Double) twist.dy).isNaN()), diff --git a/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist2dTest.java b/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist2dTest.java index 22d8f33e8f..4c8a7c3a11 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist2dTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist2dTest.java @@ -13,27 +13,27 @@ class Twist2dTest { @Test void testStraight() { var straight = new Twist2d(5.0, 0.0, 0.0); - var straightPose = Pose2d.kZero.exp(straight); + var straightPose = straight.exp(); - var expected = new Pose2d(5.0, 0.0, Rotation2d.kZero); + var expected = new Transform2d(5.0, 0.0, Rotation2d.kZero); assertEquals(expected, straightPose); } @Test void testQuarterCircle() { var quarterCircle = new Twist2d(5.0 / 2.0 * Math.PI, 0, Math.PI / 2.0); - var quarterCirclePose = Pose2d.kZero.exp(quarterCircle); + var quarterCirclePose = quarterCircle.exp(); - var expected = new Pose2d(5.0, 5.0, Rotation2d.kCCW_Pi_2); + var expected = new Transform2d(5.0, 5.0, Rotation2d.kCCW_Pi_2); assertEquals(expected, quarterCirclePose); } @Test void testDiagonalNoDtheta() { var diagonal = new Twist2d(2.0, 2.0, 0.0); - var diagonalPose = Pose2d.kZero.exp(diagonal); + var diagonalPose = diagonal.exp(); - var expected = new Pose2d(2.0, 2.0, Rotation2d.kZero); + var expected = new Transform2d(2.0, 2.0, Rotation2d.kZero); assertEquals(expected, diagonalPose); } @@ -56,13 +56,13 @@ class Twist2dTest { final var start = Pose2d.kZero; final var end = new Pose2d(5.0, 5.0, Rotation2d.kCCW_Pi_2); - final var twist = start.log(end); + final var twist = end.minus(start).log(); var expected = new Twist2d(5.0 / 2.0 * Math.PI, 0.0, Math.PI / 2.0); assertEquals(expected, twist); // Make sure computed twist gives back original end pose - final var reapplied = start.exp(twist); + final var reapplied = start.plus(twist.exp()); assertEquals(end, reapplied); } } diff --git a/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist3dTest.java b/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist3dTest.java index 03ac6100dc..016205873f 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist3dTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/geometry/Twist3dTest.java @@ -15,27 +15,27 @@ class Twist3dTest { @Test void testStraightX() { var straight = new Twist3d(5.0, 0.0, 0.0, 0.0, 0.0, 0.0); - var straightPose = Pose3d.kZero.exp(straight); + var straightPose = straight.exp(); - var expected = new Pose3d(5.0, 0.0, 0.0, Rotation3d.kZero); + var expected = new Transform3d(5.0, 0.0, 0.0, Rotation3d.kZero); assertEquals(expected, straightPose); } @Test void testStraightY() { var straight = new Twist3d(0.0, 5.0, 0.0, 0.0, 0.0, 0.0); - var straightPose = Pose3d.kZero.exp(straight); + var straightPose = straight.exp(); - var expected = new Pose3d(0.0, 5.0, 0.0, Rotation3d.kZero); + var expected = new Transform3d(0.0, 5.0, 0.0, Rotation3d.kZero); assertEquals(expected, straightPose); } @Test void testStraightZ() { var straight = new Twist3d(0.0, 0.0, 5.0, 0.0, 0.0, 0.0); - var straightPose = Pose3d.kZero.exp(straight); + var straightPose = straight.exp(); - var expected = new Pose3d(0.0, 0.0, 5.0, Rotation3d.kZero); + var expected = new Transform3d(0.0, 0.0, 5.0, Rotation3d.kZero); assertEquals(expected, straightPose); } @@ -44,18 +44,19 @@ class Twist3dTest { var zAxis = VecBuilder.fill(0.0, 0.0, 1.0); var quarterCircle = new Twist3d(5.0 / 2.0 * Math.PI, 0.0, 0.0, 0.0, 0.0, Math.PI / 2.0); - var quarterCirclePose = Pose3d.kZero.exp(quarterCircle); + var quarterCirclePose = quarterCircle.exp(); - var expected = new Pose3d(5.0, 5.0, 0.0, new Rotation3d(zAxis, Units.degreesToRadians(90.0))); + var expected = + new Transform3d(5.0, 5.0, 0.0, new Rotation3d(zAxis, Units.degreesToRadians(90.0))); assertEquals(expected, quarterCirclePose); } @Test void testDiagonalNoDtheta() { var diagonal = new Twist3d(2.0, 2.0, 0.0, 0.0, 0.0, 0.0); - var diagonalPose = Pose3d.kZero.exp(diagonal); + var diagonalPose = diagonal.exp(); - var expected = new Pose3d(2.0, 2.0, 0.0, Rotation3d.kZero); + var expected = new Transform3d(2.0, 2.0, 0.0, Rotation3d.kZero); assertEquals(expected, diagonalPose); } @@ -79,14 +80,14 @@ class Twist3dTest { final var end = new Pose3d(0.0, 5.0, 5.0, new Rotation3d(Units.degreesToRadians(90.0), 0.0, 0.0)); - final var twist = start.log(end); + final var twist = end.minus(start).log(); var expected = new Twist3d(0.0, 5.0 / 2.0 * Math.PI, 0.0, Units.degreesToRadians(90.0), 0.0, 0.0); assertEquals(expected, twist); // Make sure computed twist gives back original end pose - final var reapplied = start.exp(twist); + final var reapplied = start.plus(twist.exp()); assertEquals(end, reapplied); } @@ -96,13 +97,13 @@ class Twist3dTest { final var end = new Pose3d(5.0, 0.0, 5.0, new Rotation3d(0.0, Units.degreesToRadians(90.0), 0.0)); - final var twist = start.log(end); + final var twist = end.minus(start).log(); var expected = new Twist3d(0.0, 0.0, 5.0 / 2.0 * Math.PI, 0.0, Math.PI / 2.0, 0.0); assertEquals(expected, twist); // Make sure computed twist gives back original end pose - final var reapplied = start.exp(twist); + final var reapplied = start.plus(twist.exp()); assertEquals(end, reapplied); } @@ -112,13 +113,13 @@ class Twist3dTest { final var end = new Pose3d(5.0, 5.0, 0.0, new Rotation3d(0.0, 0.0, Units.degreesToRadians(90.0))); - final var twist = start.log(end); + final var twist = end.minus(start).log(); var expected = new Twist3d(5.0 / 2.0 * Math.PI, 0.0, 0.0, 0.0, 0.0, Math.PI / 2.0); assertEquals(expected, twist); // Make sure computed twist gives back original end pose - final var reapplied = start.exp(twist); + final var reapplied = start.plus(twist.exp()); assertEquals(end, reapplied); } } diff --git a/wpimath/src/test/java/edu/wpi/first/math/jni/Transform3dJNITest.java b/wpimath/src/test/java/edu/wpi/first/math/jni/Transform3dJNITest.java new file mode 100644 index 0000000000..0c4cad7e4a --- /dev/null +++ b/wpimath/src/test/java/edu/wpi/first/math/jni/Transform3dJNITest.java @@ -0,0 +1,16 @@ +// 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.math.jni; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +public class Transform3dJNITest { + @Test + public void testLink() { + assertDoesNotThrow(Transform3dJNI::forceLoad); + } +} diff --git a/wpimath/src/test/java/edu/wpi/first/math/jni/Pose3dJNITest.java b/wpimath/src/test/java/edu/wpi/first/math/jni/Twist3dJNITest.java similarity index 83% rename from wpimath/src/test/java/edu/wpi/first/math/jni/Pose3dJNITest.java rename to wpimath/src/test/java/edu/wpi/first/math/jni/Twist3dJNITest.java index d7f90a98e2..391f11f2b4 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/jni/Pose3dJNITest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/jni/Twist3dJNITest.java @@ -8,9 +8,9 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import org.junit.jupiter.api.Test; -public class Pose3dJNITest { +public class Twist3dJNITest { @Test public void testLink() { - assertDoesNotThrow(Pose3dJNI::forceLoad); + assertDoesNotThrow(Twist3dJNI::forceLoad); } } diff --git a/wpimath/src/test/java/edu/wpi/first/math/kinematics/ChassisSpeedsTest.java b/wpimath/src/test/java/edu/wpi/first/math/kinematics/ChassisSpeedsTest.java index 38ac1ce249..1101da623d 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/kinematics/ChassisSpeedsTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/kinematics/ChassisSpeedsTest.java @@ -28,7 +28,7 @@ class ChassisSpeedsTest { var pose = Pose2d.kZero; for (double time = 0; time < duration; time += dt) { - pose = pose.exp(twist); + pose = pose.plus(twist.exp()); } final var result = pose; // For lambda capture diff --git a/wpimath/src/test/java/edu/wpi/first/math/spline/CubicHermiteSplineTest.java b/wpimath/src/test/java/edu/wpi/first/math/spline/CubicHermiteSplineTest.java index f613abcc6c..e0d7ca3b60 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/spline/CubicHermiteSplineTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/spline/CubicHermiteSplineTest.java @@ -53,7 +53,7 @@ class CubicHermiteSplineTest { var p1 = poses.get(i + 1); // Make sure the twist is under the tolerance defined by the Spline class. - var twist = p0.pose.log(p1.pose); + var twist = p1.pose.minus(p0.pose).log(); assertAll( () -> assertTrue(Math.abs(twist.dx) < kMaxDx), () -> assertTrue(Math.abs(twist.dy) < kMaxDy), @@ -62,11 +62,13 @@ class CubicHermiteSplineTest { // Check first point assertAll( - () -> assertEquals(a.getX(), poses.get(0).pose.getX(), 1E-9), - () -> assertEquals(a.getY(), poses.get(0).pose.getY(), 1E-9), + () -> assertEquals(a.getX(), poses.getFirst().pose.getX(), 1E-9), + () -> assertEquals(a.getY(), poses.getFirst().pose.getY(), 1E-9), () -> assertEquals( - a.getRotation().getRadians(), poses.get(0).pose.getRotation().getRadians(), 1E-9)); + a.getRotation().getRadians(), + poses.getFirst().pose.getRotation().getRadians(), + 1E-9)); // Check interior waypoints boolean interiorsGood = true; @@ -84,12 +86,12 @@ class CubicHermiteSplineTest { // Check last point assertAll( - () -> assertEquals(b.getX(), poses.get(poses.size() - 1).pose.getX(), 1E-9), - () -> assertEquals(b.getY(), poses.get(poses.size() - 1).pose.getY(), 1E-9), + () -> assertEquals(b.getX(), poses.getLast().pose.getX(), 1E-9), + () -> assertEquals(b.getY(), poses.getLast().pose.getY(), 1E-9), () -> assertEquals( b.getRotation().getRadians(), - poses.get(poses.size() - 1).pose.getRotation().getRadians(), + poses.getLast().pose.getRotation().getRadians(), 1E-9)); } diff --git a/wpimath/src/test/java/edu/wpi/first/math/spline/QuinticHermiteSplineTest.java b/wpimath/src/test/java/edu/wpi/first/math/spline/QuinticHermiteSplineTest.java index de8835f20e..d5c0559224 100644 --- a/wpimath/src/test/java/edu/wpi/first/math/spline/QuinticHermiteSplineTest.java +++ b/wpimath/src/test/java/edu/wpi/first/math/spline/QuinticHermiteSplineTest.java @@ -39,7 +39,7 @@ class QuinticHermiteSplineTest { var p1 = poses.get(i + 1); // Make sure the twist is under the tolerance defined by the Spline class. - var twist = p0.pose.log(p1.pose); + var twist = p1.pose.minus(p0.pose).log(); assertAll( () -> assertTrue(Math.abs(twist.dx) < kMaxDx), () -> assertTrue(Math.abs(twist.dy) < kMaxDy), @@ -48,20 +48,22 @@ class QuinticHermiteSplineTest { // Check first point assertAll( - () -> assertEquals(a.getX(), poses.get(0).pose.getX(), 1E-9), - () -> assertEquals(a.getY(), poses.get(0).pose.getY(), 1E-9), + () -> assertEquals(a.getX(), poses.getFirst().pose.getX(), 1E-9), + () -> assertEquals(a.getY(), poses.getFirst().pose.getY(), 1E-9), () -> assertEquals( - a.getRotation().getRadians(), poses.get(0).pose.getRotation().getRadians(), 1E-9)); + a.getRotation().getRadians(), + poses.getFirst().pose.getRotation().getRadians(), + 1E-9)); // Check last point assertAll( - () -> assertEquals(b.getX(), poses.get(poses.size() - 1).pose.getX(), 1E-9), - () -> assertEquals(b.getY(), poses.get(poses.size() - 1).pose.getY(), 1E-9), + () -> assertEquals(b.getX(), poses.getLast().pose.getX(), 1E-9), + () -> assertEquals(b.getY(), poses.getLast().pose.getY(), 1E-9), () -> assertEquals( b.getRotation().getRadians(), - poses.get(poses.size() - 1).pose.getRotation().getRadians(), + poses.getLast().pose.getRotation().getRadians(), 1E-9)); } diff --git a/wpimath/src/test/native/cpp/controller/LTVUnicycleControllerTest.cpp b/wpimath/src/test/native/cpp/controller/LTVUnicycleControllerTest.cpp index 93b4e74c42..bb7914dc51 100644 --- a/wpimath/src/test/native/cpp/controller/LTVUnicycleControllerTest.cpp +++ b/wpimath/src/test/native/cpp/controller/LTVUnicycleControllerTest.cpp @@ -33,7 +33,7 @@ TEST(LTVUnicycleControllerTest, ReachesReference) { auto [vx, vy, omega] = controller.Calculate(robotPose, state); static_cast(vy); - robotPose = robotPose.Exp(frc::Twist2d{vx * kDt, 0_m, omega * kDt}); + robotPose = robotPose + frc::Twist2d{vx * kDt, 0_m, omega * kDt}.Exp(); } auto& endPose = trajectory.States().back().pose; diff --git a/wpimath/src/test/native/cpp/geometry/Pose3dTest.cpp b/wpimath/src/test/native/cpp/geometry/Pose3dTest.cpp index 7a84718ea1..045870c792 100644 --- a/wpimath/src/test/native/cpp/geometry/Pose3dTest.cpp +++ b/wpimath/src/test/native/cpp/geometry/Pose3dTest.cpp @@ -170,8 +170,8 @@ TEST(Pose3dTest, ComplexTwists) { auto start = initial_poses[i]; auto end = final_poses[i]; - auto twist = start.Log(end); - auto start_exp = start.Exp(twist); + auto twist = (end - start).Log(); + auto start_exp = start + twist.Exp(); auto eps = 1E-5; @@ -211,7 +211,7 @@ TEST(Pose3dTest, TwistNaN) { for (size_t i = 0; i < initial_poses.size(); i++) { auto start = initial_poses[i]; auto end = final_poses[i]; - auto twist = start.Log(end); + auto twist = (end - start).Log(); EXPECT_FALSE(std::isnan(twist.dx.value())); EXPECT_FALSE(std::isnan(twist.dy.value())); diff --git a/wpimath/src/test/native/cpp/geometry/Twist2dTest.cpp b/wpimath/src/test/native/cpp/geometry/Twist2dTest.cpp index 7f121c0b1e..d4a03b7a8e 100644 --- a/wpimath/src/test/native/cpp/geometry/Twist2dTest.cpp +++ b/wpimath/src/test/native/cpp/geometry/Twist2dTest.cpp @@ -13,7 +13,7 @@ using namespace frc; TEST(Twist2dTest, Straight) { const Twist2d straight{5_m, 0_m, 0_rad}; - const auto straightPose = Pose2d{}.Exp(straight); + const auto straightPose = straight.Exp(); EXPECT_DOUBLE_EQ(5.0, straightPose.X().value()); EXPECT_DOUBLE_EQ(0.0, straightPose.Y().value()); @@ -23,7 +23,7 @@ TEST(Twist2dTest, Straight) { TEST(Twist2dTest, QuarterCircle) { const Twist2d quarterCircle{5_m / 2.0 * std::numbers::pi, 0_m, units::radian_t{std::numbers::pi / 2.0}}; - const auto quarterCirclePose = Pose2d{}.Exp(quarterCircle); + const auto quarterCirclePose = quarterCircle.Exp(); EXPECT_DOUBLE_EQ(5.0, quarterCirclePose.X().value()); EXPECT_DOUBLE_EQ(5.0, quarterCirclePose.Y().value()); @@ -32,7 +32,7 @@ TEST(Twist2dTest, QuarterCircle) { TEST(Twist2dTest, DiagonalNoDtheta) { const Twist2d diagonal{2_m, 2_m, 0_deg}; - const auto diagonalPose = Pose2d{}.Exp(diagonal); + const auto diagonalPose = diagonal.Exp(); EXPECT_DOUBLE_EQ(2.0, diagonalPose.X().value()); EXPECT_DOUBLE_EQ(2.0, diagonalPose.Y().value()); @@ -55,14 +55,14 @@ TEST(Twist2dTest, Pose2dLog) { const Pose2d end{5_m, 5_m, 90_deg}; const Pose2d start; - const auto twist = start.Log(end); + const auto twist = (end - start).Log(); Twist2d expected{units::meter_t{5.0 / 2.0 * std::numbers::pi}, 0_m, units::radian_t{std::numbers::pi / 2.0}}; EXPECT_EQ(expected, twist); // Make sure computed twist gives back original end pose - const auto reapplied = start.Exp(twist); + const auto reapplied = start + twist.Exp(); EXPECT_EQ(end, reapplied); } diff --git a/wpimath/src/test/native/cpp/geometry/Twist3dTest.cpp b/wpimath/src/test/native/cpp/geometry/Twist3dTest.cpp index 245ec17517..ddf429a8fe 100644 --- a/wpimath/src/test/native/cpp/geometry/Twist3dTest.cpp +++ b/wpimath/src/test/native/cpp/geometry/Twist3dTest.cpp @@ -13,25 +13,25 @@ using namespace frc; TEST(Twist3dTest, StraightX) { const Twist3d straight{5_m, 0_m, 0_m, 0_rad, 0_rad, 0_rad}; - const auto straightPose = Pose3d{}.Exp(straight); + const auto straightPose = straight.Exp(); - Pose3d expected{5_m, 0_m, 0_m, Rotation3d{}}; + Transform3d expected{5_m, 0_m, 0_m, Rotation3d{}}; EXPECT_EQ(expected, straightPose); } TEST(Twist3dTest, StraightY) { const Twist3d straight{0_m, 5_m, 0_m, 0_rad, 0_rad, 0_rad}; - const auto straightPose = Pose3d{}.Exp(straight); + const auto straightPose = straight.Exp(); - Pose3d expected{0_m, 5_m, 0_m, Rotation3d{}}; + Transform3d expected{0_m, 5_m, 0_m, Rotation3d{}}; EXPECT_EQ(expected, straightPose); } TEST(Twist3dTest, StraightZ) { const Twist3d straight{0_m, 0_m, 5_m, 0_rad, 0_rad, 0_rad}; - const auto straightPose = Pose3d{}.Exp(straight); + const auto straightPose = straight.Exp(); - Pose3d expected{0_m, 0_m, 5_m, Rotation3d{}}; + Transform3d expected{0_m, 0_m, 5_m, Rotation3d{}}; EXPECT_EQ(expected, straightPose); } @@ -41,17 +41,17 @@ TEST(Twist3dTest, QuarterCircle) { const Twist3d quarterCircle{ 5_m / 2.0 * std::numbers::pi, 0_m, 0_m, 0_rad, 0_rad, units::radian_t{std::numbers::pi / 2.0}}; - const auto quarterCirclePose = Pose3d{}.Exp(quarterCircle); + const auto quarterCirclePose = quarterCircle.Exp(); - Pose3d expected{5_m, 5_m, 0_m, Rotation3d{zAxis, 90_deg}}; + Transform3d expected{5_m, 5_m, 0_m, Rotation3d{zAxis, 90_deg}}; EXPECT_EQ(expected, quarterCirclePose); } TEST(Twist3dTest, DiagonalNoDtheta) { const Twist3d diagonal{2_m, 2_m, 0_m, 0_rad, 0_rad, 0_rad}; - const auto diagonalPose = Pose3d{}.Exp(diagonal); + const auto diagonalPose = diagonal.Exp(); - Pose3d expected{2_m, 2_m, 0_m, Rotation3d{}}; + Transform3d expected{2_m, 2_m, 0_m, Rotation3d{}}; EXPECT_EQ(expected, diagonalPose); } @@ -71,7 +71,7 @@ TEST(Twist3dTest, Pose3dLogX) { const Pose3d end{0_m, 5_m, 5_m, Rotation3d{90_deg, 0_deg, 0_deg}}; const Pose3d start; - const auto twist = start.Log(end); + const auto twist = (end - start).Log(); Twist3d expected{0_m, units::meter_t{5.0 / 2.0 * std::numbers::pi}, 0_m, 90_deg, @@ -79,7 +79,7 @@ TEST(Twist3dTest, Pose3dLogX) { EXPECT_EQ(expected, twist); // Make sure computed twist gives back original end pose - const auto reapplied = start.Exp(twist); + const auto reapplied = start + twist.Exp(); EXPECT_EQ(end, reapplied); } @@ -87,14 +87,14 @@ TEST(Twist3dTest, Pose3dLogY) { const Pose3d end{5_m, 0_m, 5_m, Rotation3d{0_deg, 90_deg, 0_deg}}; const Pose3d start; - const auto twist = start.Log(end); + const auto twist = (end - start).Log(); Twist3d expected{0_m, 0_m, units::meter_t{5.0 / 2.0 * std::numbers::pi}, 0_deg, 90_deg, 0_deg}; EXPECT_EQ(expected, twist); // Make sure computed twist gives back original end pose - const auto reapplied = start.Exp(twist); + const auto reapplied = start + twist.Exp(); EXPECT_EQ(end, reapplied); } @@ -102,7 +102,7 @@ TEST(Twist3dTest, Pose3dLogZ) { const Pose3d end{5_m, 5_m, 0_m, Rotation3d{0_deg, 0_deg, 90_deg}}; const Pose3d start; - const auto twist = start.Log(end); + const auto twist = (end - start).Log(); Twist3d expected{units::meter_t{5.0 / 2.0 * std::numbers::pi}, 0_m, @@ -113,7 +113,7 @@ TEST(Twist3dTest, Pose3dLogZ) { EXPECT_EQ(expected, twist); // Make sure computed twist gives back original end pose - const auto reapplied = start.Exp(twist); + const auto reapplied = start + twist.Exp(); EXPECT_EQ(end, reapplied); } diff --git a/wpimath/src/test/native/cpp/kinematics/ChassisSpeedsTest.cpp b/wpimath/src/test/native/cpp/kinematics/ChassisSpeedsTest.cpp index 993ae40654..60c76aca54 100644 --- a/wpimath/src/test/native/cpp/kinematics/ChassisSpeedsTest.cpp +++ b/wpimath/src/test/native/cpp/kinematics/ChassisSpeedsTest.cpp @@ -20,7 +20,7 @@ TEST(ChassisSpeedsTest, Discretize) { frc::Pose2d pose; for (units::second_t time = 0_s; time < duration; time += dt) { - pose = pose.Exp(twist); + pose = pose + twist.Exp(); } EXPECT_NEAR((target.vx * duration).value(), pose.X().value(), kEpsilon); diff --git a/wpimath/src/test/native/cpp/spline/CubicHermiteSplineTest.cpp b/wpimath/src/test/native/cpp/spline/CubicHermiteSplineTest.cpp index d16444cd67..744680bd03 100644 --- a/wpimath/src/test/native/cpp/spline/CubicHermiteSplineTest.cpp +++ b/wpimath/src/test/native/cpp/spline/CubicHermiteSplineTest.cpp @@ -42,7 +42,7 @@ class CubicHermiteSplineTest : public ::testing::Test { auto& p1 = poses[i + 1]; // Make sure the twist is under the tolerance defined by the Spline class. - auto twist = p0.first.Log(p1.first); + auto twist = (p1.first - p0.first).Log(); EXPECT_LT(std::abs(twist.dx.value()), SplineParameterizer::kMaxDx.value()); EXPECT_LT(std::abs(twist.dy.value()), diff --git a/wpimath/src/test/native/cpp/spline/QuinticHermiteSplineTest.cpp b/wpimath/src/test/native/cpp/spline/QuinticHermiteSplineTest.cpp index 5f80f3ad51..8a6d2653ef 100644 --- a/wpimath/src/test/native/cpp/spline/QuinticHermiteSplineTest.cpp +++ b/wpimath/src/test/native/cpp/spline/QuinticHermiteSplineTest.cpp @@ -29,7 +29,7 @@ class QuinticHermiteSplineTest : public ::testing::Test { auto& p1 = poses[i + 1]; // Make sure the twist is under the tolerance defined by the Spline class. - auto twist = p0.first.Log(p1.first); + auto twist = (p1.first - p0.first).Log(); EXPECT_LT(std::abs(twist.dx.value()), SplineParameterizer::kMaxDx.value()); EXPECT_LT(std::abs(twist.dy.value()),