Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
analytic_expansion_impl.hpp
1 // Copyright (c) 2021, Samsung Research America
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 #ifndef NAV2_SMAC_PLANNER__ANALYTIC_EXPANSION_IMPL_HPP_
16 #define NAV2_SMAC_PLANNER__ANALYTIC_EXPANSION_IMPL_HPP_
17 
18 #include <ompl/config.h>
19 
20 #include <algorithm>
21 #include <limits>
22 #include <memory>
23 #include <type_traits>
24 #include <vector>
25 
26 #include "nav2_smac_planner/analytic_expansion.hpp"
27 
28 namespace nav2_smac_planner
29 {
30 
31 template<typename NodeT>
33  const MotionModel & motion_model,
34  const SearchInfo & search_info,
35  const bool & traverse_unknown,
36  const unsigned int & dim_3_size)
37 : _motion_model(motion_model),
38  _search_info(search_info),
39  _traverse_unknown(traverse_unknown),
40  _dim_3_size(dim_3_size),
41  _collision_checker(nullptr)
42 {
43 }
44 
45 template<typename NodeT>
47  GridCollisionChecker * collision_checker)
48 {
49  _collision_checker = collision_checker;
50 }
51 
52 template<typename NodeT>
53 void AnalyticExpansion<NodeT>::setContext(NodeContext * ctx)
54 {
55  _ctx = ctx;
56 }
57 
58 template<typename NodeT>
59 typename AnalyticExpansion<NodeT>::NodePtr AnalyticExpansion<NodeT>::tryAnalyticExpansion(
60  const NodePtr & current_node,
61  const NodeVector & coarse_check_goals,
62  const NodeVector & fine_check_goals,
63  const CoordinateVector & goals_coords,
64  const NodeGetter & getter, int & analytic_iterations,
65  int & closest_distance)
66 {
67  // Node2D does not support analytic expansion
68  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
69  return NodePtr(nullptr);
70  } else {
71  // This must be a valid motion model for analytic expansion to be attempted
72  if (_motion_model == MotionModel::DUBIN || _motion_model == MotionModel::REEDS_SHEPP ||
73  _motion_model == MotionModel::STATE_LATTICE)
74  {
75  // See if we are closer and should be expanding more often
76  const Coordinates node_coords =
77  NodeT::getCoords(
78  current_node->getIndex(), _collision_checker->getCostmap()->getSizeInCellsX(), _dim_3_size);
79 
80  AnalyticExpansionNodes current_best_analytic_nodes;
81  NodePtr current_best_goal = nullptr;
82  NodePtr current_best_node = nullptr;
83  float current_best_score = std::numeric_limits<float>::max();
84 
85  closest_distance = std::min(
86  closest_distance,
87  static_cast<int>(current_node->getHeuristicCost(node_coords, goals_coords)));
88  // We want to expand at a rate of d/expansion_ratio,
89  // but check to see if we are so close that we would be expanding every iteration
90  // If so, limit it to the expansion ratio (rounded up)
91  int desired_iterations = std::max(
92  static_cast<int>(closest_distance / _search_info.analytic_expansion_ratio),
93  static_cast<int>(std::ceil(_search_info.analytic_expansion_ratio)));
94 
95  // If we are closer now, we should update the target number of iterations to go
96  analytic_iterations =
97  std::min(analytic_iterations, desired_iterations);
98 
99  // Always run the expansion on the first run in case there is a
100  // trivial path to be found
101  if (analytic_iterations <= 0) {
102  // Reset the counter and try the analytic path expansion
103  analytic_iterations = desired_iterations;
104  bool found_valid_expansion = false;
105 
106  // First check the coarse search resolution goals
107  for (auto & current_goal_node : coarse_check_goals) {
108  AnalyticExpansionNodes analytic_nodes =
109  getAnalyticPath(
110  current_node, current_goal_node, getter,
111  _ctx->motion_table.state_space);
112  if (!analytic_nodes.nodes.empty()) {
113  found_valid_expansion = true;
114  NodePtr node = current_node;
115  float score = refineAnalyticPath(
116  node, current_goal_node, getter, analytic_nodes);
117  // Update the best score if we found a better path
118  if (score < current_best_score) {
119  current_best_analytic_nodes = analytic_nodes;
120  current_best_goal = current_goal_node;
121  current_best_score = score;
122  current_best_node = node;
123  }
124  }
125  }
126 
127  // perform a final search if we found a goal
128  if (found_valid_expansion) {
129  for (auto & current_goal_node : fine_check_goals) {
130  AnalyticExpansionNodes analytic_nodes =
131  getAnalyticPath(
132  current_node, current_goal_node, getter,
133  _ctx->motion_table.state_space);
134  if (!analytic_nodes.nodes.empty()) {
135  NodePtr node = current_node;
136  float score = refineAnalyticPath(
137  node, current_goal_node, getter, analytic_nodes);
138  // Update the best score if we found a better path
139  if (score < current_best_score) {
140  current_best_analytic_nodes = analytic_nodes;
141  current_best_goal = current_goal_node;
142  current_best_score = score;
143  current_best_node = node;
144  }
145  }
146  }
147  }
148  }
149 
150  if (!current_best_analytic_nodes.nodes.empty()) {
151  return setAnalyticPath(
152  current_best_node, current_best_goal,
153  current_best_analytic_nodes);
154  }
155  analytic_iterations--;
156  }
157 
158  // No valid motion model - return nullptr
159  return NodePtr(nullptr);
160  }
161 }
162 
163 template<typename NodeT>
165 #if OMPL_VERSION_VALUE >= 2000000 // 2.0.0
166  const ompl::base::ReedsSheppStateSpace::PathType & path)
167 #else
168  const ompl::base::ReedsSheppStateSpace::ReedsSheppPath & path)
169 #endif
170 {
171  const double * lengths = path.length_;
172  int changes = 0;
173  int last_dir = 0;
174  for (int i = 0; i < 5; ++i) {
175  if (lengths[i] == 0.0) {
176  continue;
177  }
178 
179  int currentDirection = (lengths[i] > 0.0) ? 1 : -1;
180  if (last_dir != 0 && currentDirection != last_dir) {
181  ++changes;
182  }
183  last_dir = currentDirection;
184  }
185 
186  return changes;
187 }
188 
189 template<typename NodeT>
191  const NodePtr & node,
192  const NodePtr & goal,
193  const NodeGetter & node_getter,
194  const ompl::base::StateSpacePtr & state_space)
195 {
196  // Node2D does not support analytic expansion
197  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
198  return AnalyticExpansionNodes();
199  } else {
200  ompl::base::ScopedState<> from(state_space), to(state_space), s(state_space);
201  from[0] = node->pose.x;
202  from[1] = node->pose.y;
203  from[2] = _ctx->motion_table.getAngleFromBin(node->pose.theta);
204  to[0] = goal->pose.x;
205  to[1] = goal->pose.y;
206  to[2] = _ctx->motion_table.getAngleFromBin(goal->pose.theta);
207 
208  float d = state_space->distance(from(), to());
209 
210  auto rs_state_space = dynamic_cast<ompl::base::ReedsSheppStateSpace *>(state_space.get());
211  int direction_changes = 0;
212  if (rs_state_space) {
213  #if OMPL_VERSION_VALUE >= 2000000 // 2.0.0
214  direction_changes = countDirectionChanges(rs_state_space->getPath(from.get(), to.get()));
215  #else
216  direction_changes = countDirectionChanges(rs_state_space->reedsShepp(from.get(), to.get()));
217  #endif
218  }
219 
220  // A move of sqrt(2) is guaranteed to be in a new cell
221  static const float sqrt_2 = sqrtf(2.0f);
222 
223  // If the length is too far, exit. This prevents unsafe shortcutting of paths
224  // into higher cost areas far out from the goal itself, let search to the work of getting
225  // close before the analytic expansion brings it home. This should never be smaller than
226  // 4-5x the minimum turning radius being used, or planning times will begin to spike.
227  if (d > _search_info.analytic_expansion_max_length || d < sqrt_2) {
228  return AnalyticExpansionNodes();
229  }
230 
231  unsigned int num_intervals = static_cast<unsigned int>(std::floor(d / sqrt_2));
232 
233  AnalyticExpansionNodes possible_nodes;
234  // When "from" and "to" are zero or one cell away,
235  // num_intervals == 0
236  possible_nodes.nodes.reserve(num_intervals); // We won't store this node or the goal
237  std::vector<double> reals;
238  double theta;
239 
240  // Pre-allocate
241  NodePtr prev(node);
242  uint64_t index = 0;
243  NodePtr next(nullptr);
244  float angle = 0.0;
245  Coordinates proposed_coordinates;
246  bool failure = false;
247  std::vector<float> node_costs;
248  node_costs.reserve(num_intervals);
249 
250  // Check intermediary poses (non-goal, non-start)
251  for (float i = 1; i <= num_intervals; i++) {
252  state_space->interpolate(from(), to(), i / num_intervals, s());
253  reals = s.reals();
254  // Make sure in range [0, 2PI)
255  theta = (reals[2] < 0.0) ? (reals[2] + 2.0 * M_PI) : reals[2];
256  theta = (theta > 2.0 * M_PI) ? (theta - 2.0 * M_PI) : theta;
257  angle = _ctx->motion_table.getAngle(theta);
258 
259  // Turn the pose into a node, and check if it is valid
260  index = NodeT::getIndex(
261  static_cast<unsigned int>(reals[0]),
262  static_cast<unsigned int>(reals[1]),
263  static_cast<unsigned int>(angle),
264  _ctx->motion_table.size_x,
265  _ctx->motion_table.num_angle_quantization);
266  // Get the node from the graph
267  if (node_getter(index, next)) {
268  Coordinates initial_node_coords = next->pose;
269  proposed_coordinates = {static_cast<float>(reals[0]), static_cast<float>(reals[1]), angle};
270  next->setPose(proposed_coordinates);
271  if (next->isNodeValid(_traverse_unknown, _collision_checker) && next != prev) {
272  // Save the node, and its previous coordinates in case we need to abort
273  possible_nodes.add(next, initial_node_coords, proposed_coordinates);
274  node_costs.emplace_back(next->getCost());
275  prev = next;
276  } else {
277  // Abort
278  next->setPose(initial_node_coords);
279  failure = true;
280  break;
281  }
282  } else {
283  // Abort
284  failure = true;
285  break;
286  }
287  }
288 
289  if (!failure) {
290  // We found 'a' valid expansion. Now to tell if its a quality option...
291  const float max_cost = _search_info.analytic_expansion_max_cost;
292  auto max_cost_it = std::max_element(node_costs.begin(), node_costs.end());
293  if (max_cost_it != node_costs.end() && *max_cost_it > max_cost) {
294  // If any element is above the comfortable cost limit, check edge cases:
295  // (1) Check if goal is in greater than max_cost space requiring
296  // entering it, but only entering it on final approach, not in-and-out
297  // (2) Checks if goal is in normal space, but enters costed space unnecessarily
298  // mid-way through, skirting obstacle or in non-globally confined space
299  bool cost_exit_high_cost_region = false;
300  for (auto iter = node_costs.rbegin(); iter != node_costs.rend(); ++iter) {
301  const float & curr_cost = *iter;
302  if (curr_cost <= max_cost) {
303  cost_exit_high_cost_region = true;
304  } else if (curr_cost > max_cost && cost_exit_high_cost_region) {
305  failure = true;
306  break;
307  }
308  }
309 
310  // (3) Handle exception: there may be no other option close to goal
311  // if max cost is set too low (optional)
312  if (failure) {
313  if (d < 2.0f * M_PI * _ctx->motion_table.min_turning_radius &&
314  _search_info.analytic_expansion_max_cost_override)
315  {
316  failure = false;
317  }
318  }
319  }
320  }
321 
322  // Reset to initial poses to not impact future searches
323  for (const auto & node_pose : possible_nodes.nodes) {
324  const auto & n = node_pose.node;
325  n->setPose(node_pose.initial_coords);
326  }
327 
328  if (failure) {
329  return AnalyticExpansionNodes();
330  }
331 
332  possible_nodes.setDirectionChanges(direction_changes);
333  return possible_nodes;
334  }
335 }
336 
337 template<typename NodeT>
339  NodePtr & node,
340  const NodePtr & goal_node,
341  const NodeGetter & getter,
342  AnalyticExpansionNodes & analytic_nodes)
343 {
344  // Node2D does not support analytic expansion
345  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
346  return std::numeric_limits<float>::max();
347  } else {
348  NodePtr test_node = node;
349  AnalyticExpansionNodes refined_analytic_nodes;
350  for (int i = 0; i < 8; i++) {
351  // Attempt to create better paths in 5 node increments, need to make sure
352  // they exist for each in order to do so (maximum of 40 points back).
353  if (test_node->parent && test_node->parent->parent &&
354  test_node->parent->parent->parent &&
355  test_node->parent->parent->parent->parent &&
356  test_node->parent->parent->parent->parent->parent)
357  {
358  test_node = static_cast<NodePtr>(test_node->parent->parent->parent->parent->parent);
359  // print the goals pose
360  refined_analytic_nodes =
361  getAnalyticPath(
362  test_node, goal_node, getter,
363  _ctx->motion_table.state_space);
364  if (refined_analytic_nodes.nodes.empty()) {
365  break;
366  }
367  if (refined_analytic_nodes.direction_changes > analytic_nodes.direction_changes) {
368  // If the direction changes are worse, we don't want to use this path
369  continue;
370  }
371  analytic_nodes = refined_analytic_nodes;
372  node = test_node;
373  } else {
374  break;
375  }
376  }
377 
378  // The analytic expansion can short-cut near obstacles when closer to a goal
379  // So, we can attempt to refine it more by increasing the possible radius
380  // higher than the minimum turning radius and use the best solution based on
381  // a scoring function similar to that used in traversal cost estimation.
382  auto scoringFn = [&](const AnalyticExpansionNodes & expansion) {
383  if (expansion.nodes.size() < 2) {
384  return std::numeric_limits<float>::max();
385  }
386 
387  float score = 0.0;
388  float normalized_cost = 0.0;
389  // Analytic expansions are consistently spaced
390  const float distance = hypotf(
391  expansion.nodes[1].proposed_coords.x - expansion.nodes[0].proposed_coords.x,
392  expansion.nodes[1].proposed_coords.y - expansion.nodes[0].proposed_coords.y);
393  const float & weight = _ctx->motion_table.cost_penalty;
394  for (auto iter = expansion.nodes.begin(); iter != expansion.nodes.end(); ++iter) {
395  normalized_cost = iter->node->getCost() / 252.0f;
396  // Search's Traversal Cost Function
397  score += distance * (1.0 + weight * normalized_cost);
398  }
399  return score;
400  };
401 
402  float original_score = scoringFn(analytic_nodes);
403  float best_score = original_score;
404  float score = std::numeric_limits<float>::max();
405  float min_turn_rad = _ctx->motion_table.min_turning_radius;
406  const float max_min_turn_rad = 4.0 * min_turn_rad; // Up to 4x the turning radius
407 
408  // SE2 produces straight-line paths independent of turning radius, skip refinement
409  if (_ctx->motion_table.motion_model == MotionModel::OMNI) {
410  return best_score;
411  }
412 
413  while (min_turn_rad < max_min_turn_rad) {
414  min_turn_rad += 0.5; // In Grid Coords, 1/2 cell steps
415  ompl::base::StateSpacePtr state_space;
416  if (_ctx->motion_table.motion_model == MotionModel::DUBIN) {
417  state_space = std::make_shared<ompl::base::DubinsStateSpace>(min_turn_rad);
418  } else {
419  state_space = std::make_shared<ompl::base::ReedsSheppStateSpace>(min_turn_rad);
420  }
421  refined_analytic_nodes = getAnalyticPath(node, goal_node, getter, state_space);
422  score = scoringFn(refined_analytic_nodes);
423 
424  // Normal scoring: prioritize lower cost as long as not more directional changes
425  if (score <= best_score &&
426  refined_analytic_nodes.direction_changes <= analytic_nodes.direction_changes)
427  {
428  analytic_nodes = refined_analytic_nodes;
429  best_score = score;
430  continue;
431  }
432 
433  // Special case: If we have a better score than original (only) and less directional changes
434  // the path quality is still better than the original and is less operationally complex
435  if (score <= original_score &&
436  refined_analytic_nodes.direction_changes < analytic_nodes.direction_changes)
437  {
438  analytic_nodes = refined_analytic_nodes;
439  best_score = score;
440  }
441  }
442 
443  return best_score;
444  }
445 }
446 
447 template<typename NodeT>
448 typename AnalyticExpansion<NodeT>::NodePtr AnalyticExpansion<NodeT>::setAnalyticPath(
449  const NodePtr & node,
450  const NodePtr & goal_node,
451  const AnalyticExpansionNodes & expanded_nodes)
452 {
453  // Node2D does not support analytic expansion
454  if constexpr (std::is_base_of_v<Node2D, NodeT>) {
455  return NodePtr(nullptr);
456  } else {
457  _detached_nodes.clear();
458  // Legitimate final path - set the parent relationships, states, and poses
459  NodePtr prev = node;
460  for (const auto & node_pose : expanded_nodes.nodes) {
461  auto n = node_pose.node;
462  cleanNode(n);
463  if (n->getIndex() != goal_node->getIndex()) {
464  if (n->wasVisited()) {
465  _detached_nodes.push_back(std::make_unique<NodeT>(-1, _ctx));
466  n = _detached_nodes.back().get();
467  }
468  n->parent = prev;
469  n->pose = node_pose.proposed_coords;
470  n->visited();
471  prev = n;
472  }
473  }
474  if (goal_node != prev) {
475  goal_node->parent = prev;
476  cleanNode(goal_node);
477  goal_node->visited();
478  }
479  return goal_node;
480  }
481 }
482 
483 template<typename NodeT>
484 void AnalyticExpansion<NodeT>::cleanNode(const NodePtr & node)
485 {
486  // NodeLattice needs to clear the motion primitive pointer
487  if constexpr (std::is_base_of_v<NodeLattice, NodeT>) {
488  node->setMotionPrimitive(nullptr);
489  }
490 }
491 
492 } // namespace nav2_smac_planner
493 
494 #endif // NAV2_SMAC_PLANNER__ANALYTIC_EXPANSION_IMPL_HPP_
AnalyticExpansionNodes getAnalyticPath(const NodePtr &node, const NodePtr &goal, const NodeGetter &getter, const ompl::base::StateSpacePtr &state_space)
Perform an analytic path expansion to the goal.
NodePtr tryAnalyticExpansion(const NodePtr &current_node, const NodeVector &coarse_check_goals, const NodeVector &fine_check_goals, const CoordinateVector &goals_coords, const NodeGetter &getter, int &iterations, int &closest_distance)
Attempt an analytic path completion.
int countDirectionChanges(const ompl::base::ReedsSheppStateSpace::ReedsSheppPath &path)
Counts the number of direction changes in a Reeds-Shepp path.
void cleanNode(const NodePtr &nodes)
Takes an expanded nodes to clean up, if necessary, of any state information that may be polluting it ...
void setCollisionChecker(GridCollisionChecker *collision_checker)
Sets the collision checker and costmap to use in expansion validation.
AnalyticExpansion(const MotionModel &motion_model, const SearchInfo &search_info, const bool &traverse_unknown, const unsigned int &dim_3_size)
Constructor for analytic expansion object.
float refineAnalyticPath(NodePtr &node, const NodePtr &goal_node, const NodeGetter &getter, AnalyticExpansionNodes &analytic_nodes)
Refined analytic path from the current node to the goal.
void setContext(NodeContext *ctx)
Sets the shared context to use.
NodePtr setAnalyticPath(const NodePtr &node, const NodePtr &goal, const AnalyticExpansionNodes &expanded_nodes)
Takes final analytic expansion and appends to current expanded node.
A costmap grid collision checker.
Analytic expansion nodes and associated metadata.
Search properties and penalties.
Definition: types.hpp:38