SPFA.cpp
Go to the documentation of this file.
1#include "SPFA.h"
2
3#include <algorithm>
4#include <chrono>
5#include <cmath>
6#include <cstddef>
7#include <iomanip>
8#include <optional>
9#include <sstream>
10#include <tuple>
11#include <vector>
12
13#include <Eigen/Geometry>
14
15#include <IceUtil/Time.h>
16
17#include <range/v3/range/conversion.hpp>
18#include <range/v3/view/transform.hpp>
19#include <range/v3/view/zip.hpp>
20
21#include <VirtualRobot/Robot.h> // IWYU pragma: keep
22#include <VirtualRobot/math/Helpers.h>
23
31
34
46#include <armarx/navigation/global_planning/aron/SPFAParams.aron.generated.h>
50
51namespace
52{
53 struct PipelineTimings
54 {
55 std::chrono::microseconds constructPath{0};
56 std::chrono::microseconds buildTrajectory{0};
57 std::chrono::microseconds resample{0};
58 std::chrono::microseconds smoothPositions{0};
59 std::chrono::microseconds recomputeVelocities{0};
60 std::chrono::microseconds recoveryHandling{0};
61 std::chrono::microseconds loadOrientationConfig{0};
62 std::chrono::microseconds optimizeOrientation{0};
63 std::chrono::microseconds stitchTrajectory{0};
64 std::chrono::microseconds finalVelocityClamp{0};
65 };
66
67 class ScopedStageTimer
68 {
69 public:
70 explicit ScopedStageTimer(std::chrono::microseconds& out) :
71 out_(out), start_(std::chrono::steady_clock::now())
72 {
73 }
74
75 ~ScopedStageTimer()
76 {
77 out_ = std::chrono::duration_cast<std::chrono::microseconds>(
78 std::chrono::steady_clock::now() - start_);
79 }
80
81 private:
82 std::chrono::microseconds& out_;
83 std::chrono::steady_clock::time_point start_;
84 };
85
86 std::map<std::string, float> toMap(const PipelineTimings& timings)
87 {
88 const auto seconds = [](std::chrono::microseconds us) -> float
89 { return static_cast<float>(us.count()) / 1e6F; };
90
91 return {{"construct_path", seconds(timings.constructPath)},
92 {"build_trajectory", seconds(timings.buildTrajectory)},
93 {"resample", seconds(timings.resample)},
94 {"smooth_positions", seconds(timings.smoothPositions)},
95 {"recompute_velocities", seconds(timings.recomputeVelocities)},
96 {"recovery_handling", seconds(timings.recoveryHandling)},
97 {"load_orientation_config", seconds(timings.loadOrientationConfig)},
98 {"optimize_orientation", seconds(timings.optimizeOrientation)},
99 {"stitch_trajectory", seconds(timings.stitchTrajectory)},
100 {"final_velocity_clamp", seconds(timings.finalVelocityClamp)}};
101 }
102
103 /// The grid search runs before `calculatePath`, so its duration is carried on the
104 /// `PlanningResult` rather than in `PipelineTimings`.
105 std::map<std::string, float>
106 withGridSearch(const PipelineTimings& timings,
108 {
109 std::map<std::string, float> map = toMap(timings);
110 map["grid_search"] = planner.gridSearchDuration;
111
112 return map;
113 }
114
115 std::string formatTimings(const PipelineTimings& timings)
116 {
117 const auto ms = [](std::chrono::microseconds us) -> std::string
118 {
119 std::ostringstream oss;
120 oss << std::fixed << std::setprecision(3) << (us.count() / 1000.0);
121 return oss.str();
122 };
123
124 std::ostringstream oss;
125 oss << "[SPFA timing] "
126 << "build=" << ms(timings.buildTrajectory) << "ms "
127 << "resample=" << ms(timings.resample) << "ms "
128 << "smooth=" << ms(timings.smoothPositions) << "ms "
129 << "velocity=" << ms(timings.recomputeVelocities) << "ms "
130 << "recovery=" << ms(timings.recoveryHandling) << "ms "
131 << "orientationConfig=" << ms(timings.loadOrientationConfig) << "ms "
132 << "orientationOptimize=" << ms(timings.optimizeOrientation) << "ms "
133 << "stitch=" << ms(timings.stitchTrajectory) << "ms "
134 << "clamp=" << ms(timings.finalVelocityClamp) << "ms";
135 return oss.str();
136 }
137} // namespace
138
140{
141
142 // SPFAParams
143
146 {
147 return Algorithms::SPFA;
148 }
149
152 {
153 arondto::SPFAParams dto;
154
155 aron_conv::toAron(dto, *this);
156
157 return dto.toAron();
158 }
159
162 {
164
165 // ARMARX_DEBUG << dict->getAllKeysAsString();
166
167 arondto::SPFAParams dto;
168 dto.fromAron(dict);
169
170 SPFAParams bo;
171 aron_conv::fromAron(dto, bo);
172
173 return bo;
174 }
175
176 // SPFA
177
178 SPFA::SPFA(const Params& params,
180 const core::Scene& ctx) :
182 impl_(params,
184 ctx.staticScene.has_value() ? ctx.staticScene->distanceToObstaclesCostmap
185 : std::nullopt)
186 {
187 }
188
189 std::optional<GlobalPlannerResult>
191 {
192 const core::Pose start(scene.robot->getGlobalPose());
193 return plan(start, goal);
194 }
195
196 std::optional<GlobalPlannerResult>
197 SPFA::plan(const core::Pose& start, const core::Pose& goal)
198 {
199 if (scene.staticScene.has_value())
200 {
201 impl_.updateCostmap(scene.staticScene->distanceToObstaclesCostmap);
202 }
203 return impl_.plan(start, goal);
204 }
205
208 {
209 return impl_.executePlanner(start);
210 }
211
212 std::optional<GlobalPlannerResult>
213 SPFA::calculatePath(const PlanningResult& planner, const core::Pose& goal)
214 {
215 return impl_.calculatePath(planner, goal);
216 }
217
218 // SPFAImpl
219
221 const core::GeneralConfig& generalParams,
222 const std::optional<navigation::algorithms::Costmap>& costmap) :
223 params_(params), generalConfig_(generalParams), costmap_(costmap)
224 {
225 }
226
227 void
228 SPFAImpl::updateCostmap(const std::optional<navigation::algorithms::Costmap>& costmap)
229 {
230 if (costmap.has_value())
231 {
232 costmap_.emplace(costmap.value());
233 }
234 else
235 {
236 costmap_.reset();
237 }
238 }
239
242 {
243 ARMARX_CHECK(costmap_.has_value());
244
246 costmap_.value(),
248 .maxVelocity = generalConfig_.maxVel.linear,
249 .obstacleMaxDistance = params_.algo.obstacleMaxDistance,
250 .obstacleDistanceWeight = params_.algo.obstacleDistanceWeight,
251 .obstacleCostExponent = params_.algo.obstacleCostExponent}};
252 }
253
254 std::optional<GlobalPlannerResult>
255 SPFAImpl::plan(const core::Pose& start, const core::Pose& goal)
256 {
257 const Eigen::Vector2f startPos2D = conv::to2D(start.translation());
258
259 // Check if start is in collision
260 if (costmap_->isInCollision(startPos2D))
261 {
262 ARMARX_WARNING << "Start position " << startPos2D << " is in collision. "
263 << "Searching for recovery position within "
264 << generalConfig_.inCollisionDistanceThresholdForRecovery << " mm...";
265
266 const auto recoveryVertex = costmap_->findClosestCollisionFreeVertex(
267 startPos2D, generalConfig_.inCollisionDistanceThresholdForRecovery);
268
269 if (!recoveryVertex)
270 {
271 ARMARX_ERROR << "No valid recovery position found within threshold of "
272 << generalConfig_.inCollisionDistanceThresholdForRecovery << " mm";
273 return std::nullopt;
274 }
275
276 const Eigen::Vector2f recoveryPos = recoveryVertex->position;
277
278 ARMARX_WARNING << "Found recovery position at " << recoveryPos
279 << " (distance: " << (recoveryPos - startPos2D).norm() << " mm). "
280 << "Prepending original start position to trajectory.";
281
282 const auto planningResult = executePlannerFromPosition(recoveryPos);
283
284 if (!planningResult.plan.has_value())
285 {
286 return std::nullopt;
287 }
288
289 const RecoveryInfo recoveryInfo{start, recoveryPos};
290
291 auto result = calculatePath(planningResult, goal, recoveryInfo);
292
293 if (!result && generalConfig_.navigateCloseAsPossible)
294 {
295 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
297 planningResult.plan.value(), goalPos, costmap_.value());
298 if (closest)
299 {
300 ARMARX_WARNING << "Goal " << goalPos << " is not reachable. "
301 << "Navigating to closest reachable position at "
302 << closest->position
303 << " (distance to goal: " << closest->euclideanDistanceToGoal
304 << " mm)";
305 core::Pose alternativeGoal = goal;
306 alternativeGoal.translation().head<2>() = closest->position;
307 result = calculatePath(planningResult, alternativeGoal, recoveryInfo);
308 }
309 }
310
311 return result;
312 }
313
314 // Normal case: start is valid
315 const auto planningResult = executePlanner(start);
316 auto result = calculatePath(planningResult, goal);
317
318 if (!result && generalConfig_.navigateCloseAsPossible && planningResult.plan.has_value())
319 {
320 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
322 planningResult.plan.value(), goalPos, costmap_.value());
323 if (closest)
324 {
325 ARMARX_WARNING << "Goal " << goalPos << " is not reachable. "
326 << "Navigating to closest reachable position at "
327 << closest->position
328 << " (distance to goal: " << closest->euclideanDistanceToGoal
329 << " mm)";
330 core::Pose alternativeGoal = goal;
331 alternativeGoal.translation().head<2>() = closest->position;
332 result = calculatePath(planningResult, alternativeGoal);
333 }
334 }
335
336 return result;
337 }
338
341 {
343
344 const Eigen::Vector2f startPosition = conv::to2D(start.translation());
345
346 // FIXME check if costmap is available
347 ARMARX_CHECK(costmap_.has_value());
348
350
351 PlanningResult result{
352 .start = start, // Preserve full pose including orientation
353 .algorithm =
354 algorithms::spfa::ShortestPathFasterAlgorithm(costmap_.value(), spfaParams),
355 .plan = std::nullopt,
356 };
357
358 const auto timeStart = IceUtil::Time::now();
359
361 try
362 {
363 result.plan = result.algorithm.spfa(startPosition, false);
364 }
365 catch (...)
366 {
367 ARMARX_INFO << "Could not execute spfa from " << "(" << startPosition.x() << ","
368 << startPosition.y() << ")" << " due to exception "
370 }
371
372 const auto timeEnd = IceUtil::Time::now();
373
374 result.gridSearchDuration =
375 static_cast<float>((timeEnd - timeStart).toMicroSeconds()) / 1e6F;
376
377 ARMARX_VERBOSE << "SPFA execution time: " << (timeEnd - timeStart).toMilliSeconds()
378 << " ms";
379
380 return result;
381 }
382
384 SPFAImpl::executePlannerFromPosition(const Eigen::Vector2f& startPosition)
385 {
386 // Create a pose with identity orientation for backward compatibility
387 const core::Pose startPose(Eigen::Translation3f(conv::to3D(startPosition)));
388 return executePlanner(startPose);
389 }
390
391 std::optional<GlobalPlannerResult>
393 const core::Pose& goal,
394 const std::optional<RecoveryInfo>& recovery)
395 {
396 PipelineTimings timings;
397
398 if (not planner.plan.has_value())
399 {
400 ARMARX_INFO << "Invalid spfa result, could not calculate path!";
401 return std::nullopt;
402 }
403
404 const Eigen::Vector2f goalPos = conv::to2D(goal.translation());
405
406 // Determine effective start position
407 Eigen::Vector2f effectiveStartPos;
408 if (recovery.has_value())
409 {
410 effectiveStartPos = recovery->recoveryPosition;
411 }
412 else
413 {
414 effectiveStartPos = conv::to2D(planner.start.translation());
415 }
416
418 try
419 {
420 ScopedStageTimer timer(timings.constructPath);
421 plan =
422 planner.algorithm.constructPath(effectiveStartPos, planner.plan->parents, goalPos);
424 }
425 catch (...)
426 {
427 ARMARX_INFO << "Could not plan collision-free path from"
428 << (recovery ? recovery->recoveryPosition
429 : conv::to2D(planner.start.translation()))
430 << " to " << "(" << goal.translation().x() << "," << goal.translation().y()
431 << ")" << " due to exception " << GetHandledExceptionString();
432
433 return std::nullopt;
434 }
435
436 // Prepend original start position if recovery was used
437 if (recovery.has_value())
438 {
439 plan.path.insert(plan.path.begin(),
440 conv::to2D(recovery->originalStartPose.translation()));
441 ARMARX_INFO << "Prepended original start position. Path now has " << plan.path.size()
442 << " points.";
443 }
444
445 ARMARX_VERBOSE << "Path contains " << plan.path.size() << " points";
446
447 ARMARX_DEBUG << "The plan consists of the following positions:";
448 for (const auto& position : plan.path)
449 {
450 ARMARX_DEBUG << position;
451 }
452
454 const auto plan3d = conv::to3D(plan.path);
455
456 std::vector<core::Position> wpts;
457 // when the position is already reached (but not the orientation) the planned path is empty
458 if (plan3d.size() >= 2)
459 {
460 for (size_t i = 1; i < (plan3d.size() - 1); i++)
461 {
462 wpts.push_back(plan3d.at(i));
463 }
464 }
465
466 // ARMARX_TRACE;
467 // auto smoothPlan = postProcessPath(plan.path);
468 // ARMARX_IMPORTANT << "Smooth path contains " << smoothPlan.size() << " points";
469
470 // ARMARX_TRACE;
471 // // we need to strip the first and the last points from the plan as they encode the start and goal position
472 // smoothPlan.erase(smoothPlan.begin());
473 // smoothPlan.pop_back();
474
475 // Compute velocities for all positions
476 // For recovery case: includes collision start (which is planner.start) and recovery position (which is wpts[0])
477 core::Positions positionsForInitialVelocities;
478 positionsForInitialVelocities.reserve(wpts.size() + 2);
479
480 // For recovery case: use the collision start pose from recoveryInfo
481 // For normal case: use planner.start
482 const core::Position startPosition = recovery.has_value()
483 ? recovery->originalStartPose.translation()
484 : planner.start.translation();
485
486 positionsForInitialVelocities.emplace_back(startPosition);
487 positionsForInitialVelocities.insert(
488 positionsForInitialVelocities.end(), wpts.begin(), wpts.end());
489 positionsForInitialVelocities.emplace_back(goal.translation());
490
491 const std::vector<float> velocitiesRespectingObstacles =
492 velocityLimit().at(positionsForInitialVelocities);
493
495
496 // Build trajectory normally (same for both recovery and non-recovery cases)
497 // Use the collision start pose for recovery case, planner.start for normal case
498 const core::Pose& trajectoryStart =
499 recovery.has_value() ? recovery->originalStartPose : planner.start;
500
502 {
503 ScopedStageTimer timer(timings.buildTrajectory);
505 trajectoryStart, wpts, goal, velocitiesRespectingObstacles);
506 }
507
508 // TODO(fabian.reister): resampling of trajectory
509
510 std::optional<core::GlobalTrajectory> resampledTrajectory;
511
512 {
513 ScopedStageTimer timer(timings.resample);
514 try
515 {
516 const float resampleDistance =
517 params_.resampleDistance > 0.F ? params_.resampleDistance
519 resampledTrajectory = trajectory.resample(resampleDistance);
520 ARMARX_DEBUG << "Terminal velocity: " << resampledTrajectory->points().back().velocity;
521 }
522 catch (...)
523 {
524 ARMARX_INFO << "Caught exception during resampling: " << GetHandledExceptionString();
525 resampledTrajectory = trajectory;
526 }
527
528 ARMARX_VERBOSE << "Resampled trajectory contains " << resampledTrajectory->points().size()
529 << " points";
530
531 resampledTrajectory->setMaxVelocity(generalConfig_.maxVel.linear);
532 ARMARX_DEBUG << "Terminal velocity: " << resampledTrajectory->points().back().velocity;
533 }
534
535 if (resampledTrajectory->points().size() == 2)
536 {
537 ARMARX_VERBOSE << "Only start and goal provided. Not optimizing orientation";
539 ARMARX_INFO << formatTimings(timings);
540 return GlobalPlannerResult{.trajectory = resampledTrajectory.value(),
541 .helperTrajectory = std::nullopt,
542 .timings = withGridSearch(timings, planner),
543 .gridPath = plan3d};
544 }
545
546 bool positionSmoothingApplied = false;
547 GlobalPlannerResult::SmoothingOutcome smoothingOutcome{};
548
549 // Smooth the resampled trajectory using the 2-D distance-to-obstacle costmap.
550 // Theta is not optimized here; orientations are computed afterwards by the
551 // OrientationOptimizer.
552 if (params_.enablePositionSmoothing)
553 {
554 ScopedStageTimer timer(timings.smoothPositions);
556 const auto smoothingParams =
559 resampledTrajectory.value(), costmap_.value(), smoothingParams);
560 const auto smoothingResult = smoother.optimize();
561
562 // Measured before the smoother runs, on exactly what it was given.
563 float inputSharpestTurn = 0.F;
564 {
565 const auto& p = resampledTrajectory->points();
566 for (std::size_t i = 1; i + 1 < p.size(); i++)
567 {
568 const Eigen::Vector2f in =
569 (p[i].waypoint.pose.translation() - p[i - 1].waypoint.pose.translation())
570 .head<2>();
571 const Eigen::Vector2f out =
572 (p[i + 1].waypoint.pose.translation() - p[i].waypoint.pose.translation())
573 .head<2>();
574
575 if (in.norm() < 1e-3F or out.norm() < 1e-3F)
576 {
577 continue;
578 }
579
580 const float cosine =
581 std::clamp(in.normalized().dot(out.normalized()), -1.F, 1.F);
582 inputSharpestTurn = std::max(
583 inputSharpestTurn, std::acos(cosine) * 180.F / static_cast<float>(M_PI));
584 }
585 }
586
587 // Where a rejected path actually failed. Without this a rejection rate cannot
588 // separate a path the smoother got wrong from a request it could not satisfy,
589 // because the endpoints it was required to keep were themselves too tight.
590 std::size_t collisions = 0;
591 std::size_t endpointCollisions = 0;
592 float worstCollisionClearance = 0.F;
593
594 if (smoothingResult.trajectory.has_value() and not smoothingResult.isCollisionFree)
595 {
597 costmap_.value(), smoothingParams.min_clearance);
598
599 const auto found = checker.violations(smoothingResult.trajectory.value());
600 const std::size_t last = smoothingResult.trajectory->points().size() - 1;
601
602 collisions = found.size();
603 worstCollisionClearance = std::numeric_limits<float>::max();
604
605 for (const auto& violation : found)
606 {
607 worstCollisionClearance =
608 std::min(worstCollisionClearance, violation.clearance);
609
610 // The first and last waypoints, and the segments adjoining them: the
611 // optimizer could not have moved either end out of the way.
612 const bool atStart = violation.index == 0;
613 const bool atGoal =
614 violation.index == last or (violation.onSegment and violation.index + 1 == last);
615
616 if (atStart or atGoal)
617 {
618 endpointCollisions++;
619 }
620 }
621
622 if (found.empty())
623 {
624 worstCollisionClearance = 0.F;
625 }
626 }
627
628 smoothingOutcome = {.ran = true,
629 .collisionFree = smoothingResult.isCollisionFree,
630 .geometryValid = smoothingResult.isGeometryValid,
631 .repairedWaypoints = smoothingResult.repairedWaypoints.size(),
632 .inputSharpestTurnDeg = inputSharpestTurn,
633 .collisions = collisions,
634 .endpointCollisions = endpointCollisions,
635 .worstCollisionClearance = worstCollisionClearance,
636 .inputMinClearance = smoothingResult.inputMinClearance,
637 .outputMinClearance = smoothingResult.outputMinClearance,
638 .judgedAgainstInput = smoothingResult.judgedAgainstInput};
639
640 if (smoothingResult.trajectory && smoothingResult.isCollisionFree &&
641 smoothingResult.isGeometryValid)
642 {
643 resampledTrajectory = smoothingResult.trajectory.value();
644 positionSmoothingApplied = true;
645
646 if (smoothingResult.repairedWaypoints.empty())
647 {
648 ARMARX_INFO << "SPFA position smoothing succeeded.";
649 }
650 else
651 {
652 ARMARX_INFO << "SPFA position smoothing succeeded after removing "
653 << smoothingResult.repairedWaypoints.size()
654 << " folded waypoint(s).";
655 }
656 }
657 else
658 {
659 // Says which check failed: a collision-free path can still be folded, and the two
660 // need different fixes.
661 ARMARX_WARNING << "[nav-guard] smoothing-rejected: "
662 << (smoothingResult.isCollisionFree
663 ? "the smoothed path is not geometrically valid (it "
664 "reverses direction)"
665 : "the smoothed path is not collision-free")
666 << "; using original resampled SPFA path.";
667
668 // The fallback is what the robot then drives, and until now it was handed back
669 // with no geometry check at all -- the smoothed path was held to a standard the
670 // path that actually replaced it was not. A fold here is as harmful as one
671 // there: the reparametrization fits a spline through the polyline, and its
672 // tangent collapses at a cusp, which drives the commanded speed to near zero at
673 // that point.
674 //
675 // Only waypoints the planner produced are candidates for removal; the check
676 // never reports the start or the goal.
677 std::vector<std::size_t> removed;
678 const std::vector<std::size_t> violations =
680 resampledTrajectory.value());
681
682 if (not violations.empty())
683 {
684 const core::GlobalTrajectory repaired =
685 algorithms::spfa::smoothing::repairGeometry(resampledTrajectory.value(),
686 removed);
687
688 // Removing a waypoint joins its neighbours directly, and that new segment
689 // can cut a corner the original went around -- so the repair is only worth
690 // taking if it is collision-free and actually monotone.
692 costmap_.value(), smoothingParams.min_clearance);
693
694 const bool usable =
695 checker.check(repaired) and
697
698 if (usable)
699 {
700 ARMARX_WARNING << "[nav-guard] fallback-repaired: the unsmoothed path "
701 "reverses direction at "
702 << violations.size() << " waypoint(s); removed "
703 << removed.size() << " of them, "
704 << resampledTrajectory->points().size() << " -> "
705 << repaired.points().size()
706 << " points, still collision-free.";
707
708 resampledTrajectory = repaired;
709 }
710 else
711 {
712 // Nothing better is available -- the smoothed path was already refused.
713 // Say so loudly rather than driving a folded path silently.
714 ARMARX_ERROR << "[nav-guard] fallback-folded: the unsmoothed path "
715 "reverses direction at "
716 << violations.size()
717 << " waypoint(s) and removing them would "
718 << (checker.check(repaired) ? "leave it folded"
719 : "cut a corner into an obstacle")
720 << ". Driving it as it stands; expect the trajectory "
721 "reparametrization to stall near waypoint "
722 << violations.front() << ".";
723 }
724 }
725 }
726 }
727 else
728 {
729 ARMARX_INFO << "SPFA position smoothing is disabled.";
730 }
731
732 // Recompute velocities based on the (possibly smoothed) positions so that obstacle
733 // proximity is reflected correctly after the path has been deformed.
734 {
735 ScopedStageTimer timer(timings.recomputeVelocities);
737 core::Positions smoothedPositions;
738 smoothedPositions.reserve(resampledTrajectory->points().size());
739 for (const auto& point : resampledTrajectory->points())
740 {
741 smoothedPositions.emplace_back(point.waypoint.pose.translation());
742 }
743
744 const std::vector<float> recomputedVelocities = velocityLimit().at(smoothedPositions);
745
746 ARMARX_CHECK(recomputedVelocities.size() == resampledTrajectory->points().size());
747
748 auto& mutablePoints = resampledTrajectory->mutablePoints();
749 for (size_t i = 0; i < mutablePoints.size(); ++i)
750 {
751 mutablePoints[i].velocity = recomputedVelocities[i];
752 }
753 }
754
755 // Compute recovery distance and find sub-trajectory start index
756 size_t subTrajectoryStartIndex = 0;
757 std::optional<core::GlobalTrajectory> trajectoryToOptimize;
758 float recoveryDistance = 0.0f;
759 {
760 ScopedStageTimer timer(timings.recoveryHandling);
761 const auto computeRecoveryDistance = [&]() -> float
762 {
763 if (!recovery.has_value())
764 return 0.0f;
765 return (recovery->recoveryPosition -
766 recovery->originalStartPose.translation().head<2>())
767 .norm();
768 };
769 recoveryDistance = computeRecoveryDistance();
770
771 // Find first point outside recovery distance (for sub-trajectory extraction)
772 if (recoveryDistance > 0)
773 {
774 auto& pts = resampledTrajectory->mutablePoints();
775 const auto startOrientation = pts.front().waypoint.pose.linear();
776
777 // Fix orientations for all points within recovery distance (collision segment)
778 for (size_t i = 0; i < pts.size(); i++)
779 {
780 const float dist =
781 (pts[i].waypoint.pose.translation() - pts.front().waypoint.pose.translation())
782 .norm();
783 if (dist <= recoveryDistance)
784 {
785 pts[i].waypoint.pose.linear() = startOrientation;
786 }
787 else
788 {
789 // Found first point outside collision - fix it for smooth transition
790 subTrajectoryStartIndex = i;
791 if (subTrajectoryStartIndex < pts.size())
792 {
793 pts[subTrajectoryStartIndex].waypoint.pose.linear() = startOrientation;
794 }
795 break;
796 }
797 }
798 ARMARX_INFO << "Recovery distance: " << recoveryDistance << " mm, fixed "
799 << subTrajectoryStartIndex
800 << " collision segment points, sub-trajectory starts at index "
801 << subTrajectoryStartIndex;
802 }
803
804 // Extract sub-trajectory for optimization (full trajectory if no recovery)
805 if (subTrajectoryStartIndex > 0)
806 {
807 trajectoryToOptimize = resampledTrajectory->getSubTrajectory(
808 subTrajectoryStartIndex, resampledTrajectory->points().size());
809 }
810 else
811 {
812 trajectoryToOptimize = resampledTrajectory;
813 }
814 }
815
817 {
818 ScopedStageTimer timer(timings.loadOrientationConfig);
819 const armarx::PackagePath pp("armarx_navigation",
820 "config/global_planning/OrientationOptimizer.json");
821 const std::string filename = pp.toSystemPath();
822
823 ARMARX_CHECK(std::filesystem::exists(filename))
824 << "OrientationOptimizer config file does not exist: " << filename;
825
826 ARMARX_INFO << "Loading config from file `" << filename << "`.";
827 std::ifstream ifs{filename};
828
829 nlohmann::json jsonConfig;
830 ifs >> jsonConfig;
831
832 ARMARX_VERBOSE << "Initializing config";
833
834
836
837 armarx::navigation::global_planning::arondto::OrientationOptimizerParams dto;
838
839 ARMARX_VERBOSE << "reading file.";
840 dto.read(reader, jsonConfig);
841
842 aron_conv::fromAron(dto, params_.optimizerParams);
843 }
844
845 OrientationOptimizer optimizer(trajectoryToOptimize.value(), params_.optimizerParams);
846 auto result = [&]() {
847 ScopedStageTimer timer(timings.optimizeOrientation);
848 return optimizer.optimize();
849 }();
850
851 if (not result)
852 {
853 ARMARX_ERROR << "Optimizer failure";
854 return std::nullopt;
855 }
856
857 // Combine collision segment (fixed) with optimized sub-trajectory
858 core::GlobalTrajectory finalTrajectory = result.trajectory.value();
859
860 {
861 ScopedStageTimer timer(timings.stitchTrajectory);
862 if (recoveryDistance > 0 && subTrajectoryStartIndex > 0)
863 {
864 // Stitch: fixed collision segment + optimized post-collision segment
865 auto& resampledPts = resampledTrajectory->mutablePoints();
866 const auto& optimizedPts = result.trajectory->points();
867
868 // Verify sizes match
869 if (subTrajectoryStartIndex + optimizedPts.size() == resampledPts.size())
870 {
871 // Copy optimized orientations and velocities to the resampled trajectory
872 for (size_t i = 0; i < optimizedPts.size(); i++)
873 {
874 resampledPts[subTrajectoryStartIndex + i].waypoint.pose.linear() =
875 optimizedPts[i].waypoint.pose.linear();
876 resampledPts[subTrajectoryStartIndex + i].velocity = optimizedPts[i].velocity;
877 }
878
879 // Use the resampled trajectory (with fixed collision segment + optimized post-collision)
880 finalTrajectory = resampledTrajectory.value();
881
882 ARMARX_INFO << "Combined fixed collision segment (" << subTrajectoryStartIndex
883 << " points) with optimized trajectory (" << optimizedPts.size()
884 << " points)";
885 }
886 else
887 {
888 ARMARX_WARNING << "Trajectory size mismatch in collision recovery stitching. "
889 << "Using optimized trajectory only.";
890 }
891 }
892 }
893
894 // TODO circular path smoothing should be done now
895
896 // algorithm::CircularPathSmoothing smoothing;
897 // auto smoothTrajectory = smoothing.smooth(result.trajectory.value());
898 // smoothTrajectory.setMaxVelocity(params.linearVelocity);
899
900 // Safety net: the orientation optimization and the recovery stitching above may change
901 // positions or velocities, so re-apply the same limit that produced them. This only ever
902 // reduces a velocity.
903 if (params_.enableFinalVelocityClamp)
904 {
905 ScopedStageTimer timer(timings.finalVelocityClamp);
906 const auto limit = velocityLimit();
907
908 for (auto& point : finalTrajectory.mutablePoints())
909 {
910 const float permissible =
911 limit.at(Eigen::Vector2f{point.waypoint.pose.translation().head<2>()});
912
913 point.velocity = std::min(permissible, point.velocity);
914 }
915 }
916 else
917 {
918 ARMARX_INFO << "Final obstacle-aware velocity clamp is disabled.";
919 }
920
921
923 ARMARX_DEBUG << "Terminal velocity: " << finalTrajectory.points().back().velocity;
924 ARMARX_INFO << formatTimings(timings);
925 return GlobalPlannerResult{.trajectory = finalTrajectory,
926 .helperTrajectory = std::nullopt,
927 .timings = withGridSearch(timings, planner),
928 .gridPath = plan3d,
929 .positionSmoothingApplied = positionSmoothingApplied,
930 .smoothingOutcome = smoothingOutcome};
931 }
932
933 std::vector<Eigen::Vector2f>
934 SPFAImpl::postProcessPath(const std::vector<Eigen::Vector2f>& path)
935 {
936 /// chain approximation
938 path, algorithm::ChainApproximation::Params{.distanceTh = 200.F});
939 approx.approximate();
940 const auto p = approx.approximatedChain();
941
942 // visualizePath(p, "approximated", simox::Color::green());
943
944 // algo::CircularPathSmoothing smoothing;
945 // const auto points = smoothing.smooth(p);
946
947 return p;
948 }
949
950} // namespace armarx::navigation::global_planning
#define M_PI
Definition MathTools.h:17
static std::filesystem::path toSystemPath(const data::PackagePath &pp)
The maximum permissible linear velocity as a function of the distance to the closest obstacle.
float at(const Eigen::Vector2f &position) const
The limit at a position in the costmap's global frame.
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
std::vector< Violation > violations(const core::GlobalTrajectory &trajectory) const
Every place the trajectory falls below the clearance.
bool check(const core::GlobalTrajectory &trajectory, bool logDetails=false) 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:123
void updateCostmap(const std::optional< navigation::algorithms::Costmap > &costmap)
Definition SPFA.cpp:228
std::optional< GlobalPlannerResult > plan(const core::Pose &start, const core::Pose &goal)
Definition SPFA.cpp:255
SPFAImpl(const Params &params, const core::GeneralConfig &generalParams, const std::optional< navigation::algorithms::Costmap > &costmap)
Definition SPFA.cpp:220
PlanningResult executePlannerFromPosition(const Eigen::Vector2f &startPosition)
Definition SPFA.cpp:384
PlanningResult executePlanner(const core::Pose &start)
Definition SPFA.cpp:340
std::vector< Eigen::Vector2f > postProcessPath(const std::vector< Eigen::Vector2f > &path)
Definition SPFA.cpp:934
std::optional< GlobalPlannerResult > calculatePath(const PlanningResult &planner, const core::Pose &goal, const std::optional< RecoveryInfo > &recovery=std::nullopt)
Definition SPFA.cpp:392
algorithms::ObstacleAwareVelocityLimit velocityLimit() const
The obstacle-aware velocity limit this planner applies, for the current costmap.
Definition SPFA.cpp:241
::armarx::navigation::global_planning::PlanningResult PlanningResult
Definition SPFA.h:162
std::optional< GlobalPlannerResult > calculatePath(const PlanningResult &planner, const core::Pose &goal)
Definition SPFA.cpp:213
std::optional< GlobalPlannerResult > plan(const core::Pose &goal) override
Definition SPFA.cpp:190
PlanningResult executePlanner(const core::Pose &start)
Definition SPFA.cpp:207
SPFA(const Params &params, const core::GeneralConfig &generalParams, const core::Scene &ctx)
Definition SPFA.cpp:178
#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: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_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
std::shared_ptr< Dict > DictPtr
Definition Dict.h:42
SmoothingParams loadSmoothingParams(const std::string &filePath)
Definition io.cpp:29
std::vector< std::size_t > findGeometryViolations(const core::GlobalTrajectory &trajectory, const GeometryLimits &limits)
Indices of waypoints that make the path double back on itself.
core::GlobalTrajectory repairGeometry(const core::GlobalTrajectory &trajectory, std::vector< std::size_t > &removed, const GeometryLimits &limits)
Drop the offending waypoints, repeating until the path is monotone.
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
Why the position smoothing stage ended the way it did.
float gridSearchDuration
Duration [s] of the SPFA grid search itself, which runs before calculatePath.
Definition SPFA.h:101
algorithms::spfa::ShortestPathFasterAlgorithm algorithm
Definition SPFA.h:97
std::optional< algorithms::spfa::ShortestPathFasterAlgorithm::Result > plan
Definition SPFA.h:98
Information about collision recovery.
Definition SPFA.h:111
aron::data::DictPtr toAron() const override
Definition SPFA.cpp:151
static SPFAParams FromAron(const aron::data::DictPtr &dict)
Definition SPFA.cpp:161
Algorithms algorithm() const override
Definition SPFA.cpp:145
static constexpr float defaultResampleDistance
Used when resampleDistance is negative.
Definition SPFA.h:65
#define ARMARX_TRACE
Definition trace.h:75