Skip to content

Commit e2781e5

Browse files
Prevent analytic expansions from shortcutting Smac Planner feasible paths (#3962)
* a potential solution to smac shortcutting * costmap reoslution * some fixes * completed prototype * some fixes for collision detection and performance * completing shortcutting fix * updating tests * adding readme --------- Signed-off-by: Steve Macenski <[email protected]>
1 parent 6a540f1 commit e2781e5

File tree

14 files changed

+167
-43
lines changed

14 files changed

+167
-43
lines changed

nav2_smac_planner/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ planner_server:
115115
angle_quantization_bins: 64 # For Hybrid nodes: Number of angle bins for search, must be 1 for 2D node (no angle search)
116116
analytic_expansion_ratio: 3.5 # For Hybrid/Lattice nodes: The ratio to attempt analytic expansions during search for final approach.
117117
analytic_expansion_max_length: 3.0 # For Hybrid/Lattice nodes: The maximum length of the analytic expansion to be considered valid to prevent unsafe shortcutting (in meters). This should be scaled with minimum turning radius and be no less than 4-5x the minimum radius
118+
analytic_expansion_max_cost: true # For Hybrid/Lattice nodes: The maximum single cost for any part of an analytic expansion to contain and be valid (except when necessary on approach to goal)
119+
analytic_expansion_max_cost_override: false # For Hybrid/Lattice nodes: Whether or not to override the maximum cost setting if within critical distance to goal (ie probably required)
118120
minimum_turning_radius: 0.40 # For Hybrid/Lattice nodes: minimum turning radius in m of path / vehicle
119121
reverse_penalty: 2.1 # For Reeds-Shepp model: penalty to apply if motion is reversing, must be => 1
120122
change_penalty: 0.0 # For Hybrid nodes: penalty to apply if motion is changing directions, must be >= 0

nav2_smac_planner/include/nav2_smac_planner/analytic_expansion.hpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class AnalyticExpansion
7373
* @brief Sets the collision checker and costmap to use in expansion validation
7474
* @param collision_checker Collision checker to use
7575
*/
76-
void setCollisionChecker(GridCollisionChecker * collision_checker);
76+
void setCollisionChecker(GridCollisionChecker * & collision_checker);
7777

7878
/**
7979
* @brief Attempt an analytic path completion
@@ -95,11 +95,12 @@ class AnalyticExpansion
9595
* @param node The node to start the analytic path from
9696
* @param goal The goal node to plan to
9797
* @param getter The function object that gets valid nodes from the graph
98+
* @param state_space State space to use for computing analytic expansions
9899
* @return A set of analytically expanded nodes to the goal from current node, if possible
99100
*/
100101
AnalyticExpansionNodes getAnalyticPath(
101102
const NodePtr & node, const NodePtr & goal,
102-
const NodeGetter & getter);
103+
const NodeGetter & getter, const ompl::base::StateSpacePtr & state_space);
103104

104105
/**
105106
* @brief Takes final analytic expansion and appends to current expanded node

nav2_smac_planner/include/nav2_smac_planner/collision_checker.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,10 @@ class GridCollisionChecker
116116
protected:
117117
std::vector<nav2_costmap_2d::Footprint> oriented_footprints_;
118118
nav2_costmap_2d::Footprint unoriented_footprint_;
119-
double footprint_cost_;
119+
float footprint_cost_;
120120
bool footprint_is_radius_;
121121
std::vector<float> angles_;
122-
double possible_inscribed_cost_{-1};
122+
float possible_inscribed_cost_{-1};
123123
rclcpp::Logger logger_{rclcpp::get_logger("SmacPlannerCollisionChecker")};
124124
rclcpp::Clock::SharedPtr clock_;
125125
};

nav2_smac_planner/include/nav2_smac_planner/node_2d.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class Node2D
8787
* @brief Gets the accumulated cost at this node
8888
* @return accumulated cost
8989
*/
90-
inline float & getAccumulatedCost()
90+
inline float getAccumulatedCost()
9191
{
9292
return _accumulated_cost;
9393
}
@@ -105,7 +105,7 @@ class Node2D
105105
* @brief Gets the costmap cost at this node
106106
* @return costmap cost
107107
*/
108-
inline float & getCost()
108+
inline float getCost()
109109
{
110110
return _cell_cost;
111111
}
@@ -123,7 +123,7 @@ class Node2D
123123
* @brief Gets if cell has been visited in search
124124
* @param If cell was visited
125125
*/
126-
inline bool & wasVisited()
126+
inline bool wasVisited()
127127
{
128128
return _was_visited;
129129
}
@@ -158,7 +158,7 @@ class Node2D
158158
* @brief Gets cell index
159159
* @return Reference to cell index
160160
*/
161-
inline unsigned int & getIndex()
161+
inline unsigned int getIndex()
162162
{
163163
return _index;
164164
}

nav2_smac_planner/include/nav2_smac_planner/node_hybrid.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ class NodeHybrid
217217
* @brief Gets the accumulated cost at this node
218218
* @return accumulated cost
219219
*/
220-
inline float & getAccumulatedCost()
220+
inline float getAccumulatedCost()
221221
{
222222
return _accumulated_cost;
223223
}
@@ -263,7 +263,7 @@ class NodeHybrid
263263
* @brief Gets the costmap cost at this node
264264
* @return costmap cost
265265
*/
266-
inline float & getCost()
266+
inline float getCost()
267267
{
268268
return _cell_cost;
269269
}
@@ -272,7 +272,7 @@ class NodeHybrid
272272
* @brief Gets if cell has been visited in search
273273
* @param If cell was visited
274274
*/
275-
inline bool & wasVisited()
275+
inline bool wasVisited()
276276
{
277277
return _was_visited;
278278
}
@@ -289,7 +289,7 @@ class NodeHybrid
289289
* @brief Gets cell index
290290
* @return Reference to cell index
291291
*/
292-
inline unsigned int & getIndex()
292+
inline unsigned int getIndex()
293293
{
294294
return _index;
295295
}

nav2_smac_planner/include/nav2_smac_planner/node_lattice.hpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,12 +105,14 @@ struct LatticeMotionTable
105105
float reverse_penalty;
106106
float travel_distance_reward;
107107
float rotation_penalty;
108+
float min_turning_radius;
108109
bool allow_reverse_expansion;
109110
std::vector<std::vector<MotionPrimitive>> motion_primitives;
110111
ompl::base::StateSpacePtr state_space;
111112
std::vector<TrigValues> trig_values;
112113
std::string current_lattice_filepath;
113114
LatticeMetadata lattice_metadata;
115+
MotionModel motion_model = MotionModel::UNKNOWN;
114116
};
115117

116118
/**
@@ -183,7 +185,7 @@ class NodeLattice
183185
* @brief Gets the accumulated cost at this node
184186
* @return accumulated cost
185187
*/
186-
inline float & getAccumulatedCost()
188+
inline float getAccumulatedCost()
187189
{
188190
return _accumulated_cost;
189191
}
@@ -201,7 +203,7 @@ class NodeLattice
201203
* @brief Gets the costmap cost at this node
202204
* @return costmap cost
203205
*/
204-
inline float & getCost()
206+
inline float getCost()
205207
{
206208
return _cell_cost;
207209
}
@@ -210,7 +212,7 @@ class NodeLattice
210212
* @brief Gets if cell has been visited in search
211213
* @param If cell was visited
212214
*/
213-
inline bool & wasVisited()
215+
inline bool wasVisited()
214216
{
215217
return _was_visited;
216218
}
@@ -227,7 +229,7 @@ class NodeLattice
227229
* @brief Gets cell index
228230
* @return Reference to cell index
229231
*/
230-
inline unsigned int & getIndex()
232+
inline unsigned int getIndex()
231233
{
232234
return _index;
233235
}

nav2_smac_planner/include/nav2_smac_planner/types.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ struct SearchInfo
4343
float rotation_penalty{5.0};
4444
float analytic_expansion_ratio{3.5};
4545
float analytic_expansion_max_length{60.0};
46+
float analytic_expansion_max_cost{200.0};
47+
bool analytic_expansion_max_cost_override{false};
4648
std::string lattice_filepath;
4749
bool cache_obstacle_heuristic{false};
4850
bool allow_reverse_expansion{false};

nav2_smac_planner/src/a_star.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ void AStarAlgorithm<NodeT>::setCollisionChecker(GridCollisionChecker * collision
110110
_y_size = y_size;
111111
NodeT::initMotionModel(_motion_model, _x_size, _y_size, _dim3_size, _search_info);
112112
}
113-
_expander->setCollisionChecker(collision_checker);
113+
_expander->setCollisionChecker(_collision_checker);
114114
}
115115

116116
template<typename NodeT>

nav2_smac_planner/src/analytic_expansion.cpp

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ AnalyticExpansion<NodeT>::AnalyticExpansion(
4141

4242
template<typename NodeT>
4343
void AnalyticExpansion<NodeT>::setCollisionChecker(
44-
GridCollisionChecker * collision_checker)
44+
GridCollisionChecker * & collision_checker)
4545
{
4646
_collision_checker = collision_checker;
4747
}
@@ -80,7 +80,8 @@ typename AnalyticExpansion<NodeT>::NodePtr AnalyticExpansion<NodeT>::tryAnalytic
8080
if (analytic_iterations <= 0) {
8181
// Reset the counter and try the analytic path expansion
8282
analytic_iterations = desired_iterations;
83-
AnalyticExpansionNodes analytic_nodes = getAnalyticPath(current_node, goal_node, getter);
83+
AnalyticExpansionNodes analytic_nodes =
84+
getAnalyticPath(current_node, goal_node, getter, current_node->motion_table.state_space);
8485
if (!analytic_nodes.empty()) {
8586
// If we have a valid path, attempt to refine it
8687
NodePtr node = current_node;
@@ -94,7 +95,8 @@ typename AnalyticExpansion<NodeT>::NodePtr AnalyticExpansion<NodeT>::tryAnalytic
9495
test_node->parent->parent->parent->parent->parent)
9596
{
9697
test_node = test_node->parent->parent->parent->parent->parent;
97-
refined_analytic_nodes = getAnalyticPath(test_node, goal_node, getter);
98+
refined_analytic_nodes =
99+
getAnalyticPath(test_node, goal_node, getter, test_node->motion_table.state_space);
98100
if (refined_analytic_nodes.empty()) {
99101
break;
100102
}
@@ -105,6 +107,50 @@ typename AnalyticExpansion<NodeT>::NodePtr AnalyticExpansion<NodeT>::tryAnalytic
105107
}
106108
}
107109

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+
while (min_turn_rad < max_min_turn_rad) {
139+
min_turn_rad += 0.5; // In Grid Coords, 1/2 cell steps
140+
ompl::base::StateSpacePtr state_space;
141+
if (node->motion_table.motion_model == MotionModel::DUBIN) {
142+
state_space = std::make_shared<ompl::base::DubinsStateSpace>(min_turn_rad);
143+
} else {
144+
state_space = std::make_shared<ompl::base::ReedsSheppStateSpace>(min_turn_rad);
145+
}
146+
refined_analytic_nodes = getAnalyticPath(node, goal_node, getter, state_space);
147+
score = scoringFn(refined_analytic_nodes);
148+
if (score <= best_score) {
149+
analytic_nodes = refined_analytic_nodes;
150+
best_score = score;
151+
}
152+
}
153+
108154
return setAnalyticPath(node, goal_node, analytic_nodes);
109155
}
110156
}
@@ -120,18 +166,18 @@ template<typename NodeT>
120166
typename AnalyticExpansion<NodeT>::AnalyticExpansionNodes AnalyticExpansion<NodeT>::getAnalyticPath(
121167
const NodePtr & node,
122168
const NodePtr & goal,
123-
const NodeGetter & node_getter)
169+
const NodeGetter & node_getter,
170+
const ompl::base::StateSpacePtr & state_space)
124171
{
125-
static ompl::base::ScopedState<> from(node->motion_table.state_space), to(
126-
node->motion_table.state_space), s(node->motion_table.state_space);
172+
static ompl::base::ScopedState<> from(state_space), to(state_space), s(state_space);
127173
from[0] = node->pose.x;
128174
from[1] = node->pose.y;
129175
from[2] = node->motion_table.getAngleFromBin(node->pose.theta);
130176
to[0] = goal->pose.x;
131177
to[1] = goal->pose.y;
132178
to[2] = node->motion_table.getAngleFromBin(goal->pose.theta);
133179

134-
float d = node->motion_table.state_space->distance(from(), to());
180+
float d = state_space->distance(from(), to());
135181

136182
// If the length is too far, exit. This prevents unsafe shortcutting of paths
137183
// into higher cost areas far out from the goal itself, let search to the work of getting
@@ -142,8 +188,8 @@ typename AnalyticExpansion<NodeT>::AnalyticExpansionNodes AnalyticExpansion<Node
142188
}
143189

144190
// A move of sqrt(2) is guaranteed to be in a new cell
145-
static const float sqrt_2 = std::sqrt(2.);
146-
unsigned int num_intervals = std::floor(d / sqrt_2);
191+
static const float sqrt_2 = std::sqrt(2.0f);
192+
unsigned int num_intervals = static_cast<unsigned int>(std::floor(d / sqrt_2));
147193

148194
AnalyticExpansionNodes possible_nodes;
149195
// When "from" and "to" are zero or one cell away,
@@ -159,10 +205,12 @@ typename AnalyticExpansion<NodeT>::AnalyticExpansionNodes AnalyticExpansion<Node
159205
float angle = 0.0;
160206
Coordinates proposed_coordinates;
161207
bool failure = false;
208+
std::vector<float> node_costs;
209+
node_costs.reserve(num_intervals);
162210

163211
// Check intermediary poses (non-goal, non-start)
164212
for (float i = 1; i < num_intervals; i++) {
165-
node->motion_table.state_space->interpolate(from(), to(), i / num_intervals, s());
213+
state_space->interpolate(from(), to(), i / num_intervals, s());
166214
reals = s.reals();
167215
// Make sure in range [0, 2PI)
168216
theta = (reals[2] < 0.0) ? (reals[2] + 2.0 * M_PI) : reals[2];
@@ -182,6 +230,7 @@ typename AnalyticExpansion<NodeT>::AnalyticExpansionNodes AnalyticExpansion<Node
182230
if (next->isNodeValid(_traverse_unknown, _collision_checker) && next != prev) {
183231
// Save the node, and its previous coordinates in case we need to abort
184232
possible_nodes.emplace_back(next, initial_node_coords, proposed_coordinates);
233+
node_costs.emplace_back(next->getCost());
185234
prev = next;
186235
} else {
187236
// Abort
@@ -196,6 +245,38 @@ typename AnalyticExpansion<NodeT>::AnalyticExpansionNodes AnalyticExpansion<Node
196245
}
197246
}
198247

248+
if (!failure) {
249+
// We found 'a' valid expansion. Now to tell if its a quality option...
250+
const float max_cost = _search_info.analytic_expansion_max_cost;
251+
if (*std::max_element(node_costs.begin(), node_costs.end()) > max_cost) {
252+
// If any element is above the comfortable cost limit, check edge cases:
253+
// (1) Check if goal is in greater than max_cost space requiring
254+
// entering it, but only entering it on final approach, not in-and-out
255+
// (2) Checks if goal is in normal space, but enters costed space unnecessarily
256+
// mid-way through, skirting obstacle or in non-globally confined space
257+
bool cost_exit_high_cost_region = false;
258+
for (auto iter = node_costs.rbegin(); iter != node_costs.rend(); ++iter) {
259+
const float & curr_cost = *iter;
260+
if (curr_cost <= max_cost) {
261+
cost_exit_high_cost_region = true;
262+
} else if (curr_cost > max_cost && cost_exit_high_cost_region) {
263+
failure = true;
264+
break;
265+
}
266+
}
267+
268+
// (3) Handle exception: there may be no other option close to goal
269+
// if max cost is set too low (optional)
270+
if (failure) {
271+
if (d < 2.0f * M_PI * goal->motion_table.min_turning_radius &&
272+
_search_info.analytic_expansion_max_cost_override)
273+
{
274+
failure = false;
275+
}
276+
}
277+
}
278+
}
279+
199280
// Reset to initial poses to not impact future searches
200281
for (const auto & node_pose : possible_nodes) {
201282
const auto & n = node_pose.node;
@@ -256,7 +337,8 @@ typename AnalyticExpansion<Node2D>::AnalyticExpansionNodes AnalyticExpansion<Nod
256337
getAnalyticPath(
257338
const NodePtr & node,
258339
const NodePtr & goal,
259-
const NodeGetter & node_getter)
340+
const NodeGetter & node_getter,
341+
const ompl::base::StateSpacePtr & state_space)
260342
{
261343
return AnalyticExpansionNodes();
262344
}

0 commit comments

Comments
 (0)