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