// Copyright (c) 2022 Samsung Research America, @artofnothingness Alexey Budyakov // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include #include #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "nav2_mppi_controller/tools/utils.hpp" #include "nav2_mppi_controller/models/path.hpp" // Tests noise generator object class RosLockGuard { public: RosLockGuard() {rclcpp::init(0, nullptr);} ~RosLockGuard() {rclcpp::shutdown();} }; RosLockGuard g_rclcpp; using namespace mppi::utils; // NOLINT using namespace mppi; // NOLINT class TestGoalChecker : public nav2_core::GoalChecker { public: TestGoalChecker() {} virtual void initialize( const rclcpp_lifecycle::LifecycleNode::WeakPtr & /*parent*/, const std::string & /*plugin_name*/, const std::shared_ptr/*costmap_ros*/) {} virtual void reset() {} virtual bool isGoalReached( const geometry_msgs::msg::Pose & /*query_pose*/, const geometry_msgs::msg::Pose & /*goal_pose*/, const geometry_msgs::msg::Twist & /*velocity*/) {return false;} virtual bool getTolerances( geometry_msgs::msg::Pose & pose_tolerance, geometry_msgs::msg::Twist & /*vel_tolerance*/) { pose_tolerance.position.x = 0.25; pose_tolerance.position.y = 0.25; return true; } }; TEST(UtilsTests, MarkerPopulationUtils) { auto pose = createPose(1.0, 2.0, 3.0); EXPECT_EQ(pose.position.x, 1.0); EXPECT_EQ(pose.position.y, 2.0); EXPECT_EQ(pose.position.z, 3.0); EXPECT_EQ(pose.orientation.w, 1.0); auto scale = createScale(1.0, 2.0, 3.0); EXPECT_EQ(scale.x, 1.0); EXPECT_EQ(scale.y, 2.0); EXPECT_EQ(scale.z, 3.0); auto color = createColor(1.0, 2.0, 3.0, 0.0); EXPECT_EQ(color.r, 1.0); EXPECT_EQ(color.g, 2.0); EXPECT_EQ(color.b, 3.0); EXPECT_EQ(color.a, 0.0); auto marker = createMarker(999, pose, scale, color, "map", "ns"); EXPECT_EQ(marker.header.frame_id, "map"); EXPECT_EQ(marker.id, 999); EXPECT_EQ(marker.pose, pose); EXPECT_EQ(marker.scale, scale); EXPECT_EQ(marker.color, color); EXPECT_EQ(marker.ns, "ns"); } TEST(UtilsTests, ConversionTests) { geometry_msgs::msg::TwistStamped output; builtin_interfaces::msg::Time time; // Check population is correct output = toTwistStamped(0.5, 0.3, time, "map"); EXPECT_NEAR(output.twist.linear.x, 0.5, 1e-6); EXPECT_NEAR(output.twist.linear.y, 0.0, 1e-6); EXPECT_NEAR(output.twist.angular.z, 0.3, 1e-6); EXPECT_EQ(output.header.frame_id, "map"); EXPECT_EQ(output.header.stamp, time); output = toTwistStamped(0.5, 0.4, 0.3, time, "map"); EXPECT_NEAR(output.twist.linear.x, 0.5, 1e-6); EXPECT_NEAR(output.twist.linear.y, 0.4, 1e-6); EXPECT_NEAR(output.twist.angular.z, 0.3, 1e-6); EXPECT_EQ(output.header.frame_id, "map"); EXPECT_EQ(output.header.stamp, time); nav_msgs::msg::Path path; path.poses.resize(5); path.poses[2].pose.position.x = 5; path.poses[2].pose.position.y = 50; models::Path path_t = toTensor(path); // Check population is correct EXPECT_EQ(path_t.x.shape(0), 5u); EXPECT_EQ(path_t.y.shape(0), 5u); EXPECT_EQ(path_t.yaws.shape(0), 5u); EXPECT_EQ(path_t.x(2), 5); EXPECT_EQ(path_t.y(2), 50); EXPECT_NEAR(path_t.yaws(2), 0.0, 1e-6); } TEST(UtilsTests, WithTolTests) { geometry_msgs::msg::Pose pose; pose.position.x = 10.0; pose.position.y = 1.0; nav2_core::GoalChecker * goal_checker = new TestGoalChecker; nav_msgs::msg::Path path; path.poses.resize(2); geometry_msgs::msg::Pose & goal = path.poses.back().pose; // Create CriticData with state and goal initialized models::State state; state.pose.pose = pose; models::Trajectories generated_trajectories; models::Path path_critic; xt::xtensor costs; float model_dt; CriticData data = { state, generated_trajectories, path_critic, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; // Test not in tolerance goal.position.x = 0.0; goal.position.y = 0.0; EXPECT_FALSE(withinPositionGoalTolerance(goal_checker, pose, goal)); EXPECT_FALSE(withinPositionGoalTolerance(0.25, pose, goal)); // Test in tolerance goal.position.x = 9.8; goal.position.y = 0.95; EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal)); EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal)); goal.position.x = 10.0; goal.position.y = 0.76; EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal)); EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal)); goal.position.x = 9.76; goal.position.y = 1.0; EXPECT_TRUE(withinPositionGoalTolerance(goal_checker, pose, goal)); EXPECT_TRUE(withinPositionGoalTolerance(0.25, pose, goal)); delete goal_checker; goal_checker = nullptr; EXPECT_FALSE(withinPositionGoalTolerance(goal_checker, pose, goal)); } TEST(UtilsTests, AnglesTests) { // Test angle normalization by creating insane angles xt::xtensor angles, zero_angles; angles = xt::ones({100}); for (unsigned int i = 0; i != angles.shape(0); i++) { angles(i) = i * i; if (i % 2 == 0) { angles(i) *= -1; } } auto norm_ang = normalize_angles(angles); for (unsigned int i = 0; i != norm_ang.shape(0); i++) { EXPECT_TRUE((norm_ang(i) >= -M_PI) && (norm_ang(i) <= M_PI)); } // Test shortest angular distance zero_angles = xt::zeros({100}); auto ang_dist = shortest_angular_distance(angles, zero_angles); for (unsigned int i = 0; i != ang_dist.shape(0); i++) { EXPECT_TRUE((ang_dist(i) >= -M_PI) && (ang_dist(i) <= M_PI)); } // Test point-pose angle geometry_msgs::msg::Pose pose; pose.position.x = 0.0; pose.position.y = 0.0; pose.orientation.w = 1.0; double point_x = 1.0, point_y = 0.0; bool forward_preference = true; EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6); forward_preference = false; EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6); point_x = -1.0; EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), 0.0, 1e-6); forward_preference = true; EXPECT_NEAR(posePointAngle(pose, point_x, point_y, forward_preference), M_PI, 1e-6); } TEST(UtilsTests, FurthestAndClosestReachedPoint) { models::State state; models::Trajectories generated_trajectories; models::Path path; geometry_msgs::msg::Pose goal; xt::xtensor costs; float model_dt = 0.1; CriticData data = {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references // Attempt to set furthest point if notionally set, should not change data.furthest_reached_path_point = 99999; setPathFurthestPointIfNotSet(data); EXPECT_EQ(data.furthest_reached_path_point, 99999); // Attempt to set if not set already with no other information, should fail CriticData data2 = {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references setPathFurthestPointIfNotSet(data2); EXPECT_EQ(data2.furthest_reached_path_point, 0); // Test the actual computation of the path point reached generated_trajectories.x = xt::ones({100, 2}); generated_trajectories.y = xt::zeros({100, 2}); generated_trajectories.yaws = xt::zeros({100, 2}); nav_msgs::msg::Path plan; plan.poses.resize(10); for (unsigned int i = 0; i != plan.poses.size(); i++) { plan.poses[i].pose.position.x = 0.2 * i; plan.poses[i].pose.position.y = 0.0; } path = toTensor(plan); CriticData data3 = {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references EXPECT_EQ(findPathFurthestReachedPoint(data3), 5u); EXPECT_EQ(findPathTrajectoryInitialPoint(data3), 5u); } TEST(UtilsTests, findPathCosts) { models::State state; models::Trajectories generated_trajectories; models::Path path; geometry_msgs::msg::Pose goal; xt::xtensor costs; float model_dt = 0.1; CriticData data = {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references // Test not set if already set, should not change data.path_pts_valid = std::vector(10, false); for (unsigned int i = 0; i != 10; i++) { (*data.path_pts_valid)[i] = false; } EXPECT_TRUE(data.path_pts_valid); setPathCostsIfNotSet(data, nullptr); EXPECT_EQ(data.path_pts_valid->size(), 10u); CriticData data3 = {state, generated_trajectories, path, goal, costs, model_dt, false, nullptr, nullptr, std::nullopt, std::nullopt}; /// Caution, keep references auto costmap_ros = std::make_shared( "dummy_costmap", "", "dummy_costmap"); rclcpp_lifecycle::State lstate; costmap_ros->on_configure(lstate); auto * costmap = costmap_ros->getCostmap(); // island in the middle of lethal cost to cross. Costmap defaults to size 5x5 @ 10cm resolution for (unsigned int i = 10; i <= 30; ++i) { // 1m-3m for (unsigned int j = 10; j <= 30; ++j) { // 1m-3m costmap->setCost(i, j, 254); } } for (unsigned int i = 40; i <= 45; ++i) { // 4m-4.5m for (unsigned int j = 45; j <= 45; ++j) { // 4m-4.5m costmap->setCost(i, j, 253); } } path.reset(50); path.x(1) = 999999999; // OFF COSTMAP path.y(1) = 999999999; path.x(10) = 1.5; // IN LETHAL path.y(10) = 1.5; path.x(20) = 4.2; // IN INFLATED path.y(20) = 4.2; // This should be evaluated and have real outputs now setPathCostsIfNotSet(data3, costmap_ros); EXPECT_TRUE(data3.path_pts_valid.has_value()); for (unsigned int i = 0; i != path.x.shape(0) - 1; i++) { if (i == 1 || i == 10) { EXPECT_FALSE((*data3.path_pts_valid)[i]); } else { EXPECT_TRUE((*data3.path_pts_valid)[i]); } } } TEST(UtilsTests, SmootherTest) { models::ControlSequence noisey_sequence, sequence_init; noisey_sequence.vx = 0.2 * xt::ones({30}); noisey_sequence.vy = 0.0 * xt::ones({30}); noisey_sequence.wz = 0.3 * xt::ones({30}); // Make the sequence noisy auto noises = xt::random::randn({30}, 0.0, 0.2); noisey_sequence.vx += noises; noisey_sequence.vy += noises; noisey_sequence.wz += noises; sequence_init = noisey_sequence; std::array history, history_init; history[3].vx = 0.1; history[3].vy = 0.0; history[3].wz = 0.3; history[2].vx = 0.1; history[2].vy = 0.0; history[2].wz = 0.3; history[1].vx = 0.1; history[1].vy = 0.0; history[1].wz = 0.3; history[0].vx = 0.0; history[0].vy = 0.0; history[0].wz = 0.0; history_init = history; models::OptimizerSettings settings; settings.shift_control_sequence = false; // so result stores 0th value in history savitskyGolayFilter(noisey_sequence, history, settings); // Check history is propogated backward EXPECT_NEAR(history_init[3].vx, history[2].vx, 0.02); EXPECT_NEAR(history_init[3].vy, history[2].vy, 0.02); EXPECT_NEAR(history_init[3].wz, history[2].wz, 0.02); // Check history element is updated for first command EXPECT_NEAR(history[3].vx, 0.2, 0.05); EXPECT_NEAR(history[3].vy, 0.0, 0.035); EXPECT_NEAR(history[3].wz, 0.23, 0.02); // Check that path is smoother float smoothed_val{0}, original_val{0}; for (unsigned int i = 0; i != noisey_sequence.vx.shape(0); i++) { smoothed_val += fabs(noisey_sequence.vx(i) - 0.2); smoothed_val += fabs(noisey_sequence.vy(i) - 0.0); smoothed_val += fabs(noisey_sequence.wz(i) - 0.3); original_val += fabs(sequence_init.vx(i) - 0.2); original_val += fabs(sequence_init.vy(i) - 0.0); original_val += fabs(sequence_init.wz(i) - 0.3); } EXPECT_LT(smoothed_val, original_val); } TEST(UtilsTests, FindPathInversionTest) { // Straight path, no inversions to be found nav_msgs::msg::Path path; for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = i; path.poses.push_back(pose); } EXPECT_EQ(utils::findFirstPathInversion(path), 10u); // To short to process path.poses.erase(path.poses.begin(), path.poses.begin() + 7); EXPECT_EQ(utils::findFirstPathInversion(path), 3u); // Has inversion at index 10, so should return 11 for the first point afterwards // 0 1 2 3 4 5 6 7 8 9 10 **9** 8 7 6 5 4 3 2 1 path.poses.clear(); for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = i; path.poses.push_back(pose); } for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = 10 - i; path.poses.push_back(pose); } EXPECT_EQ(utils::findFirstPathInversion(path), 11u); } TEST(UtilsTests, RemovePosesAfterPathInversionTest) { nav_msgs::msg::Path path; // straight path for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = i; path.poses.push_back(pose); } EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 0u); // try empty path path.poses.clear(); EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 0u); // cusping path for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = i; path.poses.push_back(pose); } for (unsigned int i = 0; i != 10; i++) { geometry_msgs::msg::PoseStamped pose; pose.pose.position.x = 10 - i; path.poses.push_back(pose); } EXPECT_EQ(utils::removePosesAfterFirstInversion(path), 11u); // Check to see if removed EXPECT_EQ(path.poses.size(), 11u); EXPECT_EQ(path.poses.back().pose.position.x, 10); }