mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-23 01:21:42 +00:00
Add trajectory generation using hermite splines (#1843)
This commit is contained in:
committed by
Peter Johnson
parent
fd612052f3
commit
457f94ba26
@@ -59,4 +59,17 @@ class Pose2dTest {
|
||||
var two = new Pose2d(0.0, 1.524, Rotation2d.fromDegrees(43.0));
|
||||
assertNotEquals(one, two);
|
||||
}
|
||||
|
||||
void testMinus() {
|
||||
var initial = new Pose2d(0.0, 0.0, Rotation2d.fromDegrees(45.0));
|
||||
var last = new Pose2d(5.0, 5.0, Rotation2d.fromDegrees(45.0));
|
||||
|
||||
final var transform = last.minus(initial);
|
||||
|
||||
assertAll(
|
||||
() -> assertEquals(transform.getTranslation().getX(), 5.0 * Math.sqrt(2.0), kEpsilon),
|
||||
() -> assertEquals(transform.getTranslation().getY(), 0.0, kEpsilon),
|
||||
() -> assertEquals(transform.getRotation().getDegrees(), 0.0, kEpsilon)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,4 +65,17 @@ class Twist2dTest {
|
||||
var two = new Twist2d(5, 1.2, 3);
|
||||
assertNotEquals(one, two);
|
||||
}
|
||||
|
||||
void testPose2dLog() {
|
||||
final var start = new Pose2d();
|
||||
final var end = new Pose2d(5.0, 5.0, Rotation2d.fromDegrees(90.0));
|
||||
|
||||
final var twist = start.log(end);
|
||||
|
||||
assertAll(
|
||||
() -> assertEquals(twist.dx, 5.0 / 2.0 * Math.PI, kEpsilon),
|
||||
() -> assertEquals(twist.dy, 0.0, kEpsilon),
|
||||
() -> assertEquals(twist.dtheta, Math.PI / 2.0, kEpsilon)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
|
||||
class CubicHermiteSplineTest {
|
||||
private static final double kMaxDx = 0.127;
|
||||
private static final double kMaxDy = 0.00127;
|
||||
private static final double kMaxDtheta = 0.0872;
|
||||
|
||||
@SuppressWarnings({"ParameterName", "PMD.UnusedLocalVariable"})
|
||||
private void run(Pose2d a, List<Translation2d> waypoints, Pose2d b) {
|
||||
// Start the timer.
|
||||
var start = System.nanoTime();
|
||||
|
||||
// Generate and parameterize the spline.
|
||||
var splines
|
||||
= SplineHelper.getCubicSplinesFromWaypoints(a, waypoints.toArray(new Translation2d[0]), b);
|
||||
var poses = new ArrayList<PoseWithCurvature>();
|
||||
|
||||
poses.add(splines[0].getPoint(0.0));
|
||||
|
||||
for (var spline : splines) {
|
||||
poses.addAll(SplineParameterizer.parameterize(spline));
|
||||
}
|
||||
|
||||
// End the timer.
|
||||
var end = System.nanoTime();
|
||||
|
||||
// Calculate the duration (used when benchmarking)
|
||||
var durationMicroseconds = (end - start) / 1000.0;
|
||||
|
||||
for (int i = 0; i < poses.size() - 1; i++) {
|
||||
var p0 = poses.get(i);
|
||||
var p1 = poses.get(i + 1);
|
||||
|
||||
// Make sure the twist is under the tolerance defined by the Spline class.
|
||||
var twist = p0.poseMeters.log(p1.poseMeters);
|
||||
assertAll(
|
||||
() -> assertTrue(Math.abs(twist.dx) < kMaxDx),
|
||||
() -> assertTrue(Math.abs(twist.dy) < kMaxDy),
|
||||
() -> assertTrue(Math.abs(twist.dtheta) < kMaxDtheta)
|
||||
);
|
||||
}
|
||||
|
||||
// Check first point
|
||||
assertAll(
|
||||
() -> assertEquals(a.getTranslation().getX(),
|
||||
poses.get(0).poseMeters.getTranslation().getX(), 1E-9),
|
||||
() -> assertEquals(a.getTranslation().getY(),
|
||||
poses.get(0).poseMeters.getTranslation().getY(), 1E-9),
|
||||
() -> assertEquals(a.getRotation().getRadians(),
|
||||
poses.get(0).poseMeters.getRotation().getRadians(), 1E-9)
|
||||
);
|
||||
|
||||
// Check last point
|
||||
assertAll(
|
||||
() -> assertEquals(b.getTranslation().getX(),
|
||||
poses.get(poses.size() - 1).poseMeters.getTranslation().getX(), 1E-9),
|
||||
() -> assertEquals(b.getTranslation().getY(),
|
||||
poses.get(poses.size() - 1).poseMeters.getTranslation().getY(), 1E-9),
|
||||
() -> assertEquals(b.getRotation().getRadians(),
|
||||
poses.get(poses.size() - 1).poseMeters.getRotation().getRadians(), 1E-9)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
@Test
|
||||
void testStraightLine() {
|
||||
run(new Pose2d(), new ArrayList<>(), new Pose2d(3, 0, new Rotation2d()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
@Test
|
||||
void testSCurve() {
|
||||
var start = new Pose2d(0, 0, Rotation2d.fromDegrees(90.0));
|
||||
ArrayList<Translation2d> waypoints = new ArrayList<>();
|
||||
waypoints.add(new Translation2d(1, 1));
|
||||
waypoints.add(new Translation2d(2, -1));
|
||||
var end = new Pose2d(3, 0, Rotation2d.fromDegrees(90.0));
|
||||
|
||||
run(start, waypoints, end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.spline;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class QuinticHermiteSplineTest {
|
||||
private static final double kMaxDx = 0.127;
|
||||
private static final double kMaxDy = 0.00127;
|
||||
private static final double kMaxDtheta = 0.0872;
|
||||
|
||||
@SuppressWarnings({"ParameterName", "PMD.UnusedLocalVariable"})
|
||||
private void run(Pose2d a, Pose2d b) {
|
||||
// Start the timer.
|
||||
var start = System.nanoTime();
|
||||
|
||||
// Generate and parameterize the spline.
|
||||
var spline = SplineHelper.getQuinticSplinesFromWaypoints(new Pose2d[]{a, b})[0];
|
||||
var poses = SplineParameterizer.parameterize(spline);
|
||||
|
||||
// End the timer.
|
||||
var end = System.nanoTime();
|
||||
|
||||
// Calculate the duration (used when benchmarking)
|
||||
var durationMicroseconds = (end - start) / 1000.0;
|
||||
|
||||
for (int i = 0; i < poses.size() - 1; i++) {
|
||||
var p0 = poses.get(i);
|
||||
var p1 = poses.get(i + 1);
|
||||
|
||||
// Make sure the twist is under the tolerance defined by the Spline class.
|
||||
var twist = p0.poseMeters.log(p1.poseMeters);
|
||||
assertAll(
|
||||
() -> assertTrue(Math.abs(twist.dx) < kMaxDx),
|
||||
() -> assertTrue(Math.abs(twist.dy) < kMaxDy),
|
||||
() -> assertTrue(Math.abs(twist.dtheta) < kMaxDtheta)
|
||||
);
|
||||
}
|
||||
|
||||
// Check first point
|
||||
assertAll(
|
||||
() -> assertEquals(a.getTranslation().getX(),
|
||||
poses.get(0).poseMeters.getTranslation().getX(), 1E-9),
|
||||
() -> assertEquals(a.getTranslation().getY(),
|
||||
poses.get(0).poseMeters.getTranslation().getY(), 1E-9),
|
||||
() -> assertEquals(a.getRotation().getRadians(),
|
||||
poses.get(0).poseMeters.getRotation().getRadians(), 1E-9)
|
||||
);
|
||||
|
||||
// Check last point
|
||||
assertAll(
|
||||
() -> assertEquals(b.getTranslation().getX(),
|
||||
poses.get(poses.size() - 1).poseMeters.getTranslation().getX(), 1E-9),
|
||||
() -> assertEquals(b.getTranslation().getY(),
|
||||
poses.get(poses.size() - 1).poseMeters.getTranslation().getY(), 1E-9),
|
||||
() -> assertEquals(b.getRotation().getRadians(),
|
||||
poses.get(poses.size() - 1).poseMeters.getRotation().getRadians(), 1E-9)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
@Test
|
||||
void testStraightLine() {
|
||||
run(new Pose2d(), new Pose2d(3, 0, new Rotation2d()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
@Test
|
||||
void testSimpleSCurve() {
|
||||
run(new Pose2d(), new Pose2d(1, 1, new Rotation2d()));
|
||||
}
|
||||
|
||||
@SuppressWarnings("PMD.JUnitTestsShouldIncludeAssert")
|
||||
@Test
|
||||
void testSquiggly() {
|
||||
run(new Pose2d(0, 0, Rotation2d.fromDegrees(90)),
|
||||
new Pose2d(-1, 0, Rotation2d.fromDegrees(90)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.CentripetalAccelerationConstraint;
|
||||
import edu.wpi.first.wpilibj.util.Units;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CentripetalAccelerationConstraintTest {
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
@Test
|
||||
void testCentripetalAccelerationConstraint() {
|
||||
double maxCentripetalAcceleration = Units.feetToMeters(7.0); // 7 feet per second squared
|
||||
var constraint = new CentripetalAccelerationConstraint(maxCentripetalAcceleration);
|
||||
|
||||
Trajectory trajectory = TrajectoryGeneratorTest.getTrajectory(
|
||||
Collections.singletonList(constraint));
|
||||
|
||||
var duration = trajectory.getTotalTimeSeconds();
|
||||
var t = 0.0;
|
||||
var dt = 0.02;
|
||||
|
||||
while (t < duration) {
|
||||
var point = trajectory.sample(t);
|
||||
var centripetalAcceleration
|
||||
= Math.pow(point.velocityMetersPerSecond, 2) * point.curvatureRadPerMeter;
|
||||
|
||||
t += dt;
|
||||
assertTrue(centripetalAcceleration <= maxCentripetalAcceleration + 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import edu.wpi.first.wpilibj.kinematics.ChassisSpeeds;
|
||||
import edu.wpi.first.wpilibj.kinematics.DifferentialDriveKinematics;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.DifferentialDriveKinematicsConstraint;
|
||||
import edu.wpi.first.wpilibj.util.Units;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class DifferentialDriveKinematicsConstraintTest {
|
||||
@SuppressWarnings({"LocalVariableName", "PMD.AvoidInstantiatingObjectsInLoops"})
|
||||
@Test
|
||||
void testDifferentialDriveKinematicsConstraint() {
|
||||
double maxVelocity = Units.feetToMeters(12.0); // 12 feet per second
|
||||
var kinematics = new DifferentialDriveKinematics(Units.inchesToMeters(27));
|
||||
var constraint = new DifferentialDriveKinematicsConstraint(kinematics, maxVelocity);
|
||||
|
||||
Trajectory trajectory = TrajectoryGeneratorTest.getTrajectory(
|
||||
Collections.singletonList(constraint));
|
||||
|
||||
var duration = trajectory.getTotalTimeSeconds();
|
||||
var t = 0.0;
|
||||
var dt = 0.02;
|
||||
|
||||
while (t < duration) {
|
||||
var point = trajectory.sample(t);
|
||||
var chassisSpeeds = new ChassisSpeeds(
|
||||
point.velocityMetersPerSecond, 0,
|
||||
point.velocityMetersPerSecond * point.curvatureRadPerMeter
|
||||
);
|
||||
|
||||
var wheelSpeeds = kinematics.toWheelSpeeds(chassisSpeeds);
|
||||
|
||||
t += dt;
|
||||
assertAll(
|
||||
() -> assertTrue(wheelSpeeds.leftMetersPerSecond <= maxVelocity + 0.05),
|
||||
() -> assertTrue(wheelSpeeds.rightMetersPerSecond <= maxVelocity + 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*----------------------------------------------------------------------------*/
|
||||
/* Copyright (c) 2019 FIRST. All Rights Reserved. */
|
||||
/* Open Source Software - may be modified and shared by FRC teams. The code */
|
||||
/* must be accompanied by the FIRST BSD license file in the root directory of */
|
||||
/* the project. */
|
||||
/*----------------------------------------------------------------------------*/
|
||||
|
||||
package edu.wpi.first.wpilibj.trajectory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import edu.wpi.first.wpilibj.geometry.Pose2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Rotation2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Transform2d;
|
||||
import edu.wpi.first.wpilibj.geometry.Translation2d;
|
||||
import edu.wpi.first.wpilibj.trajectory.constraint.TrajectoryConstraint;
|
||||
|
||||
import static edu.wpi.first.wpilibj.util.Units.feetToMeters;
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class TrajectoryGeneratorTest {
|
||||
static Trajectory getTrajectory(List<TrajectoryConstraint> constraints) {
|
||||
final double startVelocity = 0;
|
||||
final double endVelocity = 0;
|
||||
final double maxVelocity = feetToMeters(12.0);
|
||||
final double maxAccel = feetToMeters(12);
|
||||
|
||||
// 2018 cross scale auto waypoints.
|
||||
var sideStart = new Pose2d(feetToMeters(1.54), feetToMeters(23.23),
|
||||
Rotation2d.fromDegrees(-180));
|
||||
var crossScale = new Pose2d(feetToMeters(23.7), feetToMeters(6.8),
|
||||
Rotation2d.fromDegrees(-160));
|
||||
|
||||
var waypoints = new ArrayList<Pose2d>();
|
||||
waypoints.add(sideStart);
|
||||
waypoints.add(sideStart.plus(
|
||||
new Transform2d(new Translation2d(feetToMeters(-13), feetToMeters(0)),
|
||||
new Rotation2d())));
|
||||
waypoints.add(sideStart.plus(
|
||||
new Transform2d(new Translation2d(feetToMeters(-19.5), feetToMeters(5)),
|
||||
Rotation2d.fromDegrees(-90))));
|
||||
waypoints.add(crossScale);
|
||||
|
||||
return TrajectoryGenerator.generateTrajectory(
|
||||
waypoints,
|
||||
constraints,
|
||||
startVelocity,
|
||||
endVelocity,
|
||||
maxVelocity,
|
||||
maxAccel,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("LocalVariableName")
|
||||
void testGenerationAndConstraints() {
|
||||
Trajectory trajectory = getTrajectory(new ArrayList<>());
|
||||
|
||||
double duration = trajectory.getTotalTimeSeconds();
|
||||
double t = 0.0;
|
||||
double dt = 0.02;
|
||||
|
||||
while (t < duration) {
|
||||
var point = trajectory.sample(t);
|
||||
t += dt;
|
||||
assertAll(
|
||||
() -> assertTrue(Math.abs(point.velocityMetersPerSecond) < feetToMeters(12.0) + 0.05),
|
||||
() -> assertTrue(Math.abs(point.accelerationMetersPerSecondSq) < feetToMeters(12.0)
|
||||
+ 0.05)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user