SPFA.cpp
Go to the documentation of this file.
1#include "SPFA.h"
2
3#include <algorithm>
4#include <cmath>
5#include <cstddef>
6#include <optional>
7#include <tuple>
8#include <vector>
9
10#include <Eigen/Geometry>
11
12#include <IceUtil/Time.h>
13
14#include <range/v3/range/conversion.hpp>
15#include <range/v3/view/transform.hpp>
16#include <range/v3/view/zip.hpp>
17
18#include <VirtualRobot/Robot.h> // IWYU pragma: keep
19#include <VirtualRobot/math/Helpers.h>
20
28
31
39#include <armarx/navigation/global_planning/aron/SPFAParams.aron.generated.h>
43
45{
46
47 // SPFAParams
48
51 {
52 return Algorithms::SPFA;
53 }
54
57 {
58 arondto::SPFAParams dto;
59
60 aron_conv::toAron(dto, *this);
61
62 return dto.toAron();
63 }
64
67 {
69
70 // ARMARX_DEBUG << dict->getAllKeysAsString();
71
72 arondto::SPFAParams dto;
73 dto.fromAron(dict);
74
75 SPFAParams bo;
76 aron_conv::fromAron(dto, bo);
77
78 return bo;
79 }
80
81 // SPFA
82
83 SPFA::SPFA(const Params& params,
85 const core::Scene& ctx) :
87 impl_(params,
89 ctx.staticScene.has_value() ? ctx.staticScene->distanceToObstaclesCostmap
90 : std::nullopt)
91 {
92 }
93
94 std::optional<GlobalPlannerResult>
96 {
97 const core::Pose start(scene.robot->getGlobalPose());
98 return plan(start, goal);
99 }
100
101 std::optional<GlobalPlannerResult>
102 SPFA::plan(const core::Pose& start, const core::Pose& goal)
103 {
104 if (scene.staticScene.has_value())
105 {
106 impl_.updateCostmap(scene.staticScene->distanceToObstaclesCostmap);
107 }
108 return impl_.plan(start, goal);
109 }
110
113 {
114 return impl_.executePlanner(start);
115 }
116
117 std::optional<GlobalPlannerResult>
118 SPFA::calculatePath(const PlanningResult& planner, const core::Pose& goal)
119 {
120 return impl_.calculatePath(planner, goal);
121 }
122
123 // SPFAImpl
124
126 const core::GeneralConfig& generalParams,
127 const std::optional<navigation::algorithms::Costmap>& costmap) :
128 params_(params), generalConfig_(generalParams), costmap_(costmap)
129 {
130 }
131
132 void
133 SPFAImpl::updateCostmap(const std::optional<navigation::algorithms::Costmap>& costmap)
134 {
135 if (costmap.has_value())
136 {
137 costmap_.emplace(costmap.value());
138 }
139 else
140 {
141 costmap_.reset();
142 }
143 }
144
145 std::optional<GlobalPlannerResult>
146 SPFAImpl::plan(const core::Pose& start, const core::Pose& goal)
147 {
148 const Eigen::Vector2f startPos2D = conv::to2D(start.translation());
149
150 // Check if start is in collision
151 if (costmap_->isInCollision(startPos2D))
152 {
153 ARMARX_WARNING << "Start position " << startPos2D << " is in collision. "
154 << "Searching for recovery position within "
155 << generalConfig_.inCollisionDistanceThresholdForRecovery << " mm...";
156
157 const auto recoveryVertex = costmap_->findClosestCollisionFreeVertex(
158 startPos2D, generalConfig_.inCollisionDistanceThresholdForRecovery);
159
160 if (!recoveryVertex)
161 {
162 ARMARX_ERROR << "No valid recovery position found within threshold of "
163 << generalConfig_.inCollisionDistanceThresholdForRecovery << " mm";
164 return std::nullopt;
165 }
166
167 const Eigen::Vector2f recoveryPos = recoveryVertex->position;
168
169 ARMARX_WARNING << "Found recovery position at " << recoveryPos
170 << " (distance: " << (recoveryPos - startPos2D).norm() << " mm). "
171 << "Prepending original start position to trajectory.";
172
173 const auto planningResult = executePlannerFromPosition(recoveryPos);
174
175 if (!planningResult.plan.has_value())
176 {
177 return std::nullopt;
178 }
179
180 const RecoveryInfo recoveryInfo{start, recoveryPos};
181
182 auto result = calculatePath(planningResult, goal, recoveryInfo);
183
184 if (!result && generalConfig_.navigateCloseAsPossible)
185 {
186 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
188 planningResult.plan.value(), goalPos, costmap_.value());
189 if (closest)
190 {
191 ARMARX_WARNING << "Goal " << goalPos << " is not reachable. "
192 << "Navigating to closest reachable position at "
193 << closest->position
194 << " (distance to goal: " << closest->euclideanDistanceToGoal
195 << " mm)";
196 core::Pose alternativeGoal = goal;
197 alternativeGoal.translation().head<2>() = closest->position;
198 result = calculatePath(planningResult, alternativeGoal, recoveryInfo);
199 }
200 }
201
202 return result;
203 }
204
205 // Normal case: start is valid
206 const auto planningResult = executePlanner(start);
207 auto result = calculatePath(planningResult, goal);
208
209 if (!result && generalConfig_.navigateCloseAsPossible && planningResult.plan.has_value())
210 {
211 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
213 planningResult.plan.value(), goalPos, costmap_.value());
214 if (closest)
215 {
216 ARMARX_WARNING << "Goal " << goalPos << " is not reachable. "
217 << "Navigating to closest reachable position at "
218 << closest->position
219 << " (distance to goal: " << closest->euclideanDistanceToGoal
220 << " mm)";
221 core::Pose alternativeGoal = goal;
222 alternativeGoal.translation().head<2>() = closest->position;
223 result = calculatePath(planningResult, alternativeGoal);
224 }
225 }
226
227 return result;
228 }
229
232 {
234
235 const Eigen::Vector2f startPosition = conv::to2D(start.translation());
236
237 // FIXME check if costmap is available
238 ARMARX_CHECK(costmap_.has_value());
239
241
242 PlanningResult result{
243 .start = start, // Preserve full pose including orientation
244 .algorithm =
245 algorithms::spfa::ShortestPathFasterAlgorithm(costmap_.value(), spfaParams),
246 .plan = std::nullopt,
247 };
248
249 const auto timeStart = IceUtil::Time::now();
250
252 try
253 {
254 result.plan = result.algorithm.spfa(startPosition, false);
255 }
256 catch (...)
257 {
258 ARMARX_INFO << "Could not execute spfa from " << "(" << startPosition.x() << ","
259 << startPosition.y() << ")" << " due to exception "
261 }
262
263 const auto timeEnd = IceUtil::Time::now();
264
265 ARMARX_VERBOSE << "SPFA execution time: " << (timeEnd - timeStart).toMilliSeconds()
266 << " ms";
267
268 return result;
269 }
270
272 SPFAImpl::executePlannerFromPosition(const Eigen::Vector2f& startPosition)
273 {
274 // Create a pose with identity orientation for backward compatibility
275 const core::Pose startPose(Eigen::Translation3f(conv::to3D(startPosition)));
276 return executePlanner(startPose);
277 }
278
279 std::optional<GlobalPlannerResult>
281 const core::Pose& goal,
282 const std::optional<RecoveryInfo>& recovery)
283 {
284 if (not planner.plan.has_value())
285 {
286 ARMARX_INFO << "Invalid spfa result, could not calculate path!";
287 return std::nullopt;
288 }
289
290 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
291
292 // Determine effective start position
293 Eigen::Vector2f effectiveStartPos;
294 if (recovery.has_value())
295 {
296 effectiveStartPos = recovery->recoveryPosition;
297 }
298 else
299 {
300 effectiveStartPos = conv::to2D(planner.start.translation());
301 }
302
304 try
305 {
306 plan =
307 planner.algorithm.constructPath(effectiveStartPos, planner.plan->parents, goalPos);
309 }
310 catch (...)
311 {
312 ARMARX_INFO << "Could not plan collision-free path from"
313 << (recovery ? recovery->recoveryPosition
314 : conv::to2D(planner.start.translation()))
315 << " to " << "(" << goal.translation().x() << "," << goal.translation().y()
316 << ")" << " due to exception " << GetHandledExceptionString();
317
318 return std::nullopt;
319 }
320
321 // Prepend original start position if recovery was used
322 if (recovery.has_value())
323 {
324 plan.path.insert(plan.path.begin(),
325 conv::to2D(recovery->originalStartPose.translation()));
326 ARMARX_INFO << "Prepended original start position. Path now has " << plan.path.size()
327 << " points.";
328 }
329
330 ARMARX_VERBOSE << "Path contains " << plan.path.size() << " points";
331
332 ARMARX_DEBUG << "The plan consists of the following positions:";
333 for (const auto& position : plan.path)
334 {
335 ARMARX_DEBUG << position;
336 }
337
339 const auto plan3d = conv::to3D(plan.path);
340
341 std::vector<core::Position> wpts;
342 // when the position is already reached (but not the orientation) the planned path is empty
343 if (plan3d.size() >= 2)
344 {
345 for (size_t i = 1; i < (plan3d.size() - 1); i++)
346 {
347 wpts.push_back(plan3d.at(i));
348 }
349 }
350
351 // ARMARX_TRACE;
352 // auto smoothPlan = postProcessPath(plan.path);
353 // ARMARX_IMPORTANT << "Smooth path contains " << smoothPlan.size() << " points";
354
355 // ARMARX_TRACE;
356 // // we need to strip the first and the last points from the plan as they encode the start and goal position
357 // smoothPlan.erase(smoothPlan.begin());
358 // smoothPlan.pop_back();
359
361
362 // Compute velocities for all positions
363 // For recovery case: includes collision start (which is planner.start) and recovery position (which is wpts[0])
364 const std::vector<float> velocitiesRespectingObstacles = [&]()
365 {
366 const float defaultVelocity = generalConfig_.maxVel.linear;
367
368 core::Positions positions;
369 positions.reserve(wpts.size() + 2);
370
371 // For recovery case: use the collision start pose from recoveryInfo
372 // For normal case: use planner.start
373 const core::Position startPosition = recovery.has_value()
374 ? recovery->originalStartPose.translation()
375 : planner.start.translation();
376
377 positions.emplace_back(startPosition);
378 positions.insert(positions.end(), wpts.begin(), wpts.end());
379 positions.emplace_back(goal.translation());
380
381 const auto maxVelocityBasedOnObstacles = [&](const core::Position& position) -> float
382 {
383 const auto minDistanceToObstaclesOpt =
384 costmap_->value(Eigen::Vector2f{position.head<2>()});
385
386 if (not minDistanceToObstaclesOpt.has_value())
387 {
388 return defaultVelocity;
389 }
390
391 const float clippedObstacleDistance =
392 std::min(minDistanceToObstaclesOpt.value(), spfaParams.obstacleMaxDistance);
393
394 const float ds =
395 std::pow(1.F - clippedObstacleDistance / spfaParams.obstacleMaxDistance,
396 spfaParams.obstacleCostExponent);
397
398 const float obstacleFactor = 1 / (1 + spfaParams.obstacleDistanceWeight * ds);
399
400 return defaultVelocity * obstacleFactor;
401 };
402
403 return positions | ranges::views::transform(maxVelocityBasedOnObstacles) |
404 ranges::to_vector;
405 }();
406
408
409 // Build trajectory normally (same for both recovery and non-recovery cases)
410 // Use the collision start pose for recovery case, planner.start for normal case
411 const core::Pose& trajectoryStart =
412 recovery.has_value() ? recovery->originalStartPose : planner.start;
413
415 trajectoryStart, wpts, goal, velocitiesRespectingObstacles);
416
417 // TODO(fabian.reister): resampling of trajectory
418
419 std::optional<core::GlobalTrajectory> resampledTrajectory;
420
421 try
422 {
423 resampledTrajectory = trajectory.resample(200);
424 ARMARX_DEBUG << "Terminal velocity: " << resampledTrajectory->points().back().velocity;
425 }
426 catch (...)
427 {
428 ARMARX_INFO << "Caught exception during resampling: " << GetHandledExceptionString();
429 resampledTrajectory = trajectory;
430 }
431
432 ARMARX_VERBOSE << "Resampled trajectory contains " << resampledTrajectory->points().size()
433 << " points";
434
435 resampledTrajectory->setMaxVelocity(generalConfig_.maxVel.linear);
436 ARMARX_DEBUG << "Terminal velocity: " << resampledTrajectory->points().back().velocity;
437
438 if (resampledTrajectory->points().size() == 2)
439 {
440 ARMARX_VERBOSE << "Only start and goal provided. Not optimizing orientation";
442 return GlobalPlannerResult{.trajectory = resampledTrajectory.value(),
443 .helperTrajectory = std::nullopt};
444 }
445
446 // Compute recovery distance and find sub-trajectory start index
447 const auto computeRecoveryDistance = [&]() -> float
448 {
449 if (!recovery.has_value())
450 return 0.0f;
451 return (recovery->recoveryPosition -
452 recovery->originalStartPose.translation().head<2>())
453 .norm();
454 };
455 const float recoveryDistance = computeRecoveryDistance();
456
457 // Find first point outside recovery distance (for sub-trajectory extraction)
458 size_t subTrajectoryStartIndex = 0;
459 if (recoveryDistance > 0)
460 {
461 auto& pts = resampledTrajectory->mutablePoints();
462 const auto startOrientation = pts.front().waypoint.pose.linear();
463
464 // Fix orientations for all points within recovery distance (collision segment)
465 for (size_t i = 0; i < pts.size(); i++)
466 {
467 const float dist =
468 (pts[i].waypoint.pose.translation() - pts.front().waypoint.pose.translation())
469 .norm();
470 if (dist <= recoveryDistance)
471 {
472 pts[i].waypoint.pose.linear() = startOrientation;
473 }
474 else
475 {
476 // Found first point outside collision - fix it for smooth transition
477 subTrajectoryStartIndex = i;
478 if (subTrajectoryStartIndex < pts.size())
479 {
480 pts[subTrajectoryStartIndex].waypoint.pose.linear() = startOrientation;
481 }
482 break;
483 }
484 }
485 ARMARX_INFO << "Recovery distance: " << recoveryDistance << " mm, fixed "
486 << subTrajectoryStartIndex
487 << " collision segment points, sub-trajectory starts at index "
488 << subTrajectoryStartIndex;
489 }
490
491 // Extract sub-trajectory for optimization (full trajectory if no recovery)
492 std::optional<core::GlobalTrajectory> trajectoryToOptimize;
493 if (subTrajectoryStartIndex > 0)
494 {
495 trajectoryToOptimize = resampledTrajectory->getSubTrajectory(
496 subTrajectoryStartIndex, resampledTrajectory->points().size());
497 }
498 else
499 {
500 trajectoryToOptimize = resampledTrajectory;
501 }
502
504 {
505 const armarx::PackagePath pp("armarx_navigation",
506 "config/global_planning/OrientationOptimizer.json");
507 const std::string filename = pp.toSystemPath();
508
509 ARMARX_CHECK(std::filesystem::exists(filename))
510 << "OrientationOptimizer config file does not exist: " << filename;
511
512 ARMARX_INFO << "Loading config from file `" << filename << "`.";
513 std::ifstream ifs{filename};
514
515 nlohmann::json jsonConfig;
516 ifs >> jsonConfig;
517
518 ARMARX_VERBOSE << "Initializing config";
519
520
522
523 armarx::navigation::global_planning::arondto::OrientationOptimizerParams dto;
524
525 ARMARX_VERBOSE << "reading file.";
526 dto.read(reader, jsonConfig);
527
528 aron_conv::fromAron(dto, params_.optimizerParams);
529 }
530
531 OrientationOptimizer optimizer(trajectoryToOptimize.value(), params_.optimizerParams);
532 auto result = optimizer.optimize();
533
534 if (not result)
535 {
536 ARMARX_ERROR << "Optimizer failure";
537 return std::nullopt;
538 }
539
540 // Combine collision segment (fixed) with optimized sub-trajectory
541 core::GlobalTrajectory finalTrajectory = result.trajectory.value();
542
543 if (recoveryDistance > 0 && subTrajectoryStartIndex > 0)
544 {
545 // Stitch: fixed collision segment + optimized post-collision segment
546 auto& resampledPts = resampledTrajectory->mutablePoints();
547 const auto& optimizedPts = result.trajectory->points();
548
549 // Verify sizes match
550 if (subTrajectoryStartIndex + optimizedPts.size() == resampledPts.size())
551 {
552 // Copy optimized orientations and velocities to the resampled trajectory
553 for (size_t i = 0; i < optimizedPts.size(); i++)
554 {
555 resampledPts[subTrajectoryStartIndex + i].waypoint.pose.linear() =
556 optimizedPts[i].waypoint.pose.linear();
557 resampledPts[subTrajectoryStartIndex + i].velocity = optimizedPts[i].velocity;
558 }
559
560 // Use the resampled trajectory (with fixed collision segment + optimized post-collision)
561 finalTrajectory = resampledTrajectory.value();
562
563 ARMARX_INFO << "Combined fixed collision segment (" << subTrajectoryStartIndex
564 << " points) with optimized trajectory (" << optimizedPts.size()
565 << " points)";
566 }
567 else
568 {
569 ARMARX_WARNING << "Trajectory size mismatch in collision recovery stitching. "
570 << "Using optimized trajectory only.";
571 }
572 }
573
574 // TODO circular path smoothing should be done now
575
576 // algorithm::CircularPathSmoothing smoothing;
577 // auto smoothTrajectory = smoothing.smooth(result.trajectory.value());
578 // smoothTrajectory.setMaxVelocity(params.linearVelocity);
579
580 ARMARX_CHECK(costmap_.has_value());
581 const auto& costmap = costmap_.value();
582
583 for (auto& point : finalTrajectory.mutablePoints())
584 {
585 const float distance = std::min<float>(
586 spfaParams.obstacleMaxDistance,
587 costmap.value(Eigen::Vector2f{point.waypoint.pose.translation().head<2>()})
588 .value_or(0.F));
589
590 if (spfaParams.obstacleDistanceCosts)
591 {
592 const float obstacleBasedVelocity =
593 generalConfig_.maxVel.linear /
594 (1.F + spfaParams.obstacleDistanceWeight *
595 std::pow(1 - distance / spfaParams.obstacleMaxDistance,
596 spfaParams.obstacleCostExponent));
597
598 // only reduce velocity
599 point.velocity = std::min(obstacleBasedVelocity, point.velocity);
600 }
601 }
602
603
605 ARMARX_DEBUG << "Terminal velocity: " << finalTrajectory.points().back().velocity;
606 return GlobalPlannerResult{.trajectory = finalTrajectory, .helperTrajectory = std::nullopt};
607 }
608
609 std::vector<Eigen::Vector2f>
610 SPFAImpl::postProcessPath(const std::vector<Eigen::Vector2f>& path)
611 {
612 /// chain approximation
614 path, algorithm::ChainApproximation::Params{.distanceTh = 200.F});
615 approx.approximate();
616 const auto p = approx.approximatedChain();
617
618 // visualizePath(p, "approximated", simox::Color::green());
619
620 // algo::CircularPathSmoothing smoothing;
621 // const auto points = smoothing.smooth(p);
622
623 return p;
624 }
625
626} // namespace armarx::navigation::global_planning
static std::filesystem::path toSystemPath(const data::PackagePath &pp)
PlanningResult constructPath(const Eigen::Vector2f &start, const std::vector< std::vector< Eigen::Vector2i > > &spfaParents, const Eigen::Vector2f &goal) const
PlanningResult plan(const Eigen::Vector2f &start, const Eigen::Vector2f &goal, bool checkStartForCollision=true) const
const std::vector< GlobalTrajectoryPoint > & points() const
std::vector< GlobalTrajectoryPoint > & mutablePoints()
static GlobalTrajectory FromPath(const Path &path, float velocity)
Note: the velocity will not be set!
GlobalPlanner(const core::GeneralConfig &generalConfig, const core::Scene &scene)
::armarx::navigation::global_planning::PlanningResult PlanningResult
Definition SPFA.h:89
void updateCostmap(const std::optional< navigation::algorithms::Costmap > &costmap)
Definition SPFA.cpp:133
std::optional< GlobalPlannerResult > plan(const core::Pose &start, const core::Pose &goal)
Definition SPFA.cpp:146
SPFAImpl(const Params &params, const core::GeneralConfig &generalParams, const std::optional< navigation::algorithms::Costmap > &costmap)
Definition SPFA.cpp:125
PlanningResult executePlannerFromPosition(const Eigen::Vector2f &startPosition)
Definition SPFA.cpp:272
PlanningResult executePlanner(const core::Pose &start)
Definition SPFA.cpp:231
std::vector< Eigen::Vector2f > postProcessPath(const std::vector< Eigen::Vector2f > &path)
Definition SPFA.cpp:610
std::optional< GlobalPlannerResult > calculatePath(const PlanningResult &planner, const core::Pose &goal, const std::optional< RecoveryInfo > &recovery=std::nullopt)
Definition SPFA.cpp:280
::armarx::navigation::global_planning::PlanningResult PlanningResult
Definition SPFA.h:125
std::optional< GlobalPlannerResult > calculatePath(const PlanningResult &planner, const core::Pose &goal)
Definition SPFA.cpp:118
std::optional< GlobalPlannerResult > plan(const core::Pose &goal) override
Definition SPFA.cpp:95
PlanningResult executePlanner(const core::Pose &start)
Definition SPFA.cpp:112
SPFA(const Params &params, const core::GeneralConfig &generalParams, const core::Scene &ctx)
Definition SPFA.cpp:83
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_NOT_NULL(ptr)
This macro evaluates whether ptr is not null and if it turns out to be false it will throw an Express...
#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_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::shared_ptr< Dict > DictPtr
Definition Dict.h:42
std::optional< ShortestPathFasterAlgorithm::ClosestReachableResult > findClosestReachablePosition(const ShortestPathFasterAlgorithm::Result &spfaResult, const Eigen::Vector2f &goal, const Costmap &costmap)
std::vector< Eigen::Vector2f > to2D(const std::vector< Eigen::Vector3f > &v)
Definition eigen.cpp:29
std::vector< Eigen::Vector3f > to3D(const std::vector< Eigen::Vector2f > &v)
Definition eigen.cpp:14
std::vector< Position > Positions
Definition basic_types.h:37
Eigen::Isometry3f Pose
Definition basic_types.h:31
Eigen::Vector3f Position
Definition basic_types.h:36
void toAron(arondto::GlobalPlannerParams &dto, const GlobalPlannerParams &bo)
void fromAron(const arondto::GlobalPlannerParams &dto, GlobalPlannerParams &bo)
This file is part of ArmarX.
Definition fwd.h:30
std::string GetHandledExceptionString()
double norm(const Point &a)
Definition point.hpp:102
double distance(const Point &a, const Point &b)
Definition point.hpp:95
algorithms::spfa::ShortestPathFasterAlgorithm algorithm
Definition SPFA.h:66
std::optional< algorithms::spfa::ShortestPathFasterAlgorithm::Result > plan
Definition SPFA.h:67
Information about collision recovery.
Definition SPFA.h:77
aron::data::DictPtr toAron() const override
Definition SPFA.cpp:56
static SPFAParams FromAron(const aron::data::DictPtr &dict)
Definition SPFA.cpp:66
Algorithms algorithm() const override
Definition SPFA.cpp:50
#define ARMARX_TRACE
Definition trace.h:77