25#include <Eigen/Geometry>
47 core::GlobalTrajectory gtraj;
48 for (
const auto& p : traj)
50 core::GlobalTrajectoryPoint gp;
52 Eigen::Translation3f{Eigen::Vector3f(
static_cast<float>(p.x),
53 static_cast<float>(p.y),
55 Eigen::AngleAxisf{
static_cast<float>(p.theta), Eigen::Vector3f::UnitZ()};
56 gp.velocity =
static_cast<float>(p.v);
57 gtraj.mutablePoints().push_back(gp);
64 std::vector<CenterPoint> traj;
65 for (
const auto& gp : gtraj.points())
68 p.
x = gp.waypoint.pose.translation().x();
69 p.y = gp.waypoint.pose.translation().y();
70 const auto& R = gp.waypoint.pose.rotation();
71 p.theta = std::atan2(R(1, 0), R(0, 0));
81 wrapper(&x, &y, &out);
85 bool isInsideBounds(
double x,
double y,
const algorithms::Costmap& costmap)
87 const auto local = costmap.origin().inverse() * Eigen::Vector2f{
static_cast<float>(x),
88 static_cast<float>(y)};
89 const auto& bounds = costmap.getLocalSceneBounds();
90 return local.x() >= bounds.min.x() && local.x() <= bounds.max.x() &&
91 local.y() >= bounds.min.y() && local.y() <= bounds.max.y();
94 void pushWaypointsToSafeZone(std::vector<CenterPoint>& traj,
96 const algorithms::Costmap& costmap,
97 const std::vector<CenterPoint>& originalTargets,
103 const std::vector<std::pair<double, double>> neighbours = {
109 {stepSize, stepSize},
110 {stepSize, -stepSize},
111 {-stepSize, stepSize},
112 {-stepSize, -stepSize},
115 for (
int idx = 1; idx + 1 <
static_cast<int>(traj.size()); ++idx)
118 const double origX = originalTargets[idx].x;
119 const double origY = originalTargets[idx].y;
121 auto withinDeviation = [&](
double x,
double y)
123 return std::hypot(x - origX, y - origY) <= maxDeviation + 1e-6;
126 double bestDist = queryDistance(wrapper, p.x, p.y);
128 if (bestDist >= clearance)
131 for (
int iter = 0; iter < maxIterations; ++iter)
135 bool improved =
false;
137 for (
const auto& [dx, dy] : neighbours)
139 double candX = p.x + dx;
140 double candY = p.y + dy;
141 if (!isInsideBounds(candX, candY, costmap))
143 if (!withinDeviation(candX, candY))
145 double candDist = queryDistance(wrapper, candX, candY);
146 if (candDist > bestDist)
161 if (bestDist >= clearance)
167 if (queryDistance(wrapper, p.x, p.y) <= 0.0)
169 const auto vertex = costmap.findClosestCollisionFreeVertex(
170 Eigen::Vector2f{
static_cast<float>(origX),
static_cast<float>(origY)},
171 static_cast<float>(maxDeviation));
173 if (vertex && withinDeviation(vertex->position.x(), vertex->position.y()))
175 p.x = vertex->position.x();
176 p.y = vertex->position.y();
182 bool validateTrajectory(
const std::vector<CenterPoint>& traj,
const std::string& label)
185 for (
int i = 0; i < static_cast<int>(traj.size()); ++i)
191 <<
"] : x=" << traj[i].x <<
" y=" << traj[i].y
192 <<
" theta=" << traj[i].theta <<
" v=" << traj[i].v;
199 ceres::Solver::Options makeSolverOptions(
const Params& params,
int maxIterations)
201 ceres::Solver::Options options;
202 options.max_num_iterations = maxIterations;
203 options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
204 options.minimizer_progress_to_stdout =
false;
205 options.num_threads = params.num_threads;
206 options.function_tolerance = 1e-6;
207 options.parameter_tolerance = 1e-8;
208 options.trust_region_strategy_type = ceres::LEVENBERG_MARQUARDT;
209 options.initial_trust_region_radius = params.initial_trust_region_radius;
210 options.max_trust_region_radius = params.max_trust_region_radius;
211 options.min_trust_region_radius = params.min_trust_region_radius;
212 options.min_relative_decrease = 1e-4;
213 options.use_nonmonotonic_steps =
true;
220 double w_pose_smooth;
223 double w_safe_tracking;
233 wrapper(&x, &y, &d_raw);
237 double obs_dist = d_raw - clearance;
238 if (obs_dist > max_dist)
242 double penetration = -obs_dist;
243 double max_pen = 3.0 * max_dist;
244 if (penetration > max_pen)
245 penetration = max_pen;
246 double ratio = penetration / max_dist;
247 return std::exp(2.0 * ratio) - 1.0 + 0.1 * ratio;
249 return (max_dist - obs_dist) / max_dist;
252 void logResidualBreakdown(
const std::vector<CenterPoint>& traj,
254 const std::vector<CenterPoint>& originalTargets,
255 const std::vector<CenterPoint>& safeTargets,
256 const Params& params,
257 const std::string& label)
259 const int N =
static_cast<int>(traj.size());
263 double c_obs_wp = 0.0;
264 double c_obs_seg = 0.0;
265 double c_smooth = 0.0;
266 double c_tracking = 0.0;
267 double c_safe_tracking = 0.0;
268 double c_spacing = 0.0;
271 for (
int i = 0; i < N; ++i)
273 double r = evalObstacleCost(wrapper, traj[i].x, traj[i].y, params.obs_max_distance, params.clearance);
278 for (
int i = 0; i < N - 1; ++i)
280 for (
int s = 1;
s <= params.segment_obstacle_samples; ++
s)
282 double t =
static_cast<double>(
s) / (params.segment_obstacle_samples + 1);
283 double mx = (1.0 - t) * traj[i].x + t * traj[i + 1].x;
284 double my = (1.0 - t) * traj[i].y + t * traj[i + 1].y;
285 double r = evalObstacleCost(wrapper, mx, my, params.obs_max_distance, params.clearance);
291 for (
int i = 1; i <= N - 2; ++i)
293 double rx = traj[i - 1].x - 2.0 * traj[i].x + traj[i + 1].x;
294 double ry = traj[i - 1].y - 2.0 * traj[i].y + traj[i + 1].y;
295 c_smooth += rx * rx + ry * ry;
299 for (
int i = 1; i <= N - 2; ++i)
301 double dx = traj[i].x - safeTargets[i].x;
302 double dy = traj[i].y - safeTargets[i].y;
303 double d = std::hypot(dx, dy);
304 double excess = d - params.tracking_deadzone;
307 if (excess <= params.tracking_max_deviation - params.tracking_deadzone)
309 double r = params.w_tracking_in_bounds * excess;
314 double over = excess - (params.tracking_max_deviation - params.tracking_deadzone);
315 double r = params.w_tracking_in_bounds * (params.tracking_max_deviation - params.tracking_deadzone)
316 + params.w_tracking_hard * over;
323 for (
int i = 1; i <= N - 2; ++i)
325 double rx = traj[i].x - originalTargets[i].x;
326 double ry = traj[i].y - originalTargets[i].y;
328 double w = params.pass3_w_tracking;
329 c_safe_tracking += (w * rx) * (w * rx) + (w * ry) * (w * ry);
333 double d_avg = computeAverageSpacing(traj);
336 for (
int i = 0; i < N - 1; ++i)
338 double dx = traj[i + 1].x - traj[i].x;
339 double dy = traj[i + 1].y - traj[i].y;
340 double dist_sq = dx * dx + dy * dy;
341 double r = params.pass3_w_spacing * (dist_sq - d_avg * d_avg);
345 double total = c_obs_wp + c_obs_seg + c_smooth + c_tracking + c_safe_tracking + c_spacing;
347 ARMARX_INFO <<
"=== Residual breakdown [" << label <<
"] ===";
348 ARMARX_INFO <<
"Obstacle (waypoints): " << std::scientific << std::setprecision(3) << c_obs_wp;
349 ARMARX_INFO <<
"Obstacle (segments): " << std::scientific << std::setprecision(3) << c_obs_seg;
350 ARMARX_INFO <<
"Position smoothness: " << std::scientific << std::setprecision(3) << c_smooth;
351 ARMARX_INFO <<
"Bounded tracking: " << std::scientific << std::setprecision(3) << c_tracking;
352 ARMARX_INFO <<
"Soft tracking: " << std::scientific << std::setprecision(3) << c_safe_tracking;
353 ARMARX_INFO <<
"Spacing: " << std::scientific << std::setprecision(3) << c_spacing;
354 ARMARX_INFO <<
"TOTAL: " << std::scientific << std::setprecision(3) << total;
357 void runPositionPass(std::vector<CenterPoint>& traj,
359 const std::vector<CenterPoint>& originalTargets,
360 const std::vector<CenterPoint>& safeTargets,
361 const PassWeights& w,
362 const Params& params,
365 const int N =
static_cast<int>(traj.size());
368 ARMARX_INFO <<
"[SPFA smoothing] need at least 4 points for position pass";
372 normalizeTrajectoryAngles(traj);
375 for (
int i = 1; i < N - 1; ++i)
377 double dx = traj[i].x - traj[i - 1].x;
378 double dy = traj[i].y - traj[i - 1].y;
379 if (std::hypot(dx, dy) < 1.0)
381 double ndx = (i + 1 < N) ? (traj[i + 1].x - traj[i].x) : 1.0;
382 double ndy = (i + 1 < N) ? (traj[i + 1].y - traj[i].y) : 0.0;
383 double nlen = std::hypot(ndx, ndy);
390 traj[i].x += 1.0 * ndx / nlen;
391 traj[i].y += 1.0 * ndy / nlen;
395 ceres::Problem problem;
396 for (
int i = 0; i < N; ++i)
397 problem.AddParameterBlock(&traj[i].x, 4);
399 problem.SetParameterBlockConstant(&traj.front().x);
400 problem.SetParameterBlockConstant(&traj.back().x);
403 for (
int i = 1; i < N - 1; ++i)
405 std::vector<int> constant_indices = {2, 3};
410 for (
int i = 1; i <= N - 2; ++i)
412 problem.AddResidualBlock(
413 new ceres::AutoDiffCostFunction<PositionSmoothResidual, 2, 4, 4, 4>(
422 double d_avg = computeAverageSpacing(traj);
425 for (
int i = 0; i < N - 1; ++i)
427 problem.AddResidualBlock(
428 new ceres::AutoDiffCostFunction<SpacingResidual, 1, 4, 4>(
436 for (
int i = 0; i < N; ++i)
438 problem.AddResidualBlock(
439 new ceres::AutoDiffCostFunction<ObstacleResidual2D, 1, 4>(
446 for (
int i = 0; i < N - 1; ++i)
448 for (
int s = 1;
s <= params.segment_obstacle_samples; ++
s)
450 double t =
static_cast<double>(
s) / (params.segment_obstacle_samples + 1);
451 problem.AddResidualBlock(
452 new ceres::AutoDiffCostFunction<SegmentObstacleResidual2D, 1, 4, 4>(
461 for (
int i = 1; i <= N - 2; ++i)
463 problem.AddResidualBlock(
464 new ceres::AutoDiffCostFunction<BoundedTrackingResidual, 1, 4>(
467 params.tracking_deadzone,
468 params.tracking_max_deviation,
469 params.w_tracking_in_bounds,
470 params.w_tracking_hard)),
476 if (w.w_tracking > 0.0)
478 for (
int i = 1; i <= N - 2; ++i)
480 problem.AddResidualBlock(
481 new ceres::AutoDiffCostFunction<SoftTrackingResidual, 2, 4>(
488 ceres::Solver::Options options = makeSolverOptions(params, maxIterations);
489 ceres::Solver::Summary summary;
490 ceres::Solve(options, &problem, &summary);
492 normalizeTrajectoryAngles(traj);
495 ARMARX_INFO <<
"[Position pass] Final cost: " << summary.final_cost
496 <<
" iterations: " << summary.num_successful_steps
497 <<
" termination: " << summary.termination_type;
500 bool repairWaypoints(std::vector<CenterPoint>& traj,
501 const algorithms::Costmap& costmap,
503 const std::vector<CenterPoint>& originalTargets,
504 const Params& params)
506 const int N =
static_cast<int>(traj.size());
508 for (
int i = 1; i < N - 1; ++i)
510 double d = queryDistance(wrapper, traj[i].x, traj[i].y);
511 if (d > params.clearance)
514 const auto vertex = costmap.findClosestCollisionFreeVertex(
515 Eigen::Vector2f{
static_cast<float>(originalTargets[i].x),
516 static_cast<float>(originalTargets[i].y)},
517 static_cast<float>(params.tracking_max_deviation));
522 const double dx = vertex->position.x() - originalTargets[i].x;
523 const double dy = vertex->position.y() - originalTargets[i].y;
524 if (std::hypot(dx, dy) > params.tracking_max_deviation + 1e-3)
527 traj[i].x = vertex->position.x();
528 traj[i].y = vertex->position.y();
534 double computeMaxDeviation(
const std::vector<CenterPoint>& traj,
535 const std::vector<CenterPoint>& originalTargets)
538 for (std::size_t i = 0; i < traj.size(); ++i)
540 double dx = traj[i].x - originalTargets[i].x;
541 double dy = traj[i].y - originalTargets[i].y;
542 maxDev = std::max(maxDev, std::hypot(dx, dy));
551 trajectory(trajectory),
561 std::vector<CenterPoint> traj = toCenterTrajectory(trajectory);
562 const std::vector<CenterPoint> originalTargets = traj;
564 if (!validateTrajectory(traj,
"pre-hill-climb"))
566 ARMARX_WARNING <<
"[SPFA smoothing] Invalid trajectory before pre-processing. Aborting.";
567 return {std::nullopt, std::nullopt,
false,
false, {}};
570 pushWaypointsToSafeZone(traj,
575 params.hill_climb_step_size,
576 params.hill_climb_max_iterations,
577 params.tracking_max_deviation);
579 const std::vector<CenterPoint> safeTargets = traj;
581 if (!validateTrajectory(traj,
"post-hill-climb"))
583 ARMARX_WARNING <<
"[SPFA smoothing] Invalid trajectory after pre-processing. Aborting.";
584 return {std::nullopt, std::nullopt,
false,
false, {}};
587 logResidualBreakdown(traj, wrapper, originalTargets, safeTargets, params,
"after pre-processing");
590 const std::array<PassWeights, 3> passes = {
591 PassWeights{params.pass1_w_obs,
592 params.pass1_w_pose_smooth,
593 params.pass1_w_spacing,
594 params.pass1_w_tracking,
595 params.pass1_w_safe_tracking},
596 PassWeights{params.pass2_w_obs,
597 params.pass2_w_pose_smooth,
598 params.pass2_w_spacing,
599 params.pass2_w_tracking,
600 params.pass2_w_safe_tracking},
601 PassWeights{params.pass3_w_obs,
602 params.pass3_w_pose_smooth,
603 params.pass3_w_spacing,
604 params.pass3_w_tracking,
605 params.pass3_w_safe_tracking}};
607 for (
size_t pass = 0; pass < passes.size(); ++pass)
609 ARMARX_INFO <<
"[SPFA smoothing] Starting position pass " << (pass + 1);
610 runPositionPass(traj, wrapper, originalTargets, safeTargets, passes[pass], params, params.max_iterations);
611 logResidualBreakdown(traj, wrapper, originalTargets, safeTargets, params,
612 "after position pass " + std::to_string(pass + 1));
614 if (!validateTrajectory(traj,
"post-position-pass-" + std::to_string(pass + 1)))
616 ARMARX_WARNING <<
"[SPFA smoothing] NaN/Inf after position pass " << (pass + 1)
617 <<
". Reverting to pre-processed safe path.";
623 normalizeTrajectoryAngles(traj);
628 bool collisionFree = checker.
check(smoothedTrajectory);
632 ARMARX_WARNING <<
"[SPFA smoothing] Initial smoothing has collisions. Running repair pass.";
634 if (!repairWaypoints(traj, costmap, wrapper, originalTargets, params))
636 ARMARX_ERROR <<
"[SPFA smoothing] Repair failed: could not project colliding waypoints "
637 "to collision-free positions within max deviation.";
641 PassWeights repairWeights{params.repair_w_obs,
642 params.repair_w_pose_smooth,
643 params.repair_w_spacing,
644 params.repair_w_tracking,
645 params.repair_w_safe_tracking};
646 runPositionPass(traj, wrapper, originalTargets, safeTargets, repairWeights, params, params.repair_max_iterations);
647 logResidualBreakdown(traj, wrapper, originalTargets, safeTargets, params,
"after repair pass");
649 smoothedTrajectory = toGlobalTrajectory(traj);
650 collisionFree = checker.
check(smoothedTrajectory);
654 const double maxDeviation = computeMaxDeviation(traj, originalTargets);
655 const bool deviationOk = maxDeviation <= params.tracking_max_deviation + 1e-3;
657 if (collisionFree && deviationOk)
659 ARMARX_INFO <<
"[SPFA smoothing] Smoothed trajectory is collision-free. Max deviation: "
660 << maxDeviation <<
" mm";
665 ARMARX_WARNING <<
"[SPFA smoothing] Smoothed trajectory still has collisions after repair.";
667 ARMARX_WARNING <<
"[SPFA smoothing] Max deviation too large: " << maxDeviation
668 <<
" mm (limit: " << params.tracking_max_deviation <<
" mm).";
671 for (
int i = 0; i < static_cast<int>(traj.size()); ++i)
673 ARMARX_DEBUG << std::fixed << std::setprecision(3) <<
"i=" << i <<
" : x=" << traj[i].x
674 <<
" y=" << traj[i].y <<
" theta=" << traj[i].theta <<
" v=" << traj[i].v;
682 std::vector<std::size_t> repairedWaypoints;
683 bool geometryValid =
true;
687 if (not violations.empty())
689 ARMARX_WARNING <<
"[nav-guard] path-fold-detected: the smoothed path reverses "
691 << violations.size() <<
" waypoint(s), first at index "
692 << violations.front()
693 <<
". Repairing by removing them; a fold collapses the spline tangent "
694 "downstream and stalls the reparametrization there.";
701 const bool repairedIsCollisionFree = checker.
check(candidate);
704 if (repairedIsCollisionFree and repairedIsMonotone)
707 << repairedWaypoints.size() <<
" waypoint(s), "
708 << smoothedTrajectory.
points().size() <<
" -> "
709 << candidate.
points().size()
710 <<
" points, still collision-free.";
712 smoothedTrajectory = candidate;
716 ARMARX_WARNING <<
"[nav-guard] path-fold-unrepairable: removing the folded "
718 << (repairedIsCollisionFree ?
"leave the path folded"
719 :
"cut a corner into an obstacle")
720 <<
". Rejecting the smoothed path; the caller falls back to the "
723 repairedWaypoints.clear();
724 geometryValid =
false;
729 const bool success = collisionFree && deviationOk;
730 return {smoothedTrajectory, preprocessedTrajectory,
success, geometryValid,
Differentiable wrapper around the standard 2-D distance-to-obstacle costmap.
OptimizationResult optimize()
SPFASmoothing(const core::GlobalTrajectory &trajectory, const algorithms::Costmap &costmap, const Params ¶ms)
io::SmoothingParams Params
bool check(const core::GlobalTrajectory &trajectory, bool logDetails=false) const
const std::vector< GlobalTrajectoryPoint > & points() const
#define ARMARX_INFO
The normal logging level.
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
double s(double t, double s0, double v0, double a0, double j)
std::vector< CenterPoint > toCenterTrajectory(const armarx::navigation::core::GlobalTrajectory >raj)
armarx::navigation::core::GlobalTrajectory toGlobalTrajectory(const std::vector< CenterPoint > &traj)
This file is part of ArmarX.
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::multimap< std::string, std::string > Params
bool isfinite(const std::vector< T, Ts... > &v)
ceres::SubsetParameterization SubsetConstraint
This file is part of ArmarX.
#define SET_CONSTRAINT(problem, param, constraint)
Bounded tracking residual.