Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
a_star_impl.hpp
1 // Copyright (c) 2020, Samsung Research America
2 // Copyright (c) 2020, Applied Electric Vehicles Pty Ltd
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License. Reserved.
15 
16 #ifndef NAV2_SMAC_PLANNER__A_STAR_IMPL_HPP_
17 #define NAV2_SMAC_PLANNER__A_STAR_IMPL_HPP_
18 
19 #include <algorithm>
20 #include <chrono>
21 #include <cmath>
22 #include <limits>
23 #include <memory>
24 #include <stdexcept>
25 #include <thread>
26 #include <tuple>
27 #include <type_traits>
28 #include <utility>
29 #include <vector>
30 
31 #include "nav2_smac_planner/a_star.hpp"
32 
33 namespace nav2_smac_planner
34 {
35 using namespace std::chrono; // NOLINT
36 
37 template<typename NodeT>
39  const MotionModel & motion_model,
40  const SearchInfo & search_info)
41 : _traverse_unknown(true),
42  _is_initialized(false),
43  _max_iterations(0),
44  _terminal_checking_interval(5000),
45  _max_planning_time(0),
46  _x_size(0),
47  _y_size(0),
48  _search_info(search_info),
49  _start(nullptr),
50  _goal_manager(GoalManagerT()),
51  _motion_model(motion_model)
52 {
53  _graph.reserve(100000);
54 }
55 
56 template<typename NodeT>
58 {
59 }
60 
61 template<typename NodeT>
63  const bool & allow_unknown,
64  int & max_iterations,
65  const int & max_on_approach_iterations,
66  const int & terminal_checking_interval,
67  const double & max_planning_time,
68  const float & lookup_table_size,
69  const unsigned int & dim_3_size)
70 {
71  _traverse_unknown = allow_unknown;
72  _max_iterations = max_iterations;
73  _max_on_approach_iterations = max_on_approach_iterations;
74  _terminal_checking_interval = terminal_checking_interval;
75  _max_planning_time = max_planning_time;
76 
77  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
78  // Node2D-specific initialization: no distance heuristic precomputation
79  _shared_ctx = std::make_shared<NodeContext>();
80  if (dim_3_size != 1) {
81  throw std::runtime_error("Node type Node2D cannot be given non-1 dim 3 quantization.");
82  }
83  } else {
84  // SE2 node initialization: precompute distance heuristic
85  if (!_is_initialized) {
86  _shared_ctx = std::make_shared<NodeContext>();
87  _shared_ctx->distance_heuristic->precomputeDistanceHeuristic(
88  lookup_table_size, _motion_model,
89  dim_3_size,
90  _search_info, _shared_ctx->motion_table);
91  }
92  }
93 
94  _is_initialized = true;
95  _dim3_size = dim_3_size;
96  _expander = std::make_unique<AnalyticExpansion<NodeT>>(
97  _motion_model, _search_info, _traverse_unknown, _dim3_size);
98 }
99 
100 template<typename NodeT>
102 {
103  _collision_checker = collision_checker;
104  _costmap = collision_checker->getCostmap();
105  unsigned int x_size = _costmap->getSizeInCellsX();
106  unsigned int y_size = _costmap->getSizeInCellsY();
107 
108  clearGraph();
109 
110  if (getSizeX() != x_size || getSizeY() != y_size) {
111  _x_size = x_size;
112  _y_size = y_size;
113  }
114 
115  // Always refresh the motion model so dynamic penalty parameters take effect immediately
116  NodeT::initMotionModel(
117  _shared_ctx.get(), _motion_model, _x_size, _y_size, _dim3_size,
118  _search_info);
119 
120  // Always set context pointers to ensure newly allocated objects get their contexts restored
121  _goal_manager.setContext(_shared_ctx.get());
122  _expander->setContext(_shared_ctx.get());
123  _expander->setCollisionChecker(_collision_checker);
124 }
125 
126 template<typename NodeT>
127 typename AStarAlgorithm<NodeT>::NodePtr AStarAlgorithm<NodeT>::addToGraph(
128  const uint64_t & index)
129 {
130  auto iter = _graph.find(index);
131  if (iter != _graph.end()) {
132  return &(iter->second);
133  }
134 
135  return &(_graph.emplace(index, NodeT(index, _shared_ctx.get())).first->second);
136 }
137 
138 template<typename NodeT>
140  const float & mx,
141  const float & my,
142  const unsigned int & dim_3)
143 {
144  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
145  // Node2D-specific: different getIndex signature, no pose setting
146  if (dim_3 != 0) {
147  throw std::runtime_error("Node type Node2D cannot be given non-zero starting dim 3.");
148  }
149  _start = addToGraph(
151  static_cast<unsigned int>(mx),
152  static_cast<unsigned int>(my),
153  getSizeX()));
154  } else {
155  // SE2 node: use full index and set pose
156  _start = addToGraph(
157  getIndex(
158  static_cast<unsigned int>(mx),
159  static_cast<unsigned int>(my),
160  dim_3));
161  _start->setPose(Coordinates(mx, my, dim_3));
162  }
163 }
164 
165 template<typename NodeT>
167  const NodePtr & node,
168  std::vector<std::tuple<float, float, float>> * expansions_log)
169 {
170  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
171  // Node2D: no theta
172  Node2D::Coordinates coords = node->getCoords(node->getIndex());
173  expansions_log->emplace_back(
174  _costmap->getOriginX() + ((coords.x + 0.5) * _costmap->getResolution()),
175  _costmap->getOriginY() + ((coords.y + 0.5) * _costmap->getResolution()),
176  0.0);
177  } else {
178  // SE2 node: include theta
179  typename NodeT::Coordinates coords = node->pose;
180  expansions_log->emplace_back(
181  _costmap->getOriginX() + ((coords.x + 0.5) * _costmap->getResolution()),
182  _costmap->getOriginY() + ((coords.y + 0.5) * _costmap->getResolution()),
183  _shared_ctx->motion_table.getAngleFromBin(coords.theta));
184  }
185 }
186 
187 template<typename NodeT>
189  const float & mx,
190  const float & my,
191  const unsigned int & dim_3,
192  const GoalHeadingMode & goal_heading_mode,
193  const int & coarse_search_resolution)
194 {
195  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
196  // Node2D-specific: simplified goal setting, no heading modes
197  if (dim_3 != 0) {
198  throw std::runtime_error("Node type Node2D cannot be given non-zero goal dim 3.");
199  }
200  _goal_manager.clear();
201  auto goal = addToGraph(
203  static_cast<unsigned int>(mx),
204  static_cast<unsigned int>(my),
205  getSizeX()));
206 
207  goal->setPose(Node2D::Coordinates(mx, my));
208  _goal_manager.addGoal(goal);
209 
210  _coarse_search_resolution = 1;
211  } else {
212  // SE2 node: full goal handling with heading modes
213  // Default to minimal resolution unless overridden for ALL_DIRECTION
214  _coarse_search_resolution = 1;
215 
216  _goal_manager.clear();
217  Coordinates ref_goal_coord(mx, my, static_cast<float>(dim_3));
218 
219  if (!_search_info.cache_obstacle_heuristic ||
220  _goal_manager.hasGoalChanged(ref_goal_coord))
221  {
222  if (!_start) {
223  throw std::runtime_error("Start must be set before goal.");
224  }
225 
226  _shared_ctx->obstacle_heuristic->resetObstacleHeuristic(
227  _collision_checker->getCostmapROS(), _start->pose.x, _start->pose.y, mx, my,
228  _shared_ctx->motion_table.downsample_obstacle_heuristic);
229  }
230 
231  _goal_manager.setRefGoalCoordinates(ref_goal_coord);
232 
233  unsigned int num_bins = _shared_ctx->motion_table.num_angle_quantization;
234  // set goal based on heading mode
235  switch (goal_heading_mode) {
236  case GoalHeadingMode::DEFAULT:
237  {
238  // add a single goal node with single heading
239  auto goal = addToGraph(
240  getIndex(
241  static_cast<unsigned int>(mx),
242  static_cast<unsigned int>(my),
243  dim_3));
244  goal->setPose(typename NodeT::Coordinates(mx, my, static_cast<float>(dim_3)));
245  _goal_manager.addGoal(goal);
246  break;
247  }
248 
249  case GoalHeadingMode::BIDIRECTIONAL:
250  {
251  // Add two goals, one for each direction
252  // add goal in original direction
253  auto goal = addToGraph(
254  getIndex(
255  static_cast<unsigned int>(mx),
256  static_cast<unsigned int>(my),
257  dim_3));
258  goal->setPose(typename NodeT::Coordinates(mx, my, static_cast<float>(dim_3)));
259  _goal_manager.addGoal(goal);
260 
261  // Add goal node in opposite (180°) direction
262  unsigned int opposite_heading = (dim_3 + (num_bins / 2)) % num_bins;
263  auto opposite_goal = addToGraph(
264  getIndex(
265  static_cast<unsigned int>(mx),
266  static_cast<unsigned int>(my),
267  opposite_heading));
268  opposite_goal->setPose(
269  typename NodeT::Coordinates(mx, my, static_cast<float>(opposite_heading)));
270  _goal_manager.addGoal(opposite_goal);
271  break;
272  }
273 
274  case GoalHeadingMode::ALL_DIRECTION:
275  {
276  // Set the coarse search resolution only for all direction
277  _coarse_search_resolution = coarse_search_resolution;
278 
279  // Add goal nodes for all headings
280  for (unsigned int i = 0; i < num_bins; ++i) {
281  auto goal = addToGraph(
282  getIndex(
283  static_cast<unsigned int>(mx),
284  static_cast<unsigned int>(my),
285  i));
286  goal->setPose(typename NodeT::Coordinates(mx, my, static_cast<float>(i)));
287  _goal_manager.addGoal(goal);
288  }
289  break;
290  }
291  case GoalHeadingMode::UNKNOWN:
292  throw std::runtime_error("Goal heading is UNKNOWN.");
293  }
294  }
295 }
296 
297 template<typename NodeT>
299 {
300  // Check if graph was filled in
301  if (_graph.empty()) {
302  throw std::runtime_error("Failed to compute path, no costmap given.");
303  }
304 
305  // Check if points were filled in
306  if (!_start || _goal_manager.goalsIsEmpty()) {
307  throw std::runtime_error("Failed to compute path, no valid start or goal given.");
308  }
309 
310  // remove invalid goals
311  _goal_manager.removeInvalidGoals(getToleranceHeuristic(), _collision_checker, _traverse_unknown);
312 
313  // Check if ending point is valid
314  if (_goal_manager.getGoalsSet().empty()) {
315  throw nav2_core::GoalOccupied("Goal was in lethal cost");
316  }
317 
318  // Note: We do not check the if the start is valid because it is cleared
319  return true;
320 }
321 
322 template<typename NodeT>
324 {
325  if (_best_heuristic_node.first < getToleranceHeuristic()) {
326  _graph.at(_best_heuristic_node.second).backtracePath(path);
327  return true;
328  }
329 
330  return false;
331 }
332 
333 template<typename NodeT>
335  CoordinateVector & path, int & iterations,
336  const float & tolerance,
337  std::function<bool()> cancel_checker,
338  std::vector<std::tuple<float, float, float>> * expansions_log)
339 {
340  steady_clock::time_point start_time = steady_clock::now();
341  _tolerance = tolerance;
342  _best_heuristic_node = {std::numeric_limits<float>::max(), 0};
343  clearQueue();
344 
345  if (!areInputsValid()) {
346  return false;
347  }
348 
349  NodeVector coarse_check_goals, fine_check_goals;
350  _goal_manager.prepareGoalsForAnalyticExpansion(
351  coarse_check_goals, fine_check_goals,
352  _coarse_search_resolution);
353 
354  // 0) Add starting point to the open set
355  addNode(0.0, getStart());
356  getStart()->setAccumulatedCost(0.0);
357 
358  // Optimization: preallocate all variables
359  NodePtr current_node = nullptr;
360  NodePtr neighbor = nullptr;
361  NodePtr expansion_result = nullptr;
362  float g_cost = 0.0;
363  NodeVector neighbors;
364  int approach_iterations = 0;
365  NeighborIterator neighbor_iterator;
366  int analytic_iterations = 0;
367  int closest_distance = std::numeric_limits<int>::max();
368 
369  // Given an index, return a node ptr reference if its collision-free and valid
370  const uint64_t max_index = static_cast<uint64_t>(getSizeX()) *
371  static_cast<uint64_t>(getSizeY()) *
372  static_cast<uint64_t>(getSizeDim3());
373  NodeGetter neighborGetter =
374  [&, this](const uint64_t & index, NodePtr & neighbor_rtn) -> bool
375  {
376  if (index >= max_index) {
377  return false;
378  }
379 
380  neighbor_rtn = addToGraph(index);
381  return true;
382  };
383 
384  while (iterations < getMaxIterations() && !_queue.empty()) {
385  // Check for planning timeout and cancel only on every Nth iteration
386  if (iterations % _terminal_checking_interval == 0) {
387  if (cancel_checker()) {
388  throw nav2_core::PlannerCancelled("Planner was cancelled");
389  }
390  std::chrono::duration<double> planning_duration =
391  std::chrono::duration_cast<std::chrono::duration<double>>(steady_clock::now() - start_time);
392  if (static_cast<double>(planning_duration.count()) >= _max_planning_time) {
393  // In case of timeout, return the path that is closest, if within tolerance.
394  return getClosestPathWithinTolerance(path);
395  }
396  }
397 
398  // 1) Pick Nbest from O s.t. min(f(Nbest)), remove from queue
399  current_node = getNextNode();
400 
401  // Save current node coordinates for debug
402  if (expansions_log) {
403  populateExpansionsLog(current_node, expansions_log);
404  }
405 
406  // We allow for nodes to be queued multiple times in case
407  // shorter paths result in it, but we can visit only once
408  // Also a chance to perform last-checks necessary.
409  if (onVisitationCheckNode(current_node)) {
410  continue;
411  }
412 
413  iterations++;
414 
415  // 2) Mark Nbest as visited
416  current_node->visited();
417 
418  // 2.1) Use an analytic expansion (if available) to generate a path
419  expansion_result = nullptr;
420  expansion_result = _expander->tryAnalyticExpansion(
421  current_node, coarse_check_goals, fine_check_goals,
422  _goal_manager.getGoalsCoordinates(), neighborGetter, analytic_iterations, closest_distance);
423  if (expansion_result != nullptr) {
424  current_node = expansion_result;
425  }
426 
427  // 3) Check if we're at the goal, backtrace if required
428  if (_goal_manager.isGoal(current_node)) {
429  return current_node->backtracePath(path);
430  } else if (_best_heuristic_node.first < getToleranceHeuristic()) {
431  // Optimization: Let us find when in tolerance and refine within reason
432  approach_iterations++;
433  if (approach_iterations >= getOnApproachMaxIterations()) {
434  return _graph.at(_best_heuristic_node.second).backtracePath(path);
435  }
436  }
437 
438  // 4) Expand neighbors of Nbest not visited
439  neighbors.clear();
440  current_node->getNeighbors(neighborGetter, _collision_checker, _traverse_unknown, neighbors);
441 
442  for (neighbor_iterator = neighbors.begin();
443  neighbor_iterator != neighbors.end(); ++neighbor_iterator)
444  {
445  neighbor = *neighbor_iterator;
446 
447  // 4.1) Compute the cost to go to this node
448  g_cost = current_node->getAccumulatedCost() + current_node->getTraversalCost(neighbor);
449 
450  // 4.2) If this is a lower cost than prior, we set this as the new cost and new approach
451  if (g_cost < neighbor->getAccumulatedCost()) {
452  neighbor->setAccumulatedCost(g_cost);
453  neighbor->parent = current_node;
454 
455  // 4.3) Add to queue with heuristic cost
456  addNode(g_cost + getHeuristicCost(neighbor), neighbor);
457  }
458  }
459  }
460 
461  // If we run out of search options, return the path that is closest, if within tolerance.
462  return getClosestPathWithinTolerance(path);
463 }
464 
465 template<typename NodeT>
466 typename AStarAlgorithm<NodeT>::NodePtr & AStarAlgorithm<NodeT>::getStart()
467 {
468  return _start;
469 }
470 
471 template<typename NodeT>
472 typename AStarAlgorithm<NodeT>::NodePtr AStarAlgorithm<NodeT>::getNextNode()
473 {
474  NodeBasic<NodeT> node = _queue.top().second;
475  _queue.pop();
476  node.processSearchNode();
477  return node.graph_node_ptr;
478 }
479 
480 template<typename NodeT>
481 void AStarAlgorithm<NodeT>::addNode(const float & cost, NodePtr & node)
482 {
483  NodeBasic<NodeT> queued_node(node->getIndex());
484  queued_node.populateSearchNode(node);
485  _queue.emplace(cost, queued_node);
486 }
487 
488 template<typename NodeT>
489 float AStarAlgorithm<NodeT>::getHeuristicCost(const NodePtr & node)
490 {
491  const Coordinates node_coords =
492  NodeT::getCoords(node->getIndex(), getSizeX(), getSizeDim3());
493  float heuristic = node->getHeuristicCost(node_coords, _goal_manager.getGoalsCoordinates());
494  if (heuristic < _best_heuristic_node.first) {
495  _best_heuristic_node = {heuristic, node->getIndex()};
496  }
497 
498  return heuristic;
499 }
500 
501 template<typename NodeT>
502 bool AStarAlgorithm<NodeT>::onVisitationCheckNode(const NodePtr & current_node)
503 {
504  return current_node->wasVisited();
505 }
506 
507 template<typename NodeT>
509 {
510  NodeQueue q;
511  std::swap(_queue, q);
512 }
513 
514 template<typename NodeT>
516 {
517  Graph g;
518  std::swap(_graph, g);
519  _graph.reserve(100000);
520 }
521 
522 template<typename NodeT>
524  const unsigned int & x, const unsigned int & y,
525  const unsigned int & dim_3)
526 {
527  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
528  return Node2D::getIndex(x, y, dim_3);
529  } else {
530  return NodeT::getIndex(
531  x, y, dim_3, _shared_ctx->motion_table.size_x,
532  _shared_ctx->motion_table.num_angle_quantization);
533  }
534 }
535 
536 template<typename NodeT>
538 {
539  return _max_iterations;
540 }
541 
542 template<typename NodeT>
544 {
545  return _max_on_approach_iterations;
546 }
547 
548 template<typename NodeT>
550 {
551  return _tolerance;
552 }
553 
554 template<typename NodeT>
556 {
557  return _x_size;
558 }
559 
560 template<typename NodeT>
562 {
563  return _y_size;
564 }
565 
566 template<typename NodeT>
568 {
569  return _dim3_size;
570 }
571 
572 template<typename NodeT>
574 {
575  return _coarse_search_resolution;
576 }
577 
578 template<typename NodeT>
580 {
581  return _goal_manager;
582 }
583 
584 template<typename NodeT>
585 typename AStarAlgorithm<NodeT>::NodeContext * AStarAlgorithm<NodeT>::getContext()
586 {
587  return _shared_ctx.get();
588 }
589 
590 } // namespace nav2_smac_planner
591 
592 #endif // NAV2_SMAC_PLANNER__A_STAR_IMPL_HPP_
unsigned int getSizeInCellsX() const
Accessor for the x size of the costmap in cells.
Definition: costmap_2d.cpp:548
CostmapT getCostmap()
Get the current costmap object.
~AStarAlgorithm()
A destructor for nav2_smac_planner::AStarAlgorithm.
Definition: a_star_impl.hpp:57
unsigned int & getSizeDim3()
Get number of angle quantization bins (SE2) or Z coordinate (XYZ)
unsigned int getCoarseSearchResolution()
Get the resolution of the coarse search.
bool createPath(CoordinateVector &path, int &num_iterations, const float &tolerance, std::function< bool()> cancel_checker, std::vector< std::tuple< float, float, float >> *expansions_log=nullptr)
Creating path from given costmap, start, and goal.
int & getOnApproachMaxIterations()
Get maximum number of on-approach iterations after within threshold.
bool onVisitationCheckNode(const NodePtr &node)
Check if node has been visited.
void setCollisionChecker(GridCollisionChecker *collision_checker)
Sets the collision checker to use.
void initialize(const bool &allow_unknown, int &max_iterations, const int &max_on_approach_iterations, const int &terminal_checking_interval, const double &max_planning_time, const float &lookup_table_size, const unsigned int &dim_3_size)
Initialization of the planner with defaults.
Definition: a_star_impl.hpp:62
NodeContext * getContext()
Get pointer to shared node context.
NodePtr getNextNode()
Get pointer to next goal in open set.
bool areInputsValid()
Check if inputs to planner are valid.
void clearQueue()
Clear heuristic queue of nodes to search.
AStarAlgorithm(const MotionModel &motion_model, const SearchInfo &search_info)
A constructor for nav2_smac_planner::AStarAlgorithm.
Definition: a_star_impl.hpp:38
void populateExpansionsLog(const NodePtr &node, std::vector< std::tuple< float, float, float >> *expansions_log)
Populate a debug log of expansions for Hybrid-A* for visualization.
bool getClosestPathWithinTolerance(CoordinateVector &path)
Get the closest path within tolerance if available.
int & getMaxIterations()
Get maximum number of iterations to plan.
uint64_t getIndex(const unsigned int &x, const unsigned int &y, const unsigned int &dim3)
Get index at coordinates.
void clearGraph()
Clear graph of nodes searched.
unsigned int & getSizeY()
Get size of graph in Y.
void setStart(const float &mx, const float &my, const unsigned int &dim_3)
Set the starting pose for planning, as a node index.
void addNode(const float &cost, NodePtr &node)
Add a node to the open set.
GoalManagerT getGoalManager()
Get the goals manager class.
unsigned int & getSizeX()
Get size of graph in X.
float getHeuristicCost(const NodePtr &node)
Get cost of heuristic of node.
float & getToleranceHeuristic()
Get tolerance, in node nodes.
NodePtr & getStart()
Get pointer reference to starting node.
NodePtr addToGraph(const uint64_t &index)
Adds node to graph.
void setGoal(const float &mx, const float &my, const unsigned int &dim_3, const GoalHeadingMode &goal_heading_mode=GoalHeadingMode::DEFAULT, const int &coarse_search_resolution=1)
Set the goal for planning, as a node index.
Responsible for managing multiple variables storing information on the goal.
A costmap grid collision checker.
uint64_t getIndex()
Gets cell index.
Definition: node_2d.hpp:159
NodeBasic implementation for priority queue insertion.
Definition: node_basic.hpp:36
void populateSearchNode(NodeT *&node)
Take a NodeBasic and populate it with any necessary state cached in the queue for NodeT.
Definition: node_basic.hpp:53
void processSearchNode()
Take a NodeBasic and populate it with any necessary state cached in the queue for NodeTs.
Definition: node_basic.hpp:82
Search properties and penalties.
Definition: types.hpp:38