ShortestPathFasterAlgorithm.cpp
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @author Fabian Reister ( fabian dot reister at kit dot edu )
17 * @date 2022
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
23
24#include <algorithm>
25#include <array>
26#include <cmath>
27#include <cstddef>
28#include <cstdint>
29#include <limits>
30#include <string>
31#include <vector>
32
33#include <Eigen/Core>
34
35#include <range/v3/algorithm/reverse.hpp>
36
40
42
44{
45
46 namespace
47 {
48 constexpr std::size_t
49 ravel(std::size_t row, std::size_t col, std::size_t num_cols)
50 {
51 return (row * num_cols) + col;
52 }
53
54 Eigen::Vector2i
55 unravel(const std::size_t pos, const std::size_t numCols)
56 {
58 ARMARX_CHECK_GREATER(numCols, 0);
59
60 Eigen::Vector2i p;
61 p.x() = static_cast<int>(pos) / numCols;
62 p.y() = static_cast<int>(pos) % numCols;
63
64 return p;
65 }
66
67 } // namespace
68
70 const Parameters& params) :
71 costmap(grid), params(params)
72 {
73 ARMARX_VERBOSE << "Grid with size (" << grid.getGrid().rows() << ", "
74 << grid.getGrid().cols() << ").";
75 }
76
78 ShortestPathFasterAlgorithm::plan(const Eigen::Vector2f& start,
79 const Eigen::Vector2f& goal,
80 const bool checkStartForCollision) const
81 {
82 // ARMARX_CHECK(not costmap.isInCollision(start)) << "Start is in collision";
83 // ARMARX_CHECK(not costmap.isInCollision(goal)) << "Goal is in collision";
84
85 const Costmap::Vertex startVertex = costmap.toVertex(start);
86 const auto goalVertex = Eigen::Vector2i{costmap.toVertex(goal).index};
87
88 ARMARX_VERBOSE << "Planning from " << startVertex.index << " to " << goalVertex;
89
90 if ((startVertex.index.array() == goalVertex.array()).all())
91 {
92 ARMARX_VERBOSE << "Already at goal.";
93 return {.path = {}, .success = true};
94 }
95
96 const Result result = spfa(start, checkStartForCollision);
97
98 return constructPath(start, result.parents, goal);
99 }
100
103 const Eigen::Vector2f& start,
104 const std::vector<std::vector<Eigen::Vector2i>>& spfaParents,
105 const Eigen::Vector2f& goal) const
106 {
107 // we need the start vertex again to construct the final path
108 const Costmap::Vertex startVertex = costmap.toVertex(start);
109
110 const auto goalVertex = Eigen::Vector2i{costmap.toVertex(goal).index};
111
112 if ((startVertex.index.array() == goalVertex.array()).all())
113 {
114 ARMARX_VERBOSE << "Already at goal.";
115 return {.path = {}, .success = true};
116 }
117
118 const auto isStart = [&startVertex](const Eigen::Vector2i& v) -> bool
119 { return (v.array() == startVertex.index.array()).all(); };
120
121 const auto isInvalid = [](const Eigen::Vector2i& v) -> bool
122 { return (v.x() == -1) and (v.y() == -1); };
123
124 // traverse path from goal to start
125 std::vector<Eigen::Vector2f> path;
126 path.push_back(goal);
127
128 const Eigen::Vector2i* pt = &goalVertex;
129
130 ARMARX_VERBOSE << "Creating shortest path from grid";
131 while (true)
132 {
133 if (isInvalid(*pt))
134 {
135 ARMARX_WARNING << "No path found from (" << start << ") to (" << goal << ").";
136 return {.path = {}, .success = false};
137 }
138
139 const Eigen::Vector2i& parent = spfaParents.at(pt->x()).at(pt->y());
140
141 ARMARX_DEBUG << VAROUT(parent);
142
143 if (isStart(parent))
144 {
145 break;
146 }
147
148 path.push_back(costmap.toPositionGlobal(parent.array()));
149
150 pt = &parent;
151 }
152
153 path.push_back(start);
154 ARMARX_VERBOSE << "Path found with " << path.size() << " nodes.";
155
156 // reverse the path such that: start -> ... -> goal
157 ranges::reverse(path);
158
159 return {.path = path, .success = true};
160 }
161
163 ShortestPathFasterAlgorithm::spfa(const Eigen::Vector2f& start,
164 const bool checkStartForCollision) const
165 {
166 const Costmap::Vertex startVertex = costmap.toVertex(start);
167
168 ARMARX_VERBOSE << "Running spfa planner from " << startVertex.index;
169
170 auto costmapWithValidStart = costmap.getGrid();
171 if (costmap.isInCollision(start))
172 {
173 ARMARX_INFO << "SPFA: Start node is in collision, setting modifying start node";
174 costmapWithValidStart(startVertex.index.x(), startVertex.index.y()) = 0.1;
175 }
176
177 return spfa(
178 costmapWithValidStart, Eigen::Vector2i{startVertex.index}, checkStartForCollision);
179 }
180
182 ShortestPathFasterAlgorithm::spfa(const Eigen::MatrixXf& inputMap,
183 const Eigen::Vector2i& source,
184 const bool checkStartForCollision) const
185 {
186 if (checkStartForCollision)
187 {
188 ARMARX_CHECK_GREATER(inputMap(source.x(), source.y()), 0.F)
189 << "Start must not be in collision";
190 }
191
192 constexpr float eps = 1e-6;
193 constexpr std::size_t numDirs = 8;
194
195 const std::array<std::array<std::int64_t, 2>, numDirs> dirs{
196 std::array<std::int64_t, 2>{-1, -1},
197 std::array<std::int64_t, 2>{-1, 0},
198 std::array<std::int64_t, 2>{-1, 1},
199 std::array<std::int64_t, 2>{0, 1},
200 std::array<std::int64_t, 2>{1, 1},
201 std::array<std::int64_t, 2>{1, 0},
202 std::array<std::int64_t, 2>{1, -1},
203 std::array<std::int64_t, 2>{0, -1}};
204
205 const std::array<float, numDirs> dirLengths{
206 std::sqrt(2.0f), 1, std::sqrt(2.0f), 1, std::sqrt(2.0f), 1, std::sqrt(2.0f), 1};
207
208 // Process input map
209 // py::buffer_info map_buf = input_map.request();
210 const std::size_t numRows = inputMap.rows();
211 const std::size_t numCols = inputMap.cols();
212
213 // Get source coordinates
214 const int source_i = source.x();
215 const int source_j = source.y();
216
217 const std::size_t maxNumVerts = numRows * numCols;
218 constexpr std::size_t maxEdgesPerVert = numDirs;
219 // const float inf = 2 * maxNumVerts;
220 const float inf = 2 * maxNumVerts; // FIXME numeric_limits<float>::max();
221 const std::size_t queueSize = maxNumVerts + 1; // ring buffer: maxNumVerts live + 1 spare slot
222
223 // Initialize arrays
224 std::vector<std::size_t> edges(maxNumVerts * maxEdgesPerVert);
225 std::vector<std::size_t> edge_counts(maxNumVerts);
226 std::vector<std::size_t> queue(queueSize);
227 std::vector<bool> in_queue(maxNumVerts);
228 std::vector<float> weights(maxNumVerts * maxEdgesPerVert);
229 std::vector<float> dists(maxNumVerts, inf); // initialize with inf
230
231 const auto& verify = [](std::int64_t index, std::size_t maxIndex, const std::string& str)
232 {
233 if (index >= static_cast<std::int64_t>(maxIndex) || index < 0)
234 {
235 ARMARX_IMPORTANT << "invalid index " << index << " max allowed is " << maxIndex
236 << "; " << str;
237 }
238 };
239
240
241 // Build graph
242 ARMARX_VERBOSE << "Build graph";
243 for (std::size_t row = 0; row < numRows; ++row)
244 {
245 for (std::size_t col = 0; col < numCols; ++col)
246 {
247 const std::size_t v = ravel(row, col, numCols);
248 if (inputMap(static_cast<int>(row), static_cast<int>(col)) <= 0.F) // collision
249 {
250 continue;
251 }
252
253 for (std::size_t k = 0; k < numDirs; ++k)
254 {
255 const std::int64_t ip = static_cast<std::int64_t>(row) +
256 dirs[k][0]; // could eventually become negative
257 const std::int64_t jp = static_cast<std::int64_t>(col) + dirs[k][1];
258
259 if (ip < 0 || jp < 0 || ip >= static_cast<std::int64_t>(numRows) ||
260 jp >= static_cast<std::int64_t>(numCols))
261 {
262 continue;
263 }
264
265 const std::size_t vp = ravel(ip, jp, numCols);
266 if (inputMap(ip, jp) <= 0.F) // collision
267 {
268 continue;
269 }
270
271 const float clippedObstacleDistance =
272 std::min(inputMap(ip, jp), params.obstacleMaxDistance);
273
274 const float travelCost = dirLengths[k];
275
276 const float targetDistanceCost =
277 params.obstacleDistanceWeight *
278 std::pow(1.F - clippedObstacleDistance / params.obstacleMaxDistance,
279 params.obstacleCostExponent);
280
281 const float edgeCost = params.obstacleDistanceCosts
282 ? travelCost * (1 + targetDistanceCost)
283 : travelCost;
284
285 const std::size_t e = ravel(v, edge_counts.at(v), maxEdgesPerVert);
286 edges.at(e) = vp;
287 verify(e, maxNumVerts * maxEdgesPerVert, "edges");
288 weights.at(e) = edgeCost;
289 verify(e, maxNumVerts * maxEdgesPerVert, "weights");
290 edge_counts[v]++;
291 verify(v, maxNumVerts, "edges_counts");
292 }
293 }
294 }
295
296
297 // SPFA
298 ARMARX_DEBUG << "SPFA";
299 // The queue is a circular (ring) buffer: head and tail wrap around modulo
300 // queueSize. Only the vertices currently in the queue occupy slots, and the
301 // in_queue flag guarantees each vertex occupies at most one slot at a time, so
302 // the buffer holds at most maxNumVerts elements and never overflows. head and
303 // tail point at the last-popped / last-pushed slot respectively; the queue is
304 // empty when head == tail.
305 std::size_t head = 0;
306 std::size_t tail = 0;
307 const auto next = [queueSize](std::size_t i) { return (i + 1) % queueSize; };
308
309 const std::size_t s = ravel(source_i, source_j, numCols);
310 dists[s] = 0;
311 verify(s, maxNumVerts, "dists");
312 tail = next(tail);
313 queue[tail] = s;
314 in_queue[s] = true;
315 verify(s, maxNumVerts, "in_queue");
316
317 std::vector<std::int64_t> parents(maxNumVerts, -1);
318 while (head != tail)
319 {
320 head = next(head);
321 const std::size_t u = queue[head];
322 in_queue[u] = false;
323 verify(u, maxNumVerts, "in_queue");
324 for (std::size_t j = 0; j < edge_counts[u]; ++j)
325 {
326 const std::size_t e = ravel(u, j, maxEdgesPerVert);
327 const std::size_t v = edges[e];
328 const float newDist = dists[u] + weights[e];
329 if (newDist < dists[v])
330 {
331 parents[v] = u;
332 verify(v, maxNumVerts, "parents");
333 dists[v] = newDist;
334 verify(v, maxNumVerts, "dists");
335 if (not in_queue[v])
336 {
337 // Guaranteed by in_queue, but guard against logic errors: the
338 // ring buffer must never become full (would alias head == tail).
339 ARMARX_CHECK(next(tail) != head);
340 tail = next(tail);
341 queue[tail] = v;
342 in_queue[v] = true;
343 verify(v, maxNumVerts, "in_queue");
344
345 // SLF (Smallest Label First): if the freshly pushed vertex is
346 // cheaper than the current front, swap it to be popped next.
347 const std::size_t front = next(head);
348 if (dists[queue[tail]] < dists[queue[front]])
349 {
350 std::swap(queue[tail], queue[front]);
351 }
352 }
353 }
354 }
355 }
356
357 // Copy output into numpy array
358 ARMARX_DEBUG << "Copy to output variables";
359
360 Eigen::MatrixXf output_dists(numRows, numCols);
361 Result::Mask reachable(numRows, numCols);
362 reachable.setOnes();
363
364 std::vector<std::vector<Eigen::Vector2i>> output_parents(
365 numRows, std::vector<Eigen::Vector2i>(numCols, Eigen::Vector2i{-1, -1}));
366
367 std::size_t invalids = 0;
368
369 for (std::size_t row = 0; row < numRows; ++row)
370 {
371 for (std::size_t col = 0; col < numCols; ++col)
372 {
373 const std::size_t u = ravel(row, col, numCols);
374 output_dists(row, col) = (dists[u] < inf - eps) * dists[u];
375
376 if (parents[u] == -1) // no parent
377 {
378 invalids++;
379 reachable(row, col) = false;
380 continue;
381 }
382
383 output_parents.at(row).at(col) = unravel(parents[u], numCols);
384 }
385 }
386
387 // "fix": the initial position does not have any parents. But it's still reachable ...
388 reachable(source.x(), source.y()) = true;
389
390 ARMARX_VERBOSE << "Fraction of invalid cells: (" << invalids << "/" << parents.size()
391 << ")";
392
393 ARMARX_DEBUG << "Done.";
394
395// The distances are in a unit-like system, so the distance between two neighbor cells is 1
396 // We need to convert it back to a metric system (mutiplying it by the cell size) to be independent of the cell size.
397 return Result{.distances = costmap.params().cellSize * output_dists,
398 .parents = output_parents,
399 .reachable = reachable};
400 }
401
402 std::optional<ShortestPathFasterAlgorithm::ClosestReachableResult>
404 const ShortestPathFasterAlgorithm::Result& spfaResult,
405 const Eigen::Vector2f& goal,
406 const Costmap& costmap)
407 {
408 const Costmap::Vertex goalVertex = costmap.toVertex(goal);
409 const Eigen::Vector2i goalIdx = goalVertex.index;
410
411 float minDistSq = std::numeric_limits<float>::max();
412 Eigen::Vector2i closestIdx{-1, -1};
413
414 for (int row = 0; row < spfaResult.reachable.rows(); ++row)
415 {
416 for (int col = 0; col < spfaResult.reachable.cols(); ++col)
417 {
418 if (!spfaResult.reachable(row, col))
419 {
420 continue;
421 }
422
423 const float dx = static_cast<float>(row) - static_cast<float>(goalIdx.x());
424 const float dy = static_cast<float>(col) - static_cast<float>(goalIdx.y());
425 const float distSq = dx * dx + dy * dy;
426
427 if (distSq < minDistSq)
428 {
429 minDistSq = distSq;
430 closestIdx = Eigen::Vector2i{row, col};
431 }
432 }
433 }
434
435 if (closestIdx.x() < 0)
436 {
437 return std::nullopt;
438 }
439
441 .position = costmap.toPositionGlobal(closestIdx),
442 .gridIndex = closestIdx,
443 .euclideanDistanceToGoal = std::sqrt(minDistSq) * costmap.params().cellSize};
444 }
445
446
447} // namespace armarx::navigation::algorithms::spfa
uint8_t index
#define VAROUT(x)
std::string str(const T &t)
ShortestPathFasterAlgorithm(const Costmap &costmap, const Parameters &params)
PlanningResult constructPath(const Eigen::Vector2f &start, const std::vector< std::vector< Eigen::Vector2i > > &spfaParents, const Eigen::Vector2f &goal) const
Result spfa(const Eigen::Vector2f &start, bool checkStartForCollision=true) const
PlanningResult plan(const Eigen::Vector2f &start, const Eigen::Vector2f &goal, bool checkStartForCollision=true) const
#define ARMARX_CHECK_GREATER(lhs, rhs)
This macro evaluates whether lhs is greater (>) than rhs and if it turns out to be false it will thro...
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#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_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:190
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:184
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:193
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:187
std::optional< ShortestPathFasterAlgorithm::ClosestReachableResult > findClosestReachablePosition(const ShortestPathFasterAlgorithm::Result &spfaResult, const Eigen::Vector2f &goal, const Costmap &costmap)