// Copyright (c) 2020, Samsung Research America // // 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. Reserved. #include #include #include #include #include #include "gtest/gtest.h" #include "rclcpp/rclcpp.hpp" #include "nav2_costmap_2d/costmap_2d.hpp" #include "nav2_costmap_2d/costmap_subscriber.hpp" #include "nav2_util/lifecycle_node.hpp" #include "nav2_smac_planner/node_hybrid.hpp" #include "nav2_smac_planner/collision_checker.hpp" class RclCppFixture { public: RclCppFixture() {rclcpp::init(0, nullptr);} ~RclCppFixture() {rclcpp::shutdown();} }; RclCppFixture g_rclcppfixture; TEST(NodeHybridTest, test_node_hybrid) { auto node = std::make_shared("test"); nav2_smac_planner::SearchInfo info; info.change_penalty = 0.1; info.non_straight_penalty = 1.1; info.reverse_penalty = 2.0; info.minimum_turning_radius = 8; // 0.4m/5cm resolution costmap info.cost_penalty = 1.7; info.retrospective_penalty = 0.1; unsigned int size_x = 10; unsigned int size_y = 10; unsigned int size_theta = 72; // Check defaulted constants nav2_smac_planner::NodeHybrid testA(49); EXPECT_EQ(testA.travel_distance_cost, sqrt(2)); nav2_smac_planner::NodeHybrid::initMotionModel( nav2_smac_planner::MotionModel::DUBIN, size_x, size_y, size_theta, info); nav2_costmap_2d::Costmap2D * costmapA = new nav2_costmap_2d::Costmap2D( 10, 10, 0.05, 0.0, 0.0, 0); std::unique_ptr checker = std::make_unique(costmapA, 72, node); checker->setFootprint(nav2_costmap_2d::Footprint(), true, 0.0); // test construction nav2_smac_planner::NodeHybrid testB(49); EXPECT_TRUE(std::isnan(testA.getCost())); // test node valid and cost testA.pose.x = 5; testA.pose.y = 5; testA.pose.theta = 0; EXPECT_EQ(testA.isNodeValid(true, checker.get()), true); EXPECT_EQ(testA.isNodeValid(false, checker.get()), true); EXPECT_EQ(testA.getCost(), 0.0f); // test reset testA.reset(); EXPECT_TRUE(std::isnan(testA.getCost())); // Check motion-specific constants EXPECT_NEAR(testA.travel_distance_cost, 2.08842, 0.1); // check collision checking EXPECT_EQ(testA.isNodeValid(false, checker.get()), true); // check traversal cost computation // simulated first node, should return neutral cost EXPECT_NEAR(testB.getTraversalCost(&testA), 2.088, 0.1); // now with straight motion, cost is 0, so will be neutral as well // but now reduced by retrospective penalty (10%) testB.setMotionPrimitiveIndex(1); testA.setMotionPrimitiveIndex(0); EXPECT_NEAR(testB.getTraversalCost(&testA), 2.088 * 0.9, 0.1); // same direction as parent, testB testA.setMotionPrimitiveIndex(1); EXPECT_NEAR(testB.getTraversalCost(&testA), 2.297f * 0.9, 0.01); // opposite direction as parent, testB testA.setMotionPrimitiveIndex(2); EXPECT_NEAR(testB.getTraversalCost(&testA), 2.506f * 0.9, 0.01); // will throw because never collision checked testB EXPECT_THROW(testA.getTraversalCost(&testB), std::runtime_error); // check motion primitives EXPECT_EQ(testA.getMotionPrimitiveIndex(), 2u); // check operator== works on index nav2_smac_planner::NodeHybrid testC(49); EXPECT_TRUE(testA == testC); // check accumulated costs are set testC.setAccumulatedCost(100); EXPECT_EQ(testC.getAccumulatedCost(), 100.0f); // check visiting state EXPECT_EQ(testC.wasVisited(), false); testC.visited(); EXPECT_EQ(testC.wasVisited(), true); // check index EXPECT_EQ(testC.getIndex(), 49u); // check set pose and pose testC.setPose(nav2_smac_planner::NodeHybrid::Coordinates(10.0, 5.0, 4)); EXPECT_EQ(testC.pose.x, 10.0); EXPECT_EQ(testC.pose.y, 5.0); EXPECT_EQ(testC.pose.theta, 4); // check static index functions EXPECT_EQ(nav2_smac_planner::NodeHybrid::getIndex(1u, 1u, 4u, 10u, 72u), 796u); EXPECT_EQ(nav2_smac_planner::NodeHybrid::getCoords(796u, 10u, 72u).x, 1u); EXPECT_EQ(nav2_smac_planner::NodeHybrid::getCoords(796u, 10u, 72u).y, 1u); EXPECT_EQ(nav2_smac_planner::NodeHybrid::getCoords(796u, 10u, 72u).theta, 4u); delete costmapA; } TEST(NodeHybridTest, test_obstacle_heuristic) { auto node = std::make_shared("test"); nav2_smac_planner::SearchInfo info; info.change_penalty = 0.1; info.non_straight_penalty = 1.1; info.reverse_penalty = 2.0; info.minimum_turning_radius = 8; // 0.4m/5cm resolution costmap info.cost_penalty = 1.7; info.retrospective_penalty = 0.0; unsigned int size_x = 100; unsigned int size_y = 100; unsigned int size_theta = 72; nav2_smac_planner::NodeHybrid::initMotionModel( nav2_smac_planner::MotionModel::DUBIN, size_x, size_y, size_theta, info); nav2_costmap_2d::Costmap2D * costmapA = new nav2_costmap_2d::Costmap2D( 100, 100, 0.1, 0.0, 0.0, 0); // island in the middle of lethal cost to cross for (unsigned int i = 20; i <= 80; ++i) { for (unsigned int j = 40; j <= 60; ++j) { costmapA->setCost(i, j, 254); } } // path on the right is narrow and thus with high cost for (unsigned int i = 20; i <= 80; ++i) { for (unsigned int j = 61; j <= 70; ++j) { costmapA->setCost(i, j, 250); } } for (unsigned int i = 20; i <= 80; ++i) { for (unsigned int j = 71; j < 100; ++j) { costmapA->setCost(i, j, 254); } } std::unique_ptr checker = std::make_unique(costmapA, 72, node); checker->setFootprint(nav2_costmap_2d::Footprint(), true, 0.0); nav2_smac_planner::NodeHybrid testA(0); testA.pose.x = 10; testA.pose.y = 50; testA.pose.theta = 0; nav2_smac_planner::NodeHybrid testB(1); testB.pose.x = 90; testB.pose.y = 51; // goal is a bit closer to the high-cost passage testB.pose.theta = 0; // first block the high-cost passage to make sure the cost spreads through the better path for (unsigned int j = 61; j <= 70; ++j) { costmapA->setCost(50, j, 254); } nav2_smac_planner::NodeHybrid::resetObstacleHeuristic( costmapA, testA.pose.x, testA.pose.y, testB.pose.x, testB.pose.y); float wide_passage_cost = nav2_smac_planner::NodeHybrid::getObstacleHeuristic( testA.pose, testB.pose, info.cost_penalty); EXPECT_NEAR(wide_passage_cost, 91.1f, 0.1f); // then unblock it to check if cost remains the same // (it should, since the unblocked narrow path will have higher cost than the wide one // and thus lower bound of the path cost should be unchanged) for (unsigned int j = 61; j <= 70; ++j) { costmapA->setCost(50, j, 250); } nav2_smac_planner::NodeHybrid::resetObstacleHeuristic( costmapA, testA.pose.x, testA.pose.y, testB.pose.x, testB.pose.y); float two_passages_cost = nav2_smac_planner::NodeHybrid::getObstacleHeuristic( testA.pose, testB.pose, info.cost_penalty); EXPECT_EQ(wide_passage_cost, two_passages_cost); delete costmapA; } TEST(NodeHybridTest, test_node_debin_neighbors) { nav2_smac_planner::SearchInfo info; info.change_penalty = 1.2; info.non_straight_penalty = 1.4; info.reverse_penalty = 2.1; info.minimum_turning_radius = 4; // 0.2 in grid coordinates info.retrospective_penalty = 0.0; unsigned int size_x = 100; unsigned int size_y = 100; unsigned int size_theta = 72; nav2_smac_planner::NodeHybrid::initMotionModel( nav2_smac_planner::MotionModel::DUBIN, size_x, size_y, size_theta, info); // test neighborhood computation EXPECT_EQ(nav2_smac_planner::NodeHybrid::motion_table.projections.size(), 3u); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._x, 1.731517, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._y, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._theta, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._x, 1.69047, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._y, 0.3747, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._theta, 5, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._x, 1.69047, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._y, -0.3747, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._theta, -5, 0.01); } TEST(NodeHybridTest, test_node_reeds_neighbors) { auto lnode = std::make_shared("test"); nav2_smac_planner::SearchInfo info; info.change_penalty = 1.2; info.non_straight_penalty = 1.4; info.reverse_penalty = 2.1; info.minimum_turning_radius = 8; // 0.4 in grid coordinates info.retrospective_penalty = 0.0; unsigned int size_x = 100; unsigned int size_y = 100; unsigned int size_theta = 72; nav2_smac_planner::NodeHybrid::initMotionModel( nav2_smac_planner::MotionModel::REEDS_SHEPP, size_x, size_y, size_theta, info); EXPECT_EQ(nav2_smac_planner::NodeHybrid::motion_table.projections.size(), 6u); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._x, 2.088, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._y, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[0]._theta, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._x, 2.070, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._y, 0.272, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[1]._theta, 3, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._x, 2.070, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._y, -0.272, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[2]._theta, -3, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[3]._x, -2.088, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[3]._y, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[3]._theta, 0, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[4]._x, -2.07, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[4]._y, 0.272, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[4]._theta, -3, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[5]._x, -2.07, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[5]._y, -0.272, 0.01); EXPECT_NEAR(nav2_smac_planner::NodeHybrid::motion_table.projections[5]._theta, 3, 0.01); nav2_costmap_2d::Costmap2D costmapA(100, 100, 0.05, 0.0, 0.0, 0); std::unique_ptr checker = std::make_unique(&costmapA, 72, lnode); checker->setFootprint(nav2_costmap_2d::Footprint(), true, 0.0); nav2_smac_planner::NodeHybrid * node = new nav2_smac_planner::NodeHybrid(49); std::function neighborGetter = [&, this](const unsigned int & index, nav2_smac_planner::NodeHybrid * & neighbor_rtn) -> bool { // because we don't return a real object return false; }; nav2_smac_planner::NodeHybrid::NodeVector neighbors; node->getNeighbors(neighborGetter, checker.get(), false, neighbors); delete node; // should be empty since totally invalid EXPECT_EQ(neighbors.size(), 0u); } TEST(NodeHybridTest, basic_get_closest_angular_bin_test) { // Tests to check getClosestAngularBin behavior for different input types nav2_smac_planner::HybridMotionTable motion_table; { motion_table.bin_size = 3.1415926; motion_table.num_angle_quantization = 2; double test_theta = 3.1415926; unsigned int expected_angular_bin = 1; unsigned int calculated_angular_bin = motion_table.getClosestAngularBin(test_theta); EXPECT_EQ(expected_angular_bin, calculated_angular_bin); } { motion_table.bin_size = M_PI; motion_table.num_angle_quantization = 2; double test_theta = M_PI / 2.0 - 0.000001; unsigned int expected_angular_bin = 0; unsigned int calculated_angular_bin = motion_table.getClosestAngularBin(test_theta); EXPECT_EQ(expected_angular_bin, calculated_angular_bin); } { motion_table.bin_size = M_PI; motion_table.num_angle_quantization = 2; float test_theta = M_PI; unsigned int expected_angular_bin = 1; unsigned int calculated_angular_bin = motion_table.getClosestAngularBin(test_theta); EXPECT_EQ(expected_angular_bin, calculated_angular_bin); } { motion_table.bin_size = 0.0872664675; motion_table.num_angle_quantization = 72; double test_theta = 6.28317530718; // 0.0001 less than 2 pi unsigned int expected_angular_bin = 0; // should be closer to wrap around unsigned int calculated_angular_bin = motion_table.getClosestAngularBin(test_theta); EXPECT_EQ(expected_angular_bin, calculated_angular_bin); } }