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
319 ARMARX_DEBUG << "Setting start node for position " << start.matrix();
320 Node& nodeStart = closestNode(start);
321 ARMARX_INFO << "Start node orientation: " << nodeStart.orientation_deg;
322 ARMARX_INFO << "Start node rotation index: "
323 << costmap3d.closestRotationFromDegrees(nodeStart.orientation_deg).index;
324 ARMARX_CHECK(Grid::fulfillsConstraints(nodeStart, costmap3d, params.clearance))
325 << "Start node in collision (within clearance=" << params.clearance << "mm)!";
326
327 ARMARX_DEBUG << "Setting goal node for position " << goal.matrix();
328 Node& nodeGoal = closestNode(goal);
329 ARMARX_INFO << "Goal node orientation: " << nodeGoal.orientation_deg;
330 ARMARX_INFO << "Goal node rotation index: "
331 << costmap3d.closestRotationFromDegrees(nodeGoal.orientation_deg).index;
332 ARMARX_CHECK(Grid::fulfillsConstraints(nodeGoal, costmap3d, params.clearance))
333 << "Goal node in collision (within clearance=" << params.clearance << "mm)!";
334
335 // gScore - (estimated) cost from start node to a given node
336 nodeStart.gScore = 0;
337
338 // fScore - (estimated) cost from the start over the given node to the end (gScore + heuristic)
339 nodeStart.fScore = nodeStart.gScore + heuristic(nodeStart, nodeGoal);
340
341 // open set
342 auto cmp = [&](const Node* left, const Node* right)
343 { return left->fScore > right->fScore; };
344 std::priority_queue<Node*, std::vector<Node*>, decltype(cmp)> openSetPq(cmp);
345 openSetPq.push(&nodeStart);
346 nodeStart.inOpenSet = true;
347
348 bool foundSolution = false;
349 while (!openSetPq.empty())
350 {
351 Node& currentBest = *openSetPq.top();
352 currentBest.inOpenSet = false;
353 openSetPq.pop();
354 if (&currentBest == &nodeGoal)
355 {
356 foundSolution = true;
357 break;
358 }
359
360 currentBest.inClosedSet = true;
361 for (size_t i = 0; i < currentBest.successors.size(); i++)
362 {
363 Node& neighbor = *currentBest.successors[i];
364 if (neighbor.inClosedSet)
365 {
366 continue;
367 }
368
369 Costs cost;
370 try
371 {
372 cost = costs(currentBest, neighbor);
373 }
374 catch (const std::exception& e)
375 {
376 ARMARX_ERROR << "Cost calculation failed for neighbor at position "
377 << neighbor.position.transpose() << " with orientation "
378 << neighbor.orientation_deg << " deg";
379 ARMARX_ERROR << "Current node position: " << currentBest.position.transpose()
380 << " with orientation " << currentBest.orientation_deg << " deg";
381 throw;
382 }
383 const auto pred1 = currentBest.predecessor
384 ? std::make_optional(*currentBest.predecessor)
385 : std::nullopt;
386 const auto pred2 = pred1.has_value()
387 ? pred1->predecessor
388 ? std::make_optional(*(pred1->predecessor))
389 : std::nullopt
390 : std::nullopt;
391 cost.smoothnessCosts = params.smoothnessWeightFactor *
392 smoothnessCosts(neighbor, currentBest, pred1, pred2);
393 float tentativeGScore = currentBest.gScore + cost.combined();
394 if ((not neighbor.inOpenSet) || tentativeGScore < neighbor.gScore)
395 {
396 neighbor.predecessor = std::experimental::make_observer(&currentBest);
397 neighbor.predecessorCosts = cost;
398 neighbor.gScore = tentativeGScore;
399 neighbor.fScore = tentativeGScore + heuristic(neighbor, nodeGoal);
400 if (not neighbor.inOpenSet)
401 {
402 openSetPq.push(&neighbor);
403 neighbor.inOpenSet = true;
404 }
405 }
406 }
407 }
408
409 // Found solution, now retrieve path from goal to start
410 if (foundSolution)
411 {
412 const auto resultNodes = nodeGoal.traversePredecessors();
413 result = resultNodes |
414 ranges::views::transform(
415 [this](const Node* node) noexcept {
416 return costmap3d.globalPose(costmap3d.toVertex(node->position).index,
417 node->orientation_deg);
418 }) |
419 ranges::to_vector;
420 auto predecessorCosts = resultNodes |
421 ranges::views::transform([](const Node* node) noexcept
422 { return node->predecessorCosts; }) |
423 ranges::to_vector;
424 ranges::reverse(predecessorCosts);
425 ARMARX_DEBUG << "Predecessor costs";
426 Costs combinedCosts = {}; // default-initialize to 0
427 for (const auto& c : predecessorCosts)
428 {
429 ARMARX_DEBUG << "------------------";
430 if (not c.has_value())
431 {
432 continue;
433 }
434 ARMARX_DEBUG << "euclidian: " << c->euclidian;
435 ARMARX_DEBUG << "obs_dist: " << c->obstacleProximity;
436 ARMARX_DEBUG << "obs_dist_2: " << c->obstacleProximity2;
437 ARMARX_DEBUG << "ori_diff: " << c->orientationDifference;
438 ARMARX_DEBUG << "forward: " << c->forwardMovement;
439 ARMARX_DEBUG << "smoothness: " << c->smoothnessCosts;
440 combinedCosts = combinedCosts + c.value();
441 }
442
443 ARMARX_INFO << "==================";
444 ARMARX_INFO << "Total costs:";
445 ARMARX_INFO << "euclidian: " << combinedCosts.euclidian;
446 ARMARX_INFO << "obs_dist: " << combinedCosts.obstacleProximity;
447 ARMARX_INFO << "obs_dist_2: " << combinedCosts.obstacleProximity2;
448 ARMARX_INFO << "ori_diff: " << combinedCosts.orientationDifference;
449 ARMARX_INFO << "forward: " << combinedCosts.forwardMovement;
450 ARMARX_INFO << "smoothness: " << combinedCosts.smoothnessCosts;
451 ARMARX_INFO << "==================";
452 }
453
454 // Since the graph was traversed from goal to start, we have to reverse the order
455 ranges::reverse(result);
456
457 return result;
458 }
459
460 Costs
461 AStarPlanner::costs(const Node& n1, const Node& n2) const
462 {
463 const auto vectorDiff = n1.position - n2.position;
464 const float euclideanDistance = vectorDiff.norm();
465
466 // additional costs if
467 const float obstacleDistanceN1 = std::min(n1.obstacleDistance, params.maxObstacleDistance);
468 const float obstacleDistanceN2 = std::min(n2.obstacleDistance, params.maxObstacleDistance);
469
470 // const float obstacleProximityChangeCosts = std::max(obstacleDistanceN1 - obstacleDistanceN2, 0.F);
471 const float obstacleProximityChangeCosts = obstacleDistanceN1 - obstacleDistanceN2;
472
473 // Small note for heuristic:
474 // obstacleProximityChangeCosts can be negative
475
476 // Additional obstacle distance cost (penalty for being near an obstacle for longer than necessary)
477 // Shift by clearance: the "boundary" is now at clearance mm, not at 0 mm.
478 float effectiveDistN2 = std::max(obstacleDistanceN2 - params.clearance, 0.0f);
479 float ratio = (params.maxObstacleDistance - effectiveDistN2) / params.maxObstacleDistance;
480 ratio = std::clamp(ratio, 0.0f, 1.0f);
481 const float obstacleProximityN2CostsNormalized =
482 std::pow(ratio, params.obstacleDistanceContinuousExponent);
483
484 // orientation costs
485 const float orientationDist = std::abs(n1.orientation_deg - n2.orientation_deg);
486
487 // prefer to move forward
488 auto pose_n1 = costmap3d.globalPose({0, 0}, n1.orientation_deg);
489 auto pose_n2 = costmap3d.globalPose({0, 0}, n2.orientation_deg);
490 auto ori_vec_n1 = pose_n1.linear() * Eigen::Vector2f::UnitY();
491 auto ori_vec_n2 = pose_n2.linear() * Eigen::Vector2f::UnitY();
492 Eigen::Vector2f ori_vec = ori_vec_n1 + ori_vec_n2;
493 ori_vec.normalize();
494 const Eigen::Vector2f vectorDiff_norm = vectorDiff.normalized();
495 float forward_angle = 0;
496 if (vectorDiff.norm() == 0.F)
497 {
498 ARMARX_DEBUG << "Zero-length vector between nodes, cannot calculate forward movement "
499 "cost. n1 position: "
500 << n1.position.transpose() << ", n2 position: " << n2.position.transpose();
501 ARMARX_DEBUG << "Using 0 instead";
502 }
503 else
504 {
505 forward_angle = std::abs(angleBetween(ori_vec, -vectorDiff_norm));
506 }
507
508 // Dynamic forward weight: prefer forward movement more strongly near obstacles.
509 // When close to a wall, sideways/backward motion is more likely to collide.
510 float obstacleProximityRatio = obstacleDistanceN2 / params.maxObstacleDistance;
511 float dynamicForwardWeight = params.forwardWeightFactor
512 * (1.0F + params.forwardWeightObstacleScale
513 * (1.0F - obstacleProximityRatio));
514
515 Costs costs = {
516 .euclidian = euclideanDistance,
517 .obstacleProximity = obstacleProximityChangeCosts * params.obstacleDistanceWeightFactor,
518 .obstacleProximity2 =
519 obstacleProximityN2CostsNormalized * params.obstacleDistanceContinuousWeightFactor,
520 .orientationDifference = orientationDist * params.orientationWeightFactor,
521 .forwardMovement = forward_angle * dynamicForwardWeight,
522 .smoothnessCosts = 0 // TODO set
523 };
524 return costs;
525 }
526
527
528} // 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:181
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:196
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:187
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:77