AStarWithOrientation.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <chrono>
5#include <optional>
6#include <vector>
7
8#include <Eigen/Core>
9#include <Eigen/Geometry>
10
11#include <range/v3/algorithm/transform.hpp>
12#include <range/v3/range/conversion.hpp>
13#include <range/v3/view/enumerate.hpp>
14#include <range/v3/view/transform.hpp>
15
16#include <VirtualRobot/Nodes/RobotNode.h>
17#include <VirtualRobot/Robot.h> // IWYU pragma: keep
18#include <VirtualRobot/XML/RobotIO.h>
19
25#include <ArmarXCore/interface/serialization/Eigen/Eigen_fdi.h>
26
28
38#include <armarx/navigation/core/aron/Trajectory.aron.generated.h>
42#include <armarx/navigation/global_planning/aron/AStarWithOrientationParams.aron.generated.h>
45
47{
48 // AStarWithOrientationParams
49
55
58 {
59 arondto::AStarWithOrientationParams dto;
60
62 aron_conv::toAron(dto, bo);
63
64 return dto.toAron();
65 }
66
69 {
71
72 arondto::AStarWithOrientationParams dto;
73 dto.fromAron(dict);
74
76 aron_conv::fromAron(dto, bo);
77
78 return bo;
79 }
80
81 // AStarWithOrientation
82
83 // TODO: How can we work with arbitrary robot shapes?
84 // Maybe take a 2D-shape as input and create a 5 degree sweep shape out of it
85 //
86 // How to work with orientation?
87 // - One option would be to keep the robot center at the same location
88 // This way, we could make use of the precomputed costmap and check the cart borders for collisions.
89 //
90
93 const core::Scene& ctx) :
95 impl(params,
97 ctx.staticScene.has_value() && ctx.staticScene->orientationAwareCostmap.has_value()
98 ? std::make_optional(ctx.staticScene->orientationAwareCostmap.value())
99 : std::nullopt,
100 ctx.robot)
101 {
102 }
103
104 std::optional<GlobalPlannerResult>
106 {
107 const core::Pose start(scene.robot->getGlobalPose());
108 return plan(start, goal);
109 }
110
111 std::optional<GlobalPlannerResult>
112 AStarWithOrientation::plan(const core::Pose& startRobotRoot, const core::Pose& goalRobotRoot)
113 {
114 if (scene.staticScene.has_value() && scene.staticScene->orientationAwareCostmap.has_value())
115 {
116 impl.updateCostmap(scene.staticScene->orientationAwareCostmap.value());
117 }
118 return impl.plan(startRobotRoot, goalRobotRoot);
119 }
120
121 void
123 const std::string& vizLayerNamePrefix)
124 {
125 impl.visualizeDebugInfo(vizClient, vizLayerNamePrefix);
126 }
127
128 // AStarWithOrientationImpl
129
130
132 const Params& params,
134 const std::optional<navigation::algorithms::orientation_aware::Costmap3D>& costmap,
137 {
138 }
139
140 void
142 const std::optional<navigation::algorithms::orientation_aware::Costmap3D>& costmap)
143 {
144 if (costmap.has_value())
145 {
146 this->costmap.emplace(costmap.value());
147 }
148 else
149 {
150 this->costmap.reset();
151 }
152 }
153
154 std::optional<GlobalPlannerResult>
156 const core::Pose& goalRobotRoot)
157 {
158 ARMARX_CHECK(costmap.has_value())
159 << "AStarWithOrientation cannot be executed because there is no 3D costmap available";
160 const auto aStarParams =
161 aStarParamsOverride.has_value()
162 ? aStarParamsOverride.value()
164 algorithms::orientation_aware::AStarPlanner planner{costmap.value(), aStarParams};
165
166 expandedNodes = 0;
167 aStarSeconds = 0.0;
168 smootherSeconds = 0.0;
169
170
171 const Eigen::Isometry3f root_T_used_root =
173
174 // start and goal in the costmap; consistent with the generation of the costmap
175 const core::Pose start = startRobotRoot * root_T_used_root;
176 const core::Pose goal = goalRobotRoot * root_T_used_root;
177
178 lastRecoveryOriginalStart.reset();
179 lastRecoveryPosition.reset();
180
181 // If the robot is standing in collision (or within the planner's clearance zone),
182 // plan from the closest position where it fits at its current orientation and
183 // prepend the actual start pose afterwards, mirroring the SPFA recovery.
184 core::Pose2D start2D = conv::to2D(start);
185 const float startOrientationDeg = costmap->rotationDegrees(start2D);
186 bool recovered = false;
187
188 if (not costmap->isFreeWithClearance(
189 Eigen::Vector2f{start2D.translation()}, startOrientationDeg, aStarParams.clearance))
190 {
191 ARMARX_WARNING << "Start position " << start2D.translation().transpose()
192 << " is in collision (within clearance=" << aStarParams.clearance
193 << " mm). Searching for recovery position within "
194 << generalConfig.inCollisionDistanceThresholdForRecovery << " mm...";
195
196 const auto recoveryVertex = costmap->findClosestCollisionFreeVertex(
197 start2D.translation(),
198 startOrientationDeg,
199 generalConfig.inCollisionDistanceThresholdForRecovery,
200 aStarParams.clearance);
201
202 if (not recoveryVertex.has_value())
203 {
204 ARMARX_ERROR << "No valid recovery position found within threshold of "
205 << generalConfig.inCollisionDistanceThresholdForRecovery << " mm";
206 return {};
207 }
208
209 ARMARX_WARNING << "Found recovery position at "
210 << recoveryVertex->position.transpose() << " (distance: "
211 << (recoveryVertex->position - start2D.translation()).norm()
212 << " mm). Prepending original start position to trajectory.";
213
214 lastRecoveryOriginalStart = start;
215 lastRecoveryPosition = recoveryVertex->position;
216
217 // keep the current orientation; only translate out of the collision
218 start2D.translation() = recoveryVertex->position;
219 recovered = true;
220 }
221
222 // The effective start of the planned trajectory: the recovery position (current
223 // orientation kept) if the robot is in collision, the actual start otherwise.
224 const core::Pose effectiveStart = conv::to3D(start2D);
225
226 std::vector<core::Pose2D> plan;
227 const auto aStarStart = std::chrono::steady_clock::now();
228 try
229 {
230 plan = planner.plan(start2D, conv::to2D(goal));
231 aStarSeconds =
232 std::chrono::duration<double>(std::chrono::steady_clock::now() - aStarStart)
233 .count();
234 expandedNodes = planner.lastExpandedNodes();
235 }
236 catch (...)
237 {
238 ARMARX_WARNING << "Could not execute orientation-aware A* from "
239 << start2D.translation().transpose() << " due to exception "
241 return {};
242 }
243
244 if (plan.empty())
245 {
246 ARMARX_WARNING << "Could not calculate a path with orientation_aware A*";
247 return {};
248 }
249
250 std::vector<core::GlobalTrajectoryPoint> trajectory;
251 trajectory.reserve(plan.size() + 2);
252 std::vector<core::GlobalTrajectoryPoint> trajBeforeTransform;
253 trajBeforeTransform.reserve(plan.size() + 2);
254
255 const Eigen::Isometry3f root_used_T_root = root_T_used_root.inverse();
256
257 trajectory.push_back({.waypoint = {effectiveStart * root_used_T_root}, .velocity = 300.F});
258 trajBeforeTransform.push_back({.waypoint = {effectiveStart}, .velocity = 300.F});
259
260 for (std::size_t i = 1; i < plan.size(); ++i)
261 {
262 const auto& pose2d = plan[i];
263 // need to transform points back to robot's root frame
265 .waypoint = {conv::to3D(pose2d) * root_used_T_root},
266 .velocity = 300.F,
267 });
268 trajBeforeTransform.push_back(core::GlobalTrajectoryPoint{
269 .waypoint = {conv::to3D(pose2d)},
270 .velocity = 300.F,
271 });
272 }
273
274 // Replace the grid-snapped goal with the exact requested goal pose so the
275 // smoothed trajectory terminates precisely at the target.
276 trajBeforeTransform.back().waypoint.pose = goal;
277 trajectory.back().waypoint.pose = goal * root_used_T_root;
278
280
281 // Resampling
282 //traj = traj.resample(30.F); // not working properly for orientation
283 //traj = traj.calcSubsampledTrajectory(3); // working, but not really needed (smoothing is good enough without
284
285 lastRawTrajectory = core::GlobalTrajectory{trajBeforeTransform};
286
287 const auto smoothingParams =
288 smoothingParamsOverride.has_value()
289 ? smoothingParamsOverride.value()
291
292 // `smoothingEnabled` is true unless an offline evaluation turned it off, so the deployed
293 // path is unchanged: smooth, then collision-check with fallback to the raw A* trajectory.
294 core::GlobalTrajectory traj_smoothed = lastRawTrajectory;
295 if (smoothingEnabled)
296 {
297 const auto smootherStart = std::chrono::steady_clock::now();
298
300 core::GlobalTrajectory{trajBeforeTransform},
301 costmap.value(),
302 smoothingParams);
303 const auto optimizationResult = smoother.optimize();
304
305 smootherSeconds =
306 std::chrono::duration<double>(std::chrono::steady_clock::now() - smootherStart)
307 .count();
308
309 traj_smoothed = optimizationResult.trajectory.value();
310 lastPreprocessedTrajectory = optimizationResult.preprocessedTrajectory.value();
311 }
312 else
313 {
314 lastPreprocessedTrajectory = lastRawTrajectory;
315 }
316 lastSmoothedTrajectory = traj_smoothed;
317
318 auto traj_smoothed_converted = traj_smoothed.mutablePoints() |
319 ranges::views::transform(
320 [root_used_T_root](const core::GlobalTrajectoryPoint& pt)
321 {
322 auto pt2 = pt;
323 pt2.waypoint.pose =
324 pt2.waypoint.pose * root_used_T_root;
325 return pt2;
326 }) |
327 ranges::to_vector;
328
329 // Collision check with fallback to original A* trajectory
331 auto checkResult = checker.check(traj_smoothed, /*logDetails=*/true);
332 lastPointsInCollision = checkResult.collisionPoints;
333
334 core::GlobalTrajectory traj_to_use;
335 core::GlobalTrajectory traj_converted;
336 if (checkResult.collisionCount > 2 && !smoothingParams.always_return_smoothed)
337 {
338 ARMARX_WARNING << "Smoothed trajectory has " << checkResult.collisionCount
339 << " collision points; falling back to original A* trajectory.";
340 traj_to_use = lastRawTrajectory;
341 traj_converted = lastRawTrajectory.mutablePoints() |
342 ranges::views::transform(
343 [root_used_T_root](const core::GlobalTrajectoryPoint& pt)
344 {
345 auto pt2 = pt;
346 pt2.waypoint.pose =
347 pt2.waypoint.pose * root_used_T_root;
348 return pt2;
349 }) |
350 ranges::to_vector;
351 }
352 else
353 {
354 if (!checkResult.isCollisionFree())
355 {
356 if (smoothingParams.always_return_smoothed)
357 {
358 ARMARX_WARNING << "Smoothed trajectory has " << checkResult.collisionCount
359 << " collision point(s); returning smoothed anyway because "
360 "alwaysReturnSmoothed is enabled.";
361 }
362 else
363 {
364 ARMARX_WARNING << "Smoothed trajectory has " << checkResult.collisionCount
365 << " minor collision(s); returning smoothed anyway.";
366 }
367 }
368 else
369 {
370 ARMARX_INFO << "Smoothed trajectory is collision-free.";
371 }
372 traj_to_use = traj_smoothed;
373 traj_converted = traj_smoothed_converted;
374 }
375
376 if (recovered)
377 {
378 // Prepend the original (in-collision) start pose so the first trajectory segment
379 // moves the robot out of the collision. Keep the escape segment slow while the
380 // robot is (potentially) in contact.
381 constexpr float maxRecoveryVelocity = 150.F; // [mm/s]
382
383 auto& trajectoryPoints = traj_converted.mutablePoints();
384 const float recoveryVelocity =
385 std::min(trajectoryPoints.front().velocity, maxRecoveryVelocity);
386 trajectoryPoints.front().velocity = recoveryVelocity;
387 trajectoryPoints.insert(trajectoryPoints.begin(),
388 core::GlobalTrajectoryPoint{.waypoint = {startRobotRoot},
389 .velocity = recoveryVelocity});
390
391 auto& helperPoints = traj_to_use.mutablePoints();
392 helperPoints.front().velocity = recoveryVelocity;
393 helperPoints.insert(helperPoints.begin(),
394 core::GlobalTrajectoryPoint{.waypoint = {start},
395 .velocity = recoveryVelocity});
396
397 // also show the escape segment in the debug visualization
398 auto& rawPoints = lastRawTrajectory.mutablePoints();
399 rawPoints.insert(rawPoints.begin(),
400 core::GlobalTrajectoryPoint{.waypoint = {start},
401 .velocity = recoveryVelocity});
402 }
403
404 return GlobalPlannerResult{.trajectory = traj_converted,
405 .helperTrajectory = traj_to_use};
406 }
407
408 void
410 const std::string& vizLayerNamePrefix)
411 {
412 // 1) Raw A* trajectory (yellow)
413 {
414 auto layer = vizClient.layer(vizLayerNamePrefix + "_raw");
415 layer.clear();
416 layer.add(viz::Path("path")
417 .points(lastRawTrajectory.positions())
418 .color(simox::Color::yellow()));
419 for (const auto& [idx, tp] : lastRawTrajectory.points() | ranges::views::enumerate)
420 {
421 const Eigen::Vector3f target =
422 100.0F * tp.waypoint.pose.linear() * Eigen::Vector3f::UnitY();
423 layer.add(viz::Arrow("theta_" + std::to_string(idx))
424 .fromTo(tp.waypoint.pose.translation(),
425 tp.waypoint.pose.translation() + target)
426 .color(simox::Color::yellow()));
427 }
428 vizClient.commit(layer);
429 }
430
431 // 2) Pre-processed trajectory (green)
432 {
433 auto layer = vizClient.layer(vizLayerNamePrefix + "_preprocessed");
434 layer.clear();
435 layer.add(viz::Path("path")
436 .points(lastPreprocessedTrajectory.positions())
437 .color(simox::Color::green()));
438 for (const auto& [idx, tp] : lastPreprocessedTrajectory.points()
439 | ranges::views::enumerate)
440 {
441 const Eigen::Vector3f target =
442 100.0F * tp.waypoint.pose.linear() * Eigen::Vector3f::UnitY();
443 layer.add(viz::Arrow("theta_" + std::to_string(idx))
444 .fromTo(tp.waypoint.pose.translation(),
445 tp.waypoint.pose.translation() + target)
446 .color(simox::Color::green()));
447 }
448 vizClient.commit(layer);
449 }
450
451 // 3) Smoothed trajectory with clearance-based coloring
452 {
453 auto layer = vizClient.layer(vizLayerNamePrefix + "_smoothed");
454 layer.clear();
455
456 if (costmap.has_value())
457 {
459 costmap.value());
460 std::vector<simox::color::Color> colors;
461 colors.reserve(lastSmoothedTrajectory.points().size());
462 for (const auto& tp : lastSmoothedTrajectory.points())
463 {
464 double x = tp.waypoint.pose.translation().x();
465 double y = tp.waypoint.pose.translation().y();
466 double theta = std::atan2(tp.waypoint.pose.linear()(1, 0),
467 tp.waypoint.pose.linear()(0, 0));
468 double d_raw;
469 wrapper(&x, &y, &theta, &d_raw);
470 if (d_raw < 50.0)
471 colors.push_back(simox::Color::red());
472 else if (d_raw < 200.0)
473 colors.push_back(simox::Color::yellow());
474 else
475 colors.push_back(simox::Color::green());
476 }
477
478 // Draw path segments with per-point colors
479 const auto positions = lastSmoothedTrajectory.positions();
480 for (std::size_t i = 0; i + 1 < positions.size(); ++i)
481 {
482 std::vector<Eigen::Vector3f> seg = {positions[i], positions[i + 1]};
483 // Use the more conservative color of the two endpoints
484 auto color = (colors[i] == simox::Color::red() || colors[i + 1] == simox::Color::red())
485 ? simox::Color::red()
486 : (colors[i] == simox::Color::yellow()
487 || colors[i + 1] == simox::Color::yellow())
488 ? simox::Color::yellow()
489 : simox::Color::green();
490 layer.add(viz::Path("segment_" + std::to_string(i)).points(seg).color(color));
491 }
492
493 for (const auto& [idx, tp] : lastSmoothedTrajectory.points()
494 | ranges::views::enumerate)
495 {
496 const Eigen::Vector3f target =
497 100.0F * tp.waypoint.pose.linear() * Eigen::Vector3f::UnitY();
498 layer.add(viz::Arrow("theta_" + std::to_string(idx))
499 .fromTo(tp.waypoint.pose.translation(),
500 tp.waypoint.pose.translation() + target)
501 .color(colors[idx]));
502 }
503 }
504 else
505 {
506 layer.add(viz::Path("path")
507 .points(lastSmoothedTrajectory.positions())
508 .color(simox::Color::blue()));
509 }
510 vizClient.commit(layer);
511 }
512
513 // 4) Collision recovery: original (in-collision) start and escape target
514 {
515 auto layer = vizClient.layer(vizLayerNamePrefix + "_recovery");
516 layer.clear();
517 if (lastRecoveryOriginalStart.has_value() && lastRecoveryPosition.has_value())
518 {
519 const Eigen::Vector3f from = lastRecoveryOriginalStart->translation();
520 const Eigen::Vector3f to = conv::to3D(lastRecoveryPosition.value());
521 layer.add(viz::Ellipsoid("original_start")
522 .pose(Eigen::Isometry3f{Eigen::Translation3f{from}}.matrix())
523 .axisLengths(Eigen::Vector3f{30.F, 30.F, 30.F})
524 .color(simox::Color::red()));
525 layer.add(viz::Arrow("to_recovery_position")
526 .fromTo(from, to)
527 .color(simox::Color::red()));
528 }
529 vizClient.commit(layer);
530 }
531
532 // 5) Existing collision ellipsoids
533 {
534 auto layer = vizClient.layer(vizLayerNamePrefix + "_points_in_collision");
535 layer.clear();
536 for (std::size_t i = 0; i < lastPointsInCollision.size(); ++i)
537 {
538 const auto& p = lastPointsInCollision[i];
539 std::stringstream ss;
540 ss << "point_in_collision_" << i;
541 viz::Ellipsoid ellipsoid =
542 viz::Ellipsoid(ss.str())
543 .pose(
544 Eigen::Isometry3f{Eigen::Translation3f{p.waypoint.pose.translation()}}
545 .matrix())
546 .axisLengths(Eigen::Vector3f{3.0F, 3.0F, 3.0F})
547 .color(simox::Color::red());
548 layer.add(ellipsoid);
549 }
550 vizClient.commit(layer);
551 }
552 }
553
554} // namespace armarx::navigation::global_planning
The A* planner (3D version including orientation dimension)
std::vector< GlobalTrajectoryPoint > & mutablePoints()
std::optional< GlobalPlannerResult > plan(const core::Pose &start, const core::Pose &goal)
void updateCostmap(const std::optional< navigation::algorithms::orientation_aware::Costmap3D > &costmap)
std::optional< navigation::algorithms::orientation_aware::Costmap3D > costmap
AStarWithOrientationImpl(const Params &params, const core::GeneralConfig &generalConfig, const std::optional< navigation::algorithms::orientation_aware::Costmap3D > &costmap, VirtualRobot::RobotPtr robot)
void visualizeDebugInfo(viz::Client &vizClient, const std::string &vizLayerNamePrefix="global_planner_debug")
void visualizeDebugInfo(viz::Client &vizClient, const std::string &vizLayerNamePrefix="global_planner_debug") override
std::optional< GlobalPlannerResult > plan(const core::Pose &goal) override
AStarWithOrientation(const Params &params, const core::GeneralConfig &generalConfig, const core::Scene &ctx)
GlobalPlanner(const core::GeneralConfig &generalConfig, const core::Scene &scene)
virtual Layer layer(std::string const &name) const
Definition Client.cpp:80
CommitResult commit(StagedCommit const &commit)
Definition Client.cpp:89
DerivedT & pose(Eigen::Matrix4f const &pose)
Definition ElementOps.h:176
DerivedT & color(Color color)
Definition ElementOps.h:218
#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_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
std::shared_ptr< class Robot > RobotPtr
Definition Bus.h:19
std::shared_ptr< Dict > DictPtr
Definition Dict.h:42
AStarWithOrientationParams loadAStarWithOrientationParams(const std::string &filePath)
Definition io.cpp:13
SmoothingParams loadSmoothingParams(const std::string &filePath)
Definition io.cpp:13
Eigen::Isometry3f get_root_T_used_root(const armarx::navigation::algorithms::orientation_aware::Costmap3D &costmap, const VirtualRobot::RobotPtr &robot_in)
Definition util.h:31
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
Eigen::Isometry2f Pose2D
Definition basic_types.h:34
Eigen::Isometry3f Pose
Definition basic_types.h:31
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
This file offers overloads of toIce() and fromIce() functions for STL container types.
std::string GetHandledExceptionString()
static AStarWithOrientationParams FromAron(const aron::data::DictPtr &dict)
Ellipsoid & axisLengths(const Eigen::Vector3f &axisLengths)
Definition Elements.h:156