26 #include "planner_tester.hpp"
27 #include "geometry_msgs/msg/twist.hpp"
28 #include "nav2_map_server/map_mode.hpp"
29 #include "nav2_map_server/map_io.hpp"
30 #include "nav2_msgs/msg/costmap_meta_data.hpp"
32 using namespace std::chrono_literals;
33 using namespace std::chrono;
35 using nav2_util::TestCostmap;
37 namespace nav2_system_tests
40 PlannerTester::PlannerTester()
41 : nav2::LifecycleNode(
"PlannerTester"), is_active_(false),
42 map_set_(false), costmap_set_(false),
43 using_fake_costmap_(true), trinary_costmap_(true),
44 track_unknown_space_(false), lethal_threshold_(100), unknown_cost_value_(-1),
45 testCostmapType_(TestCostmap::open_space), base_transform_(nullptr),
46 map_publish_rate_(100s)
50 void PlannerTester::activate()
53 throw std::runtime_error(
"Trying to activate while already active");
59 spin_thread_ = std::make_unique<nav2::NodeThread>(
this);
62 costmap_ = std::make_unique<Costmap>(
this);
63 loadSimpleCostmap(TestCostmap::open_space);
65 startRobotTransform();
68 auto state = rclcpp_lifecycle::State();
69 planner_tester_ = std::make_shared<NavFnPlannerTester>();
70 planner_tester_->declare_parameter(
71 "GridBased.use_astar", rclcpp::ParameterValue(
true));
72 planner_tester_->set_parameter(
73 rclcpp::Parameter(std::string(
"GridBased.use_astar"), rclcpp::ParameterValue(
true)));
74 planner_tester_->set_parameter(
75 rclcpp::Parameter(std::string(
"expected_planner_frequency"), rclcpp::ParameterValue(-1.0)));
76 planner_tester_->onConfigure(state);
77 publishRobotTransform();
78 map_pub_ = this->create_publisher<nav_msgs::msg::OccupancyGrid>(
"map");
79 map_pub_->on_activate();
80 path_valid_client_ = this->create_client<nav2_msgs::srv::IsPathValid>(
"is_path_valid");
83 planner_tester_->onActivate(state);
86 void PlannerTester::deactivate()
89 throw std::runtime_error(
"Trying to deactivate while already inactive");
96 auto state = rclcpp_lifecycle::State();
97 planner_tester_->onDeactivate(state);
98 planner_tester_->onCleanup(state);
103 tf_broadcaster_.reset();
106 PlannerTester::~PlannerTester()
113 void PlannerTester::startRobotTransform()
116 tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(
this);
119 geometry_msgs::msg::Point robot_position;
120 robot_position.x = 1.0;
121 robot_position.y = 1.0;
122 updateRobotPosition(robot_position);
125 transform_timer_ = create_wall_timer(
126 100ms, std::bind(&PlannerTester::publishRobotTransform,
this));
129 void PlannerTester::updateRobotPosition(
const geometry_msgs::msg::Point & position)
131 if (!base_transform_) {
132 base_transform_ = std::make_unique<geometry_msgs::msg::TransformStamped>();
133 base_transform_->header.frame_id =
"map";
134 base_transform_->child_frame_id =
"base_link";
136 std::cout << now().nanoseconds() << std::endl;
138 base_transform_->header.stamp = now() + rclcpp::Duration(0.25s);
139 base_transform_->transform.translation.x = position.x;
140 base_transform_->transform.translation.y = position.y;
141 base_transform_->transform.rotation.w = 1.0;
143 publishRobotTransform();
146 void PlannerTester::publishRobotTransform()
148 if (base_transform_) {
149 tf_broadcaster_->sendTransform(*base_transform_);
153 void PlannerTester::loadDefaultMap()
156 double resolution = 1.0;
158 double occupancy_threshold = 0.65;
159 double free_threshold = 0.196;
162 std::vector<double> origin = {0.0, 0.0, 0.0};
164 nav2_map_server::MapMode mode = nav2_map_server::MapMode::Trinary;
166 std::string file_path =
"";
167 char const * path = getenv(
"TEST_MAP");
169 throw std::runtime_error(
170 "Path to map image file"
171 " has not been specified in environment variable `TEST_MAP`.");
173 file_path = std::string(path);
176 RCLCPP_INFO(this->get_logger(),
"Loading map with file_path: %s", file_path.c_str());
179 map_ = std::make_shared<nav_msgs::msg::OccupancyGrid>();
182 load_parameters.image_file_name = file_path;
183 load_parameters.resolution = resolution;
184 load_parameters.origin = origin;
185 load_parameters.free_thresh = free_threshold;
186 load_parameters.occupied_thresh = occupancy_threshold;
187 load_parameters.mode = mode;
188 load_parameters.negate = negate;
189 loadMapFromFile(load_parameters, *map_);
193 "Failed to load image from file: %s", file_path.c_str());
197 map_->header.stamp = this->now();
198 map_->header.frame_id =
"map";
199 map_->info.map_load_time = this->now();
202 map_timer_ = create_wall_timer(1s, [
this]() ->
void {map_pub_->publish(*map_);});
205 costmap_set_ =
false;
206 using_fake_costmap_ =
false;
211 void PlannerTester::loadSimpleCostmap(
const TestCostmap & testCostmapType)
213 RCLCPP_INFO(get_logger(),
"loadSimpleCostmap called.");
215 RCLCPP_DEBUG(this->get_logger(),
"Setting a new costmap with fake values");
218 costmap_->set_test_costmap(testCostmapType);
221 using_fake_costmap_ =
true;
224 void PlannerTester::setCostmap()
227 RCLCPP_ERROR(this->get_logger(),
"Map has not been provided");
231 costmap_ = std::make_unique<Costmap>(
232 this, trinary_costmap_, track_unknown_space_, lethal_threshold_, unknown_cost_value_);
234 costmap_->set_static_map(*map_);
237 using_fake_costmap_ =
false;
240 bool PlannerTester::defaultPlannerTest(
241 ComputePathToPoseResult & path,
245 RCLCPP_ERROR(this->get_logger(),
"Costmap must be set before requesting a plan");
250 geometry_msgs::msg::Point robot_position;
251 ComputePathToPoseCommand goal;
252 auto costmap_properties = costmap_->get_properties();
254 if (using_fake_costmap_) {
255 RCLCPP_DEBUG(this->get_logger(),
"Planning using a fake costmap");
257 robot_position.x = 1.0;
258 robot_position.y = 1.0;
260 goal.pose.position.x = 8.0;
261 goal.pose.position.y = 8.0;
264 RCLCPP_DEBUG(this->get_logger(),
"Planning using the provided map");
268 robot_position.x = 390.0;
269 robot_position.y = 10.0;
271 goal.pose.position.x = 10.0;
272 goal.pose.position.y = 390.0;
277 return plannerTest(robot_position, goal, path);
280 bool PlannerTester::defaultPlannerRandomTests(
281 const unsigned int number_tests,
282 const float acceptable_fail_ratio = 0.1)
285 RCLCPP_ERROR(this->get_logger(),
"Costmap must be set before requesting a plan");
289 if (using_fake_costmap_) {
292 "Randomized testing with hardcoded costmaps not implemented yet");
297 std::random_device random_device;
298 std::mt19937 generator(random_device());
301 std::uniform_int_distribution<> distribution_x(1, costmap_->get_properties().size_x - 1);
302 std::uniform_int_distribution<> distribution_y(1, costmap_->get_properties().size_y - 1);
304 auto generate_random = [&]()
mutable -> std::pair<int, int> {
305 bool point_is_free =
false;
307 while (!point_is_free) {
308 x = distribution_x(generator);
309 y = distribution_y(generator);
310 point_is_free = costmap_->is_free(x, y);
312 return std::make_pair(x, y);
316 geometry_msgs::msg::Point robot_position;
317 ComputePathToPoseCommand goal;
318 ComputePathToPoseResult path;
320 unsigned int num_fail = 0;
321 auto start = high_resolution_clock::now();
322 for (
unsigned int test_num = 0; test_num < number_tests; ++test_num) {
323 RCLCPP_DEBUG(this->get_logger(),
"Running test #%u", test_num + 1);
329 auto vals = generate_random();
330 robot_position.x = vals.first;
331 robot_position.y = vals.second;
333 vals = generate_random();
334 goal.pose.position.x = vals.first;
335 goal.pose.position.y = vals.second;
337 if (!plannerTest(robot_position, goal, path)) {
339 this->get_logger(),
"Failed with start at %0.2f, %0.2f and goal at %0.2f, %0.2f",
340 robot_position.x, robot_position.y, goal.pose.position.x, goal.pose.position.y);
344 auto end = high_resolution_clock::now();
345 auto elapsed = duration_cast<milliseconds>(end - start);
349 "Tested with %u tests. Planner failed on %u. Test time %ld ms",
350 number_tests, num_fail, elapsed.count());
352 if ((num_fail / number_tests) > acceptable_fail_ratio) {
359 bool PlannerTester::plannerTest(
360 const geometry_msgs::msg::Point & robot_position,
361 const ComputePathToPoseCommand & goal,
362 ComputePathToPoseResult & path)
364 RCLCPP_DEBUG(this->get_logger(),
"Getting the path from the planner");
367 updateRobotPosition(robot_position);
371 TaskStatus status = createPlan(goal, path);
373 RCLCPP_DEBUG(this->get_logger(),
"Path request status: %d",
static_cast<int8_t
>(status));
375 if (status == TaskStatus::FAILED) {
377 }
else if (status == TaskStatus::SUCCEEDED) {
379 RCLCPP_DEBUG(this->get_logger(),
"Got path, checking for possible collisions");
380 return isCollisionFree(path) && isWithinTolerance(robot_position, goal, path);
386 TaskStatus PlannerTester::createPlan(
387 const ComputePathToPoseCommand & goal,
388 ComputePathToPoseResult & path)
391 planner_tester_->setCostmap(costmap_.get());
394 if (planner_tester_->createPath(goal, path)) {
395 return TaskStatus::SUCCEEDED;
398 return TaskStatus::FAILED;
401 bool PlannerTester::isPathValid(
402 nav_msgs::msg::Path & path,
unsigned int max_cost,
403 bool consider_unknown_as_obstacle)
405 planner_tester_->setCostmap(costmap_.get());
407 auto request = std::make_shared<nav2_msgs::srv::IsPathValid::Request>();
408 request->path = path;
409 request->max_cost = max_cost;
410 request->consider_unknown_as_obstacle = consider_unknown_as_obstacle;
411 auto result = path_valid_client_->async_call(request);
413 RCLCPP_INFO(this->get_logger(),
"Waiting for service complete");
414 if (rclcpp::spin_until_future_complete(
415 this->planner_tester_, result,
416 std::chrono::milliseconds(100)) ==
417 rclcpp::FutureReturnCode::SUCCESS)
419 return result.get()->is_valid;
421 RCLCPP_INFO(get_logger(),
"Failed to call is_path_valid service");
426 bool PlannerTester::isCollisionFree(
const ComputePathToPoseResult & path)
437 bool collisionFree =
true;
439 for (
auto pose : path.poses) {
440 collisionFree = costmap_->is_free(
441 static_cast<unsigned int>(std::round(pose.pose.position.x)),
442 static_cast<unsigned int>(std::round(pose.pose.position.y)));
444 if (!collisionFree) {
446 this->get_logger(),
"Path has collision at (%.2f, %.2f)",
447 pose.pose.position.x, pose.pose.position.y);
453 RCLCPP_DEBUG(this->get_logger(),
"Path has no collisions");
457 bool PlannerTester::isWithinTolerance(
458 const geometry_msgs::msg::Point & robot_position,
459 const ComputePathToPoseCommand & goal,
460 const ComputePathToPoseResult & path)
const
462 return isWithinTolerance(
463 robot_position, goal, path, 0.0, ComputePathToPoseResult());
466 bool PlannerTester::isWithinTolerance(
467 const geometry_msgs::msg::Point & robot_position,
468 const ComputePathToPoseCommand & goal,
469 const ComputePathToPoseResult & path,
471 const ComputePathToPoseResult & )
const
476 auto path_start = path.poses[0];
477 auto path_end = path.poses.end()[-1];
480 path_start.pose.position.x == robot_position.x &&
481 path_start.pose.position.y == robot_position.y &&
482 path_end.pose.position.x == goal.pose.position.x &&
483 path_end.pose.position.y == goal.pose.position.y)
485 RCLCPP_DEBUG(this->get_logger(),
"Path has correct start and end points");
489 RCLCPP_WARN(this->get_logger(),
"Path deviates from requested start and end points");
492 this->get_logger(),
"Requested path starts at (%.2f, %.2f) and ends at (%.2f, %.2f)",
493 robot_position.x, robot_position.y, goal.pose.position.x, goal.pose.position.y);
496 this->get_logger(),
"Computed path starts at (%.2f, %.2f) and ends at (%.2f, %.2f)",
497 path_start.pose.position.x, path_start.pose.position.y,
498 path_end.pose.position.x, path_end.pose.position.y);
503 void PlannerTester::printPath(
const ComputePathToPoseResult & path)
const
506 auto ss = std::stringstream{};
508 for (
auto pose : path.poses) {
509 ss <<
" point #" << index <<
" with" <<
510 " x: " << std::setprecision(3) << pose.pose.position.x <<
511 " y: " << std::setprecision(3) << pose.pose.position.y <<
'\n';
515 RCLCPP_INFO(get_logger(), ss.str().c_str());
Class for a single layered costmap initialized from an occupancy grid representing the map.