blob: 32461d3540ded0853af57dfa732d1ea56b53d743 [file] [log] [blame]
#include "aos/util/trapezoid_profile.h"
#include <compare>
#include <cstdlib>
#include <memory>
#include <ratio>
#include "Eigen/Dense" // IWYU pragma: keep
#include "gtest/gtest.h"
namespace aos::util::testing {
class TrapezoidProfileTest : public ::testing::Test {
public:
EIGEN_MAKE_ALIGNED_OPERATOR_NEW;
protected:
TrapezoidProfileTest() : profile_(kDeltaTime) {
position_.setZero();
profile_.set_maximum_acceleration(0.75);
profile_.set_maximum_deceleration(0.75);
profile_.set_maximum_velocity(1.75);
}
// Runs an iteration.
void RunIteration(double goal_position, double goal_velocity) {
position_ = profile_.Update(goal_position, goal_velocity);
}
void RunFor(double goal_position, double goal_velocity,
std::chrono::nanoseconds duration) {
while (duration > std::chrono::nanoseconds(0)) {
position_ = profile_.Update(goal_position, goal_velocity);
duration -= kDeltaTime;
}
ASSERT_EQ(duration.count(), 0);
}
const Eigen::Matrix<double, 2, 1> &position() { return position_; }
AsymmetricTrapezoidProfile profile_;
::testing::AssertionResult At(double position, double velocity) {
static const double kDoubleNear = 0.00001;
if (::std::abs(velocity - position_(1)) > kDoubleNear) {
return ::testing::AssertionFailure()
<< "velocity is " << position_(1) << " not " << velocity;
}
if (::std::abs(position - position_(0)) > kDoubleNear) {
return ::testing::AssertionFailure()
<< "position is " << position_(0) << " not " << position;
}
return ::testing::AssertionSuccess()
<< "at " << position << " moving at " << velocity;
}
private:
static constexpr ::std::chrono::nanoseconds kDeltaTime =
::std::chrono::milliseconds(10);
Eigen::Matrix<double, 2, 1> position_;
};
constexpr ::std::chrono::nanoseconds TrapezoidProfileTest::kDeltaTime;
TEST_F(TrapezoidProfileTest, ReachesGoal) {
RunFor(3, 0, std::chrono::milliseconds(4500));
EXPECT_TRUE(At(3, 0));
}
// Tests that decreasing the maximum velocity in the middle when it is already
// moving faster than the new max is handled correctly.
TEST_F(TrapezoidProfileTest, ContinousUnderVelChange) {
profile_.set_maximum_velocity(1.75);
RunFor(12.0, 0, std::chrono::milliseconds(10));
double last_pos = position()(0);
double last_vel = 1.75;
for (int i = 0; i < 1600; ++i) {
if (i == 400) {
profile_.set_maximum_velocity(0.75);
}
RunFor(12.0, 0, std::chrono::milliseconds(10));
if (i >= 400) {
EXPECT_TRUE(::std::abs(last_pos - position()(0)) <= 1.75 * 0.01);
EXPECT_NEAR(last_vel, ::std::abs(last_pos - position()(0)), 0.0001);
}
last_vel = ::std::abs(last_pos - position()(0));
last_pos = position()(0);
}
EXPECT_TRUE(At(12.0, 0));
}
// There is some somewhat tricky code for dealing with going backwards.
TEST_F(TrapezoidProfileTest, Backwards) {
RunFor(-2, 0, std::chrono::milliseconds(4000));
EXPECT_TRUE(At(-2, 0));
}
TEST_F(TrapezoidProfileTest, SwitchGoalInMiddle) {
RunFor(-2, 0, std::chrono::milliseconds(2000));
EXPECT_FALSE(At(-2, 0));
RunFor(0, 0, std::chrono::milliseconds(5500));
EXPECT_TRUE(At(0, 0));
}
// Checks to make sure that it hits top speed.
TEST_F(TrapezoidProfileTest, TopSpeed) {
RunFor(4, 0, std::chrono::milliseconds(2000));
EXPECT_NEAR(1.5, position()(1), 10e-5);
RunFor(4, 0, std::chrono::milliseconds(20000));
EXPECT_TRUE(At(4, 0));
}
// Tests that the position and velocity exactly match at the end. Some code we
// have assumes this to be true as a simplification.
TEST_F(TrapezoidProfileTest, ExactlyReachesGoal) {
RunFor(1, 0, std::chrono::milliseconds(4500));
EXPECT_EQ(position()(1), 0.0);
EXPECT_EQ(position()(0), 1.0);
}
// Tests that we can move a goal without the trajectory teleporting. The goal
// needs to move to something we haven't already passed, but will blow by.
TEST_F(TrapezoidProfileTest, MoveGoal) {
profile_.set_maximum_acceleration(2.0);
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(1.0, 2.0));
RunFor(5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(3.0, 2.0));
RunFor(3.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(4.0, 0.0));
RunFor(3.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(3.5, 0.0));
}
// Tests that we can move a goal back before where we currently are without
// teleporting.
TEST_F(TrapezoidProfileTest, MoveGoalFar) {
profile_.set_maximum_acceleration(2.0);
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(1.0, 2.0));
RunFor(5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(3.0, 2.0));
RunFor(2.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(4.0, 0.0));
RunFor(2.5, 0, std::chrono::seconds(2));
EXPECT_TRUE(At(2.5, 0.0));
}
// Tests that we can move a goal without the trajectory teleporting. The goal
// needs to move to something we haven't already passed, but will blow by. Do
// this one in the negative direction.
TEST_F(TrapezoidProfileTest, MoveGoalNegative) {
profile_.set_maximum_acceleration(2.0);
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(-5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-1.0, -2.0));
RunFor(-5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-3.0, -2.0));
RunFor(-3.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-4.0, 0.0));
RunFor(-3.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-3.5, 0.0));
}
// Tests that we can move a goal back before where we currently are without
// teleporting. Do this one in the negative direction.
TEST_F(TrapezoidProfileTest, MoveGoalNegativeFar) {
profile_.set_maximum_acceleration(2.0);
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(-5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-1.0, -2.0));
RunFor(-5.0, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-3.0, -2.0));
RunFor(-2.5, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-4.0, 0.0));
RunFor(-2.5, 0, std::chrono::seconds(2));
EXPECT_TRUE(At(-2.5, 0.0));
}
// Tests that we can execute a profile with acceleration and deceleration not
// matching in magnitude.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecel) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(1.0);
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(0.5, 1.0));
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(1.5, 1.0));
RunFor(1.75, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(1.75, 0.0));
}
// Tests that we can execute a profile with acceleration and deceleration not
// matching in magnitude, and hitting saturation.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelUnconstrained) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(0.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(0.5, 1.0));
RunFor(0.75, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(0.75, 0.0));
}
// Tests that we can execute a profile with acceleration and deceleration not
// matching in magnitude, and hitting saturation.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelUnconstrainedNegative) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(2.0);
RunFor(-0.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-0.5, -1.0));
RunFor(-0.75, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(-0.75, 0.0));
}
// Tests that we can execute a profile with acceleration and deceleration not
// matching in magnitude when going in the negative direction.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelNegative) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(1.0);
RunFor(-1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-0.5, -1.0));
RunFor(-1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(-1.5, -1.0));
RunFor(-1.75, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(-1.75, 0.0));
}
// Tests that we can move the goal when an asymmetric profile is executing.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelMoveGoal) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(1.0);
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(0.5, 1.0));
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(1.5, 1.0));
RunFor(1.6, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(1.75, 0.0));
RunFor(1.6, 0, std::chrono::milliseconds(520));
RunFor(1.6, 0, std::chrono::milliseconds(2500));
EXPECT_TRUE(At(1.6, 0.0));
}
// Tests that we can move the goal when an asymmetric profile is executing in
// the negative direction.
TEST_F(TrapezoidProfileTest, AsymmetricAccelDecelMoveGoalFar) {
// Accelerates up until t=1. Will be at x=0.5
profile_.set_maximum_acceleration(1.0);
// Decelerates in t=0.5 Will take x=0.25
profile_.set_maximum_deceleration(2.0);
profile_.set_maximum_velocity(1.0);
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(0.5, 1.0));
RunFor(1.75, 0, std::chrono::seconds(1));
EXPECT_TRUE(At(1.5, 1.0));
RunFor(1.0, 0, std::chrono::milliseconds(500));
EXPECT_TRUE(At(1.75, 0.0));
RunFor(1.0, 0, std::chrono::milliseconds(2500));
EXPECT_TRUE(At(1.0, 0.0));
}
} // namespace aos::util::testing