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