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 costmapWithValidStart(startVertex.index.x(), startVertex.index.y()) = 0.1;
174 }
175
176 return spfa(
177 costmapWithValidStart, Eigen::Vector2i{startVertex.index}, checkStartForCollision);
178 }
179
181 ShortestPathFasterAlgorithm::spfa(const Eigen::MatrixXf& inputMap,
182 const Eigen::Vector2i& source,
183 const bool checkStartForCollision) const
184 {
185 if (checkStartForCollision)
186 {
187 ARMARX_CHECK_GREATER(inputMap(source.x(), source.y()), 0.F)
188 << "Start must not be in collision";
189 }
190
191 constexpr float eps = 1e-6;
192 constexpr std::size_t numDirs = 8;
193
194 const std::array<std::array<std::int64_t, 2>, numDirs> dirs{
195 std::array<std::int64_t, 2>{-1, -1},
196 std::array<std::int64_t, 2>{-1, 0},
197 std::array<std::int64_t, 2>{-1, 1},
198 std::array<std::int64_t, 2>{0, 1},
199 std::array<std::int64_t, 2>{1, 1},
200 std::array<std::int64_t, 2>{1, 0},
201 std::array<std::int64_t, 2>{1, -1},
202 std::array<std::int64_t, 2>{0, -1}};
203
204 const std::array<float, numDirs> dirLengths{
205 std::sqrt(2.0f), 1, std::sqrt(2.0f), 1, std::sqrt(2.0f), 1, std::sqrt(2.0f), 1};
206
207 // Process input map
208 // py::buffer_info map_buf = input_map.request();
209 const std::size_t numRows = inputMap.rows();
210 const std::size_t numCols = inputMap.cols();
211
212 // Get source coordinates
213 const int source_i = source.x();
214 const int source_j = source.y();
215
216 const std::size_t maxNumVerts = numRows * numCols;
217 constexpr std::size_t maxEdgesPerVert = numDirs;
218 // const float inf = 2 * maxNumVerts;
219 const float inf = 2 * maxNumVerts; // FIXME numeric_limits<float>::max();
220 const std::size_t queueSize = numDirs * numRows * numCols;
221
222 // Initialize arrays
223 std::vector<std::size_t> edges(maxNumVerts * maxEdgesPerVert);
224 std::vector<std::size_t> edge_counts(maxNumVerts);
225 std::vector<std::size_t> queue(queueSize);
226 std::vector<bool> in_queue(maxNumVerts);
227 std::vector<float> weights(maxNumVerts * maxEdgesPerVert);
228 std::vector<float> dists(maxNumVerts, inf); // initialize with inf
229
230 const auto& verify = [](std::int64_t index, std::size_t maxIndex, const std::string& str)
231 {
232 if (index >= static_cast<std::int64_t>(maxIndex) || index < 0)
233 {
234 ARMARX_IMPORTANT << "invalid index " << index << " max allowed is " << maxIndex
235 << "; " << str;
236 }
237 };
238
239
240 // Build graph
241 ARMARX_VERBOSE << "Build graph";
242 for (std::size_t row = 0; row < numRows; ++row)
243 {
244 for (std::size_t col = 0; col < numCols; ++col)
245 {
246 const std::size_t v = ravel(row, col, numCols);
247 if (inputMap(static_cast<int>(row), static_cast<int>(col)) <= 0.F) // collision
248 {
249 continue;
250 }
251
252 for (std::size_t k = 0; k < numDirs; ++k)
253 {
254 const std::int64_t ip = static_cast<std::int64_t>(row) +
255 dirs[k][0]; // could eventually become negative
256 const std::int64_t jp = static_cast<std::int64_t>(col) + dirs[k][1];
257
258 if (ip < 0 || jp < 0 || ip >= static_cast<std::int64_t>(numRows) ||
259 jp >= static_cast<std::int64_t>(numCols))
260 {
261 continue;
262 }
263
264 const std::size_t vp = ravel(ip, jp, numCols);
265 if (inputMap(ip, jp) <= 0.F) // collision
266 {
267 continue;
268 }
269
270 const float clippedObstacleDistance =
271 std::min(inputMap(ip, jp), params.obstacleMaxDistance);
272
273 const float travelCost = dirLengths[k];
274
275 const float targetDistanceCost =
276 params.obstacleDistanceWeight *
277 std::pow(1.F - clippedObstacleDistance / params.obstacleMaxDistance,
278 params.obstacleCostExponent);
279
280 const float edgeCost = params.obstacleDistanceCosts
281 ? travelCost * (1 + targetDistanceCost)
282 : travelCost;
283
284 const std::size_t e = ravel(v, edge_counts.at(v), maxEdgesPerVert);
285 edges.at(e) = vp;
286 verify(e, maxNumVerts * maxEdgesPerVert, "edges");
287 weights.at(e) = edgeCost;
288 verify(e, maxNumVerts * maxEdgesPerVert, "weights");
289 edge_counts[v]++;
290 verify(v, maxNumVerts, "edges_counts");
291 }
292 }
293 }
294
295
296 // SPFA
297 ARMARX_DEBUG << "SPFA";
298 std::size_t head = 0;
299 std::size_t tail = 0;
300
301 const std::size_t s = ravel(source_i, source_j, numCols);
302 dists[s] = 0;
303 verify(s, maxNumVerts, "dists");
304 queue[++tail] = s;
305 verify(tail, queueSize, "queue");
306 in_queue[s] = true;
307 verify(s, maxNumVerts, "in_queue");
308
309 std::vector<std::int64_t> parents(maxNumVerts, -1);
310 while (head < tail)
311 {
312 const std::size_t u = queue[++head];
313 in_queue[u] = false;
314 verify(u, maxNumVerts, "in_queue");
315 for (std::size_t j = 0; j < edge_counts[u]; ++j)
316 {
317 const std::size_t e = ravel(u, j, maxEdgesPerVert);
318 const std::size_t v = edges[e];
319 const float newDist = dists[u] + weights[e];
320 if (newDist < dists[v])
321 {
322 parents[v] = u;
323 verify(v, maxNumVerts, "parents");
324 dists[v] = newDist;
325 verify(v, maxNumVerts, "dists");
326 if (not in_queue[v])
327 {
328 ARMARX_CHECK_LESS(tail, queueSize);
329 queue[++tail] = v;
330 verify(tail, queueSize, "queue!");
331 in_queue[v] = true;
332 verify(v, maxNumVerts, "in_queue");
333
334 if (dists[queue[tail]] < dists[queue[head + 1]])
335 {
336 std::swap(queue[tail], queue[head + 1]);
337 }
338 }
339 }
340 }
341 }
342
343 // Copy output into numpy array
344 ARMARX_DEBUG << "Copy to output variables";
345
346 Eigen::MatrixXf output_dists(numRows, numCols);
347 Result::Mask reachable(numRows, numCols);
348 reachable.setOnes();
349
350 std::vector<std::vector<Eigen::Vector2i>> output_parents(
351 numRows, std::vector<Eigen::Vector2i>(numCols, Eigen::Vector2i{-1, -1}));
352
353 std::size_t invalids = 0;
354
355 for (std::size_t row = 0; row < numRows; ++row)
356 {
357 for (std::size_t col = 0; col < numCols; ++col)
358 {
359 const std::size_t u = ravel(row, col, numCols);
360 output_dists(row, col) = (dists[u] < inf - eps) * dists[u];
361
362 if (parents[u] == -1) // no parent
363 {
364 invalids++;
365 reachable(row, col) = false;
366 continue;
367 }
368
369 output_parents.at(row).at(col) = unravel(parents[u], numCols);
370 }
371 }
372
373 // "fix": the initial position does not have any parents. But it's still reachable ...
374 reachable(source.x(), source.y()) = true;
375
376 ARMARX_VERBOSE << "Fraction of invalid cells: (" << invalids << "/" << parents.size()
377 << ")";
378
379 ARMARX_DEBUG << "Done.";
380
381// The distances are in a unit-like system, so the distance between two neighbor cells is 1
382 // We need to convert it back to a metric system (mutiplying it by the cell size) to be independent of the cell size.
383 return Result{.distances = costmap.params().cellSize * output_dists,
384 .parents = output_parents,
385 .reachable = reachable};
386 }
387
388 std::optional<ShortestPathFasterAlgorithm::ClosestReachableResult>
390 const ShortestPathFasterAlgorithm::Result& spfaResult,
391 const Eigen::Vector2f& goal,
392 const Costmap& costmap)
393 {
394 const Costmap::Vertex goalVertex = costmap.toVertex(goal);
395 const Eigen::Vector2i goalIdx = goalVertex.index;
396
397 float minDistSq = std::numeric_limits<float>::max();
398 Eigen::Vector2i closestIdx{-1, -1};
399
400 for (int row = 0; row < spfaResult.reachable.rows(); ++row)
401 {
402 for (int col = 0; col < spfaResult.reachable.cols(); ++col)
403 {
404 if (!spfaResult.reachable(row, col))
405 {
406 continue;
407 }
408
409 const float dx = static_cast<float>(row) - static_cast<float>(goalIdx.x());
410 const float dy = static_cast<float>(col) - static_cast<float>(goalIdx.y());
411 const float distSq = dx * dx + dy * dy;
412
413 if (distSq < minDistSq)
414 {
415 minDistSq = distSq;
416 closestIdx = Eigen::Vector2i{row, col};
417 }
418 }
419 }
420
421 if (closestIdx.x() < 0)
422 {
423 return std::nullopt;
424 }
425
427 .position = costmap.toPositionGlobal(closestIdx),
428 .gridIndex = closestIdx,
429 .euclideanDistanceToGoal = std::sqrt(minDistSq) * costmap.params().cellSize};
430 }
431
432
433} // 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_LESS(lhs, rhs)
This macro evaluates whether lhs is less (<) than rhs and if it turns out to be false it will throw a...
#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_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)