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