AStarPlanner.cpp
Go to the documentation of this file.
1#include "AStarPlanner.h"
2
3#include <math.h>
4
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <experimental/memory>
9#include <map>
10#include <memory>
11#include <optional>
12#include <queue>
13#include <vector>
14
15#include <Eigen/Geometry>
16
17#include <qnamespace.h>
18
19#include <range/v3/algorithm/reverse.hpp>
20#include <range/v3/range/conversion.hpp>
21#include <range/v3/view/map.hpp>
22#include <range/v3/view/transform.hpp>
23
28
34
35#include <omp.h>
36
38{
39
40 Grid::Grid(int rows, int cols, int orientations, const Costmap3D& costmap, float clearance)
41 {
42 this->clearance = clearance;
43 initialize(rows, cols, orientations, costmap);
44 }
45
46 void
48 {
49 for (int r = 0; r < rows; r++)
50 {
51 for (int c = 0; c < cols; c++)
52 {
53 for (int rot_idx = 0; rot_idx < orientations; rot_idx++)
54 {
55 Node& node = getNode(r, c, rot_idx);
56 node.inOpenSet = false;
57 node.inClosedSet = false;
58 node.fScore = -1.F;
59 node.gScore = -1.F;
60 }
61 }
62 }
63 }
64
65 Node&
66 Grid::getNode(int row, int col, int orientation)
67 {
68 return data_[((row * cols) + col) * orientations + orientation];
69 }
70
71 void
72 Grid::initialize(int rows, int cols, int orientations, const Costmap3D& costmap)
73 {
75 this->rows = rows;
76 this->cols = cols;
77 this->orientations = orientations;
78
79 // Measure time
81
82 // create the grid with the correct size
83 ARMARX_VERBOSE << "Creating grid";
84
85 data_ = std::make_unique<Node[]>(rows * cols * orientations);
86
87 // intitializing grid
88 for (int r = 0; r < rows; r++)
89 {
90 for (int c = 0; c < cols; c++)
91 {
92 for (int rot_idx = 0; rot_idx < orientations; rot_idx++)
93 {
94 const auto pos = costmap.toPositionGlobal({r, c});
95 const float obstacleDistance = costmap.value_ignore_mask(
97 const auto rotation_deg =
98 costmap.rotationFromIndex(Costmap3D::RotationIndex{rot_idx}).degrees;
99
100 getNode(r, c, rot_idx).initialize(pos, obstacleDistance, rotation_deg);
101 }
102 }
103 }
104
105
106 ARMARX_VERBOSE << "Creating graph";
107
108 ARMARX_CHECK(costmap.params().orientations == orientations);
109
110 // Init successors
111 for (int r = 0; r < rows; r++)
112 {
113 for (int c = 0; c < cols; c++)
114 {
115 for (int rot_idx = 0; rot_idx < orientations; rot_idx++)
116 {
117 Node& candidate = getNode(r, c, rot_idx);
118 if (!fulfillsConstraints(candidate, costmap, this->clearance))
119 {
120 continue;
121 }
122
123 // Add the valid node as successor to all its neighbors
124 for (int nR = -1; nR <= 1; nR++)
125 {
126 for (int nC = -1; nC <= 1; nC++)
127 {
128 const int neighborIndexC = c + nC;
129 const int neighborIndexR = r + nR;
130 if (neighborIndexC < 0 || neighborIndexR < 0 || (nR == 0 && nC == 0))
131 {
132 continue;
133 }
134
135 if (neighborIndexR >= static_cast<int>(rows) ||
136 neighborIndexC >= static_cast<int>(cols))
137 {
138 continue;
139 }
140
141 //grid[neighborIndexR][neighborIndexC][rot_idx]->successors.push_back(candidate);
142
143 for (int nRot = -3; nRot <= 3; nRot++)
144 {
145 int neighborRotIndex = rot_idx + nRot;
146 if (neighborRotIndex < 0)
147 {
148 neighborRotIndex += orientations;
149 }
150 if (neighborRotIndex >= orientations)
151 {
152 neighborRotIndex -= orientations;
153 }
154 getNode(neighborIndexR, neighborIndexC, neighborRotIndex)
155 .successors.emplace_back(&candidate);
156 }
157 }
158 }
159
160 // Add successors by just rotating in place
161 bool enableRotationInPlace =
162 false; // currently has problems with zero-lenght vectors for distance to previous node (would need to be handled separately)
163 for (int nRot = -1; enableRotationInPlace && nRot <= 1; nRot++)
164 {
165 if (nRot == 0)
166 {
167 continue;
168 }
169 int neighborRotIndex = rot_idx + nRot;
170 if (neighborRotIndex < 0)
171 {
172 neighborRotIndex += orientations;
173 }
174 if (neighborRotIndex >= orientations)
175 {
176 neighborRotIndex -= orientations;
177 }
178 getNode(r, c, neighborRotIndex).successors.emplace_back(&candidate);
179 }
180 }
181 }
182 }
183
184 armarx::core::time::DateTime t2 = armarx::core::time::DateTime::Now();
185 ARMARX_VERBOSE << "Grid created in " << (t2 - t1).toMilliSeconds() << " ms";
186 }
187
188 bool
189 Grid::fulfillsConstraints(const Node& n, const Costmap3D& costmap, float clearance)
190 {
192
193 // Treat nodes within the clearance zone as collision.
194 return n.obstacleDistance > clearance;
195 }
196
198 costmap3d(costmap3d), params(params)
199 {
200 }
201
202 float
203 AStarPlanner::heuristic(const Node& n1, const Node& n2)
204 {
205 // TODO: Update heuristic and cost functions
206 // Cost function for orientation change should not be 0, otherwise the robot would rotate more than what we want
207 //
208 return std::max(1.F - params.obstacleDistanceWeightFactor, 0.F) *
209 (n1.position - n2.position).norm();
210
211 // maybe we also need to include the orientation in the heuristic?
212 }
213
214 Node&
215 AStarPlanner::closestNode(const core::Pose2D& pose)
216 {
217 float rot_deg = Eigen::Rotation2Df{pose.rotation()}.angle() * 180.F / M_PI;
218 if (rot_deg <= 0.F)
219 {
220 rot_deg += 360.F;
221 }
222 ARMARX_DEBUG << VAROUT(rot_deg);
223
224 const auto vertex = costmap3d.toVertex(pose.translation());
225 const auto rot = costmap3d.closestRotationFromDegrees(rot_deg);
226 ARMARX_DEBUG << "Closest rotation: " << rot.index << " (" << rot.degrees << " deg)";
227
228 const int r = vertex.index.x();
229 const int c = vertex.index.y();
230
233 ARMARX_CHECK_GREATER_EQUAL(rot.index, 0);
234
235 const int rows = static_cast<int>(costmap3d.getSize().x());
236 const int cols = static_cast<int>(costmap3d.getSize().y());
237
238 ARMARX_CHECK_LESS_EQUAL(r, rows - 1);
239 ARMARX_CHECK_LESS_EQUAL(c, cols - 1);
240 ARMARX_CHECK_LESS_EQUAL(rot.index, costmap3d.params().orientations - 1);
241
242 return grid->getNode(static_cast<int>(r), static_cast<int>(c), rot.index);
243 }
244
245 namespace
246 {
247
248 float
249 angleBetween(const Eigen::Vector2f& a, const Eigen::Vector2f& b)
250 {
251 float dotProd = a.dot(b);
252 float magA = a.norm();
253 float magB = b.norm();
254
255 if (magA == 0.0f || magB == 0.0f)
256 {
257 // This is expected if in-place rotations are allowed. However, in other cases this would be unexpected behavior.
258 return 0.F;
259 }
260
261 float cosTheta = dotProd / (magA * magB);
262
263 // Clamp to [-1, 1] to avoid NaNs due to floating-point errors
264 if (cosTheta > 1.0f)
265 cosTheta = 1.0f;
266 if (cosTheta < -1.0f)
267 cosTheta = -1.0f;
268
269 return std::acos(cosTheta); // returns radians
270 }
271
272 float
273 smoothnessCosts(const Node& n1,
274 const Node& n2,
275 const std::optional<Node>& n3,
276 const std::optional<Node>& n4)
277 {
278 float cost = 0;
279
280 auto angle_to_cost = [&](float angle) { return angle * angle; };
281
282 if (n3.has_value())
283 {
284 const Eigen::Vector2f v21 = n1.position - n2.position;
285 const Eigen::Vector2f v23 = n3.value().position - n2.position;
286 float angle = std::abs(angleBetween(v21, -v23));
287 ARMARX_CHECK(angle <= M_PI + 0.001);
288 cost += angle_to_cost(angle);
289 }
290 if (false && n3.has_value() && n4.has_value())
291 {
292 const Eigen::Vector2f v32 = n2.position - n3.value().position;
293 const Eigen::Vector2f v34 = n4.value().position - n3.value().position;
294 float angle = angleBetween(v32, -v34);
295 ARMARX_CHECK(angle <= M_PI + 0.001);
296 cost += angle_to_cost(angle);
297 }
298 return cost;
299 }
300 } // namespace
301
302 std::vector<core::Pose2D>
304 {
306
307 if (grid.has_value())
308 {
309 grid->clear();
310 }
311 else
312 {
313 // initialize grid
314 const auto size = costmap3d.getSize();
315 grid = {size.x(), size.y(), costmap3d.params().orientations, costmap3d, params.clearance};
316 }
317 std::vector<core::Pose2D> result;
318 expandedNodes = 0;
319
320 ARMARX_DEBUG << "Setting start node for position " << start.matrix();
321 Node& nodeStart = closestNode(start);
322 ARMARX_INFO << "Start node orientation: " << nodeStart.orientation_deg;
323 ARMARX_INFO << "Start node rotation index: "
324 << costmap3d.closestRotationFromDegrees(nodeStart.orientation_deg).index;
325 ARMARX_CHECK(Grid::fulfillsConstraints(nodeStart, costmap3d, params.clearance))
326 << "Start node in collision (within clearance=" << params.clearance << "mm)!";
327
328 ARMARX_DEBUG << "Setting goal node for position " << goal.matrix();
329 Node& nodeGoal = closestNode(goal);
330 ARMARX_INFO << "Goal node orientation: " << nodeGoal.orientation_deg;
331 ARMARX_INFO << "Goal node rotation index: "
332 << costmap3d.closestRotationFromDegrees(nodeGoal.orientation_deg).index;
333 ARMARX_CHECK(Grid::fulfillsConstraints(nodeGoal, costmap3d, params.clearance))
334 << "Goal node in collision (within clearance=" << params.clearance << "mm)!";
335
336 // gScore - (estimated) cost from start node to a given node
337 nodeStart.gScore = 0;
338
339 // fScore - (estimated) cost from the start over the given node to the end (gScore + heuristic)
340 nodeStart.fScore = nodeStart.gScore + heuristic(nodeStart, nodeGoal);
341
342 // open set
343 auto cmp = [&](const Node* left, const Node* right)
344 { return left->fScore > right->fScore; };
345 std::priority_queue<Node*, std::vector<Node*>, decltype(cmp)> openSetPq(cmp);
346 openSetPq.push(&nodeStart);
347 nodeStart.inOpenSet = true;
348
349 bool foundSolution = false;
350 while (!openSetPq.empty())
351 {
352 Node& currentBest = *openSetPq.top();
353 currentBest.inOpenSet = false;
354 openSetPq.pop();
355 if (&currentBest == &nodeGoal)
356 {
357 foundSolution = true;
358 break;
359 }
360
361 currentBest.inClosedSet = true;
362 ++expandedNodes;
363 for (size_t i = 0; i < currentBest.successors.size(); i++)
364 {
365 Node& neighbor = *currentBest.successors[i];
366 if (neighbor.inClosedSet)
367 {
368 continue;
369 }
370
371 Costs cost;
372 try
373 {
374 cost = costs(currentBest, neighbor);
375 }
376 catch (const std::exception& e)
377 {
378 ARMARX_ERROR << "Cost calculation failed for neighbor at position "
379 << neighbor.position.transpose() << " with orientation "
380 << neighbor.orientation_deg << " deg";
381 ARMARX_ERROR << "Current node position: " << currentBest.position.transpose()
382 << " with orientation " << currentBest.orientation_deg << " deg";
383 throw;
384 }
385 const auto pred1 = currentBest.predecessor
386 ? std::make_optional(*currentBest.predecessor)
387 : std::nullopt;
388 const auto pred2 = pred1.has_value()
389 ? pred1->predecessor
390 ? std::make_optional(*(pred1->predecessor))
391 : std::nullopt
392 : std::nullopt;
393 cost.smoothnessCosts = params.smoothnessWeightFactor *
394 smoothnessCosts(neighbor, currentBest, pred1, pred2);
395 float tentativeGScore = currentBest.gScore + cost.combined();
396 if ((not neighbor.inOpenSet) || tentativeGScore < neighbor.gScore)
397 {
398 neighbor.predecessor = std::experimental::make_observer(&currentBest);
399 neighbor.predecessorCosts = cost;
400 neighbor.gScore = tentativeGScore;
401 neighbor.fScore = tentativeGScore + heuristic(neighbor, nodeGoal);
402 if (not neighbor.inOpenSet)
403 {
404 openSetPq.push(&neighbor);
405 neighbor.inOpenSet = true;
406 }
407 }
408 }
409 }
410
411 // Found solution, now retrieve path from goal to start
412 if (foundSolution)
413 {
414 const auto resultNodes = nodeGoal.traversePredecessors();
415 result = resultNodes |
416 ranges::views::transform(
417 [this](const Node* node) noexcept {
418 return costmap3d.globalPose(costmap3d.toVertex(node->position).index,
419 node->orientation_deg);
420 }) |
421 ranges::to_vector;
422 auto predecessorCosts = resultNodes |
423 ranges::views::transform([](const Node* node) noexcept
424 { return node->predecessorCosts; }) |
425 ranges::to_vector;
426 ranges::reverse(predecessorCosts);
427 ARMARX_DEBUG << "Predecessor costs";
428 Costs combinedCosts = {}; // default-initialize to 0
429 for (const auto& c : predecessorCosts)
430 {
431 ARMARX_DEBUG << "------------------";
432 if (not c.has_value())
433 {
434 continue;
435 }
436 ARMARX_DEBUG << "euclidian: " << c->euclidian;
437 ARMARX_DEBUG << "obs_dist: " << c->obstacleProximity;
438 ARMARX_DEBUG << "obs_dist_2: " << c->obstacleProximity2;
439 ARMARX_DEBUG << "ori_diff: " << c->orientationDifference;
440 ARMARX_DEBUG << "forward: " << c->forwardMovement;
441 ARMARX_DEBUG << "smoothness: " << c->smoothnessCosts;
442 combinedCosts = combinedCosts + c.value();
443 }
444
445 ARMARX_INFO << "==================";
446 ARMARX_INFO << "Total costs:";
447 ARMARX_INFO << "euclidian: " << combinedCosts.euclidian;
448 ARMARX_INFO << "obs_dist: " << combinedCosts.obstacleProximity;
449 ARMARX_INFO << "obs_dist_2: " << combinedCosts.obstacleProximity2;
450 ARMARX_INFO << "ori_diff: " << combinedCosts.orientationDifference;
451 ARMARX_INFO << "forward: " << combinedCosts.forwardMovement;
452 ARMARX_INFO << "smoothness: " << combinedCosts.smoothnessCosts;
453 ARMARX_INFO << "==================";
454 }
455
456 // Since the graph was traversed from goal to start, we have to reverse the order
457 ranges::reverse(result);
458
459 return result;
460 }
461
462 Costs
463 AStarPlanner::costs(const Node& n1, const Node& n2) const
464 {
465 const auto vectorDiff = n1.position - n2.position;
466 const float euclideanDistance = vectorDiff.norm();
467
468 // additional costs if
469 const float obstacleDistanceN1 = std::min(n1.obstacleDistance, params.maxObstacleDistance);
470 const float obstacleDistanceN2 = std::min(n2.obstacleDistance, params.maxObstacleDistance);
471
472 // const float obstacleProximityChangeCosts = std::max(obstacleDistanceN1 - obstacleDistanceN2, 0.F);
473 const float obstacleProximityChangeCosts = obstacleDistanceN1 - obstacleDistanceN2;
474
475 // Small note for heuristic:
476 // obstacleProximityChangeCosts can be negative
477
478 // Additional obstacle distance cost (penalty for being near an obstacle for longer than necessary)
479 // Shift by clearance: the "boundary" is now at clearance mm, not at 0 mm.
480 float effectiveDistN2 = std::max(obstacleDistanceN2 - params.clearance, 0.0f);
481 float ratio = (params.maxObstacleDistance - effectiveDistN2) / params.maxObstacleDistance;
482 ratio = std::clamp(ratio, 0.0f, 1.0f);
483 const float obstacleProximityN2CostsNormalized =
484 std::pow(ratio, params.obstacleDistanceContinuousExponent);
485
486 // orientation costs
487 const float orientationDist = std::abs(n1.orientation_deg - n2.orientation_deg);
488
489 // prefer to move forward
490 auto pose_n1 = costmap3d.globalPose({0, 0}, n1.orientation_deg);
491 auto pose_n2 = costmap3d.globalPose({0, 0}, n2.orientation_deg);
492 auto ori_vec_n1 = pose_n1.linear() * Eigen::Vector2f::UnitY();
493 auto ori_vec_n2 = pose_n2.linear() * Eigen::Vector2f::UnitY();
494 Eigen::Vector2f ori_vec = ori_vec_n1 + ori_vec_n2;
495 ori_vec.normalize();
496 const Eigen::Vector2f vectorDiff_norm = vectorDiff.normalized();
497 float forward_angle = 0;
498 if (vectorDiff.norm() == 0.F)
499 {
500 ARMARX_DEBUG << "Zero-length vector between nodes, cannot calculate forward movement "
501 "cost. n1 position: "
502 << n1.position.transpose() << ", n2 position: " << n2.position.transpose();
503 ARMARX_DEBUG << "Using 0 instead";
504 }
505 else
506 {
507 forward_angle = std::abs(angleBetween(ori_vec, -vectorDiff_norm));
508 }
509
510 // Dynamic forward weight: prefer forward movement more strongly near obstacles.
511 // When close to a wall, sideways/backward motion is more likely to collide.
512 float obstacleProximityRatio = obstacleDistanceN2 / params.maxObstacleDistance;
513 float dynamicForwardWeight = params.forwardWeightFactor
514 * (1.0F + params.forwardWeightObstacleScale
515 * (1.0F - obstacleProximityRatio));
516
517 Costs costs = {
518 .euclidian = euclideanDistance,
519 .obstacleProximity = obstacleProximityChangeCosts * params.obstacleDistanceWeightFactor,
520 .obstacleProximity2 =
521 obstacleProximityN2CostsNormalized * params.obstacleDistanceContinuousWeightFactor,
522 .orientationDifference = orientationDist * params.orientationWeightFactor,
523 .forwardMovement = forward_angle * dynamicForwardWeight,
524 .smoothnessCosts = 0 // TODO set
525 };
526 return costs;
527 }
528
529
530} // namespace armarx::navigation::algorithms::orientation_aware
#define M_PI
Definition MathTools.h:17
#define VAROUT(x)
constexpr T c
Represents a point in time.
Definition DateTime.h:25
static DateTime Now()
Definition DateTime.cpp:51
AStarPlanner(const Costmap3D &costmap, io::AStarWithOrientationParams params)
std::vector< core::Pose2D > plan(const core::Pose2D &start, const core::Pose2D &goal)
core::Pose2D globalPose(const Index &index, const RotationDegrees &rotationDeg) const
Definition Costmap3D.h:116
Rotation rotationFromIndex(RotationIndex index) const
Rotation closestRotationFromDegrees(RotationDegrees degrees) const
Vertex toVertex(const Position &globalPosition) const
Grid(int rows, int cols, int orientations, const Costmap3D &costmap, float clearance)
static bool fulfillsConstraints(const Node &n, const Costmap3D &costmap, float clearance=0.0f)
Node & getNode(int row, int col, int orientation)
A Node can store data to all valid neighbors (successors) and a precessor.
Definition Node.h:55
std::vector< const Node * > traversePredecessors() const
Collects all predecessors in order to generate path to starting point.
Definition Node.cpp:24
void initialize(const Eigen::Vector2f &position, float obstacleDistance, float orientation_deg)
Definition Node.cpp:16
std::experimental::observer_ptr< Node > predecessor
For traversal.
Definition Node.h:72
std::vector< std::experimental::observer_ptr< Node > > successors
All nodes that are adjacent to this one.
Definition Node.h:70
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_LESS_EQUAL(lhs, rhs)
This macro evaluates whether lhs is less or equal (<=) rhs and if it turns out to be false it will th...
#define ARMARX_CHECK_GREATER_EQUAL(lhs, rhs)
This macro evaluates whether lhs is greater or equal (>=) rhs and if it turns out to be false it will...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
Eigen::Isometry2f Pose2D
Definition basic_types.h:34
This file offers overloads of toIce() and fromIce() functions for STL container types.
float euclideanDistance(IteratorType1 first1, IteratorType1 last1, IteratorType2 first2)
Returns the euclidean distance.
Definition Metrics.h:104
observer_ptr< _Tp > make_observer(_Tp *__p) noexcept
double norm(const Point &a)
Definition point.hpp:102
double angle(const Point &a, const Point &b, const Point &c)
Definition point.hpp:109
int orientations
How many orientations of the robot each cell contains.
Definition Costmap3D.h:38
#define ARMARX_TRACE
Definition trace.h:75