Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
planner_tester.cpp
1 // Copyright (c) 2018 Intel Corporation
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License. Reserved.
14 
15 #include <string>
16 #include <random>
17 #include <tuple>
18 #include <utility>
19 #include <vector>
20 #include <memory>
21 #include <iostream>
22 #include <chrono>
23 #include <sstream>
24 #include <iomanip>
25 
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"
31 
32 using namespace std::chrono_literals;
33 using namespace std::chrono; // NOLINT
34 using nav2_util::Costmap;
35 using nav2_util::TestCostmap;
36 
37 namespace nav2_system_tests
38 {
39 
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)
47 {
48 }
49 
50 void PlannerTester::activate()
51 {
52  if (is_active_) {
53  throw std::runtime_error("Trying to activate while already active");
54  return;
55  }
56  is_active_ = true;
57 
58  // Launch a thread to process the messages for this node
59  spin_thread_ = std::make_unique<nav2::NodeThread>(this);
60 
61  // We start with a 10x10 grid with no obstacles
62  costmap_ = std::make_unique<Costmap>(this);
63  loadSimpleCostmap(TestCostmap::open_space);
64 
65  startRobotTransform();
66 
67  // The navfn wrapper
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");
81  rclcpp::Rate r(1);
82  r.sleep();
83  planner_tester_->onActivate(state);
84 }
85 
86 void PlannerTester::deactivate()
87 {
88  if (!is_active_) {
89  throw std::runtime_error("Trying to deactivate while already inactive");
90  return;
91  }
92  is_active_ = false;
93 
94  spin_thread_.reset();
95 
96  auto state = rclcpp_lifecycle::State();
97  planner_tester_->onDeactivate(state);
98  planner_tester_->onCleanup(state);
99 
100  map_timer_.reset();
101  map_pub_.reset();
102  map_.reset();
103  tf_broadcaster_.reset();
104 }
105 
106 PlannerTester::~PlannerTester()
107 {
108  if (is_active_) {
109  deactivate();
110  }
111 }
112 
113 void PlannerTester::startRobotTransform()
114 {
115  // Provide the robot pose transform
116  tf_broadcaster_ = std::make_shared<tf2_ros::TransformBroadcaster>(this);
117 
118  // Set an initial pose
119  geometry_msgs::msg::Point robot_position;
120  robot_position.x = 1.0;
121  robot_position.y = 1.0;
122  updateRobotPosition(robot_position);
123 
124  // Publish the transform periodically
125  transform_timer_ = create_wall_timer(
126  100ms, std::bind(&PlannerTester::publishRobotTransform, this));
127 }
128 
129 void PlannerTester::updateRobotPosition(const geometry_msgs::msg::Point & position)
130 {
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";
135  }
136  std::cout << now().nanoseconds() << std::endl;
137 
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;
142 
143  publishRobotTransform();
144 }
145 
146 void PlannerTester::publishRobotTransform()
147 {
148  if (base_transform_) {
149  tf_broadcaster_->sendTransform(*base_transform_);
150  }
151 }
152 
153 void PlannerTester::loadDefaultMap()
154 {
155  // Specs for the default map
156  double resolution = 1.0;
157  bool negate = false;
158  double occupancy_threshold = 0.65;
159  double free_threshold = 0.196;
160 
161  // Define origin offset
162  std::vector<double> origin = {0.0, 0.0, 0.0};
163 
164  nav2_map_server::MapMode mode = nav2_map_server::MapMode::Trinary;
165 
166  std::string file_path = "";
167  char const * path = getenv("TEST_MAP");
168  if (path == NULL) {
169  throw std::runtime_error(
170  "Path to map image file"
171  " has not been specified in environment variable `TEST_MAP`.");
172  } else {
173  file_path = std::string(path);
174  }
175 
176  RCLCPP_INFO(this->get_logger(), "Loading map with file_path: %s", file_path.c_str());
177 
178  try {
179  map_ = std::make_shared<nav_msgs::msg::OccupancyGrid>();
180 
181  nav2_map_server::LoadParameters load_parameters;
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_);
190  } catch (...) {
191  RCLCPP_ERROR(
192  this->get_logger(),
193  "Failed to load image from file: %s", file_path.c_str());
194  throw;
195  }
196 
197  map_->header.stamp = this->now();
198  map_->header.frame_id = "map";
199  map_->info.map_load_time = this->now();
200 
201  // TODO(orduno): #443 replace with a latched topic
202  map_timer_ = create_wall_timer(1s, [this]() -> void {map_pub_->publish(*map_);});
203 
204  map_set_ = true;
205  costmap_set_ = false;
206  using_fake_costmap_ = false;
207 
208  setCostmap();
209 }
210 
211 void PlannerTester::loadSimpleCostmap(const TestCostmap & testCostmapType)
212 {
213  RCLCPP_INFO(get_logger(), "loadSimpleCostmap called.");
214  if (costmap_set_) {
215  RCLCPP_DEBUG(this->get_logger(), "Setting a new costmap with fake values");
216  }
217 
218  costmap_->set_test_costmap(testCostmapType);
219 
220  costmap_set_ = true;
221  using_fake_costmap_ = true;
222 }
223 
224 void PlannerTester::setCostmap()
225 {
226  if (!map_set_) {
227  RCLCPP_ERROR(this->get_logger(), "Map has not been provided");
228  return;
229  }
230 
231  costmap_ = std::make_unique<Costmap>(
232  this, trinary_costmap_, track_unknown_space_, lethal_threshold_, unknown_cost_value_);
233 
234  costmap_->set_static_map(*map_);
235 
236  costmap_set_ = true;
237  using_fake_costmap_ = false;
238 }
239 
240 bool PlannerTester::defaultPlannerTest(
241  ComputePathToPoseResult & path,
242  const double /*deviation_tolerance*/)
243 {
244  if (!costmap_set_) {
245  RCLCPP_ERROR(this->get_logger(), "Costmap must be set before requesting a plan");
246  return false;
247  }
248 
249  // TODO(orduno) #443 Add support for planners that take into account robot orientation
250  geometry_msgs::msg::Point robot_position;
251  ComputePathToPoseCommand goal;
252  auto costmap_properties = costmap_->get_properties();
253 
254  if (using_fake_costmap_) {
255  RCLCPP_DEBUG(this->get_logger(), "Planning using a fake costmap");
256 
257  robot_position.x = 1.0;
258  robot_position.y = 1.0;
259 
260  goal.pose.position.x = 8.0;
261  goal.pose.position.y = 8.0;
262 
263  } else {
264  RCLCPP_DEBUG(this->get_logger(), "Planning using the provided map");
265 
266  // Defined with respect to world coordinate system
267  // Planner will do coordinate transformation to map internally
268  robot_position.x = 390.0;
269  robot_position.y = 10.0;
270 
271  goal.pose.position.x = 10.0;
272  goal.pose.position.y = 390.0;
273  }
274 
275  // TODO(orduno): #443 On a default test, provide the reference path to compare with the planner
276  // result.
277  return plannerTest(robot_position, goal, path);
278 }
279 
280 bool PlannerTester::defaultPlannerRandomTests(
281  const unsigned int number_tests,
282  const float acceptable_fail_ratio = 0.1)
283 {
284  if (!costmap_set_) {
285  RCLCPP_ERROR(this->get_logger(), "Costmap must be set before requesting a plan");
286  return false;
287  }
288 
289  if (using_fake_costmap_) {
290  RCLCPP_ERROR(
291  this->get_logger(),
292  "Randomized testing with hardcoded costmaps not implemented yet");
293  return false;
294  }
295 
296  // Initialize random number generator
297  std::random_device random_device;
298  std::mt19937 generator(random_device());
299 
300  // Obtain random positions within map
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);
303 
304  auto generate_random = [&]() mutable -> std::pair<int, int> {
305  bool point_is_free = false;
306  int x, y;
307  while (!point_is_free) {
308  x = distribution_x(generator);
309  y = distribution_y(generator);
310  point_is_free = costmap_->is_free(x, y);
311  }
312  return std::make_pair(x, y);
313  };
314 
315  // TODO(orduno) #443 Add support for planners that take into account robot orientation
316  geometry_msgs::msg::Point robot_position;
317  ComputePathToPoseCommand goal;
318  ComputePathToPoseResult path;
319 
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);
324 
325  // Compose the robot start position and goal using random numbers
326  // Defined with respect to world coordinate system
327  // Planner will do coordinate transformation to map internally
328 
329  auto vals = generate_random();
330  robot_position.x = vals.first;
331  robot_position.y = vals.second;
332 
333  vals = generate_random();
334  goal.pose.position.x = vals.first;
335  goal.pose.position.y = vals.second;
336 
337  if (!plannerTest(robot_position, goal, path)) {
338  RCLCPP_WARN(
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);
341  ++num_fail;
342  }
343  }
344  auto end = high_resolution_clock::now();
345  auto elapsed = duration_cast<milliseconds>(end - start);
346 
347  RCLCPP_INFO(
348  this->get_logger(),
349  "Tested with %u tests. Planner failed on %u. Test time %ld ms",
350  number_tests, num_fail, elapsed.count());
351 
352  if ((num_fail / number_tests) > acceptable_fail_ratio) {
353  return false;
354  }
355 
356  return true;
357 }
358 
359 bool PlannerTester::plannerTest(
360  const geometry_msgs::msg::Point & robot_position,
361  const ComputePathToPoseCommand & goal,
362  ComputePathToPoseResult & path)
363 {
364  RCLCPP_DEBUG(this->get_logger(), "Getting the path from the planner");
365 
366  // First make available the current robot position for the planner to take as starting point
367  updateRobotPosition(robot_position);
368  sleep(0.05);
369 
370  // Then request to compute a path
371  TaskStatus status = createPlan(goal, path);
372 
373  RCLCPP_DEBUG(this->get_logger(), "Path request status: %d", static_cast<int8_t>(status));
374 
375  if (status == TaskStatus::FAILED) {
376  return false;
377  } else if (status == TaskStatus::SUCCEEDED) {
378  // TODO(orduno): #443 check why task may report success while planner returns a path of 0 points
379  RCLCPP_DEBUG(this->get_logger(), "Got path, checking for possible collisions");
380  return isCollisionFree(path) && isWithinTolerance(robot_position, goal, path);
381  }
382 
383  return false;
384 }
385 
386 TaskStatus PlannerTester::createPlan(
387  const ComputePathToPoseCommand & goal,
388  ComputePathToPoseResult & path)
389 {
390  // Update the costmap of the planner to the set data
391  planner_tester_->setCostmap(costmap_.get());
392 
393  // Call planning algorithm
394  if (planner_tester_->createPath(goal, path)) {
395  return TaskStatus::SUCCEEDED;
396  }
397 
398  return TaskStatus::FAILED;
399 }
400 
401 bool PlannerTester::isPathValid(
402  nav_msgs::msg::Path & path, unsigned int max_cost,
403  bool consider_unknown_as_obstacle)
404 {
405  planner_tester_->setCostmap(costmap_.get());
406  // create a fake service request
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);
412 
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)
418  {
419  return result.get()->is_valid;
420  } else {
421  RCLCPP_INFO(get_logger(), "Failed to call is_path_valid service");
422  return false;
423  }
424 }
425 
426 bool PlannerTester::isCollisionFree(const ComputePathToPoseResult & path)
427 {
428  // At each point of the path, check if the corresponding cell is free
429 
430  // TODO(orduno): #443 for now we are assuming the robot is the size of a single cell
431  // costmap/world_model has consider the robot footprint
432 
433  // TODO(orduno): #443 Tweak criteria for defining if a path goes into obstacles.
434  // Current navfn planner will sometimes produce paths that cut corners
435  // i.e. some points are around the corner are actually inside the obstacle
436 
437  bool collisionFree = true;
438 
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)));
443 
444  if (!collisionFree) {
445  RCLCPP_WARN(
446  this->get_logger(), "Path has collision at (%.2f, %.2f)",
447  pose.pose.position.x, pose.pose.position.y);
448  printPath(path);
449  return false;
450  }
451  }
452 
453  RCLCPP_DEBUG(this->get_logger(), "Path has no collisions");
454  return true;
455 }
456 
457 bool PlannerTester::isWithinTolerance(
458  const geometry_msgs::msg::Point & robot_position,
459  const ComputePathToPoseCommand & goal,
460  const ComputePathToPoseResult & path) const
461 {
462  return isWithinTolerance(
463  robot_position, goal, path, 0.0, ComputePathToPoseResult());
464 }
465 
466 bool PlannerTester::isWithinTolerance(
467  const geometry_msgs::msg::Point & robot_position,
468  const ComputePathToPoseCommand & goal,
469  const ComputePathToPoseResult & path,
470  const double /*deviationTolerance*/,
471  const ComputePathToPoseResult & /*reference_path*/) const
472 {
473  // TODO(orduno) #443 Work in progress, for now we only check that the path start matches the
474  // robot start location and that the path end matches the goal.
475 
476  auto path_start = path.poses[0];
477  auto path_end = path.poses.end()[-1];
478 
479  if (
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)
484  {
485  RCLCPP_DEBUG(this->get_logger(), "Path has correct start and end points");
486 
487  return true;
488  }
489  RCLCPP_WARN(this->get_logger(), "Path deviates from requested start and end points");
490 
491  RCLCPP_DEBUG(
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);
494 
495  RCLCPP_DEBUG(
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);
499 
500  return false;
501 }
502 
503 void PlannerTester::printPath(const ComputePathToPoseResult & path) const
504 {
505  auto index = 0;
506  auto ss = std::stringstream{};
507 
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';
512  ++index;
513  }
514 
515  RCLCPP_INFO(get_logger(), ss.str().c_str());
516 }
517 
518 } // namespace nav2_system_tests
Class for a single layered costmap initialized from an occupancy grid representing the map.
Definition: costmap.hpp:45