26#include <Eigen/Geometry>
48 core::GlobalTrajectory gtraj;
49 for (
const auto& p : traj)
51 core::GlobalTrajectoryPoint gp;
53 Eigen::Translation3f{Eigen::Vector3f(
static_cast<float>(p.x),
54 static_cast<float>(p.y),
56 Eigen::AngleAxisf{
static_cast<float>(p.theta), Eigen::Vector3f::UnitZ()};
57 gp.velocity =
static_cast<float>(p.v);
58 gtraj.mutablePoints().push_back(gp);
65 std::vector<CenterPoint> traj;
66 for (
const auto& gp : gtraj.points())
69 p.
x = gp.waypoint.pose.translation().x();
70 p.y = gp.waypoint.pose.translation().y();
71 const auto& R = gp.waypoint.pose.rotation();
72 p.theta = std::atan2(R(1, 0), R(0, 0));
82 wrapper(&x, &y, &out);
105 std::vector<std::size_t> projectTrappedWaypoints(std::vector<CenterPoint>& traj,
107 const algorithms::Costmap& costmap,
108 const Params& params)
110 std::vector<std::size_t> unresolved;
112 for (std::size_t idx = 1; idx + 1 < traj.size(); idx++)
116 if (queryDistance(wrapper, p.x, p.y) > 0.0)
125 const auto vertex = costmap.findClosestCollisionFreeVertex(
126 Eigen::Vector2f{
static_cast<float>(p.x),
static_cast<float>(p.y)},
127 static_cast<float>(params.tracking_max_deviation),
128 static_cast<float>(params.min_clearance));
130 if (not vertex.has_value() or
131 std::hypot(vertex->position.x() - p.x, vertex->position.y() - p.y) >
132 params.tracking_max_deviation + 1e-3)
134 unresolved.push_back(idx);
138 p.x = vertex->position.x();
139 p.y = vertex->position.y();
149 const double length = std::hypot(b.x -
a.x, b.y -
a.y);
151 static_cast<int>(std::ceil(length / std::max(params.segment_sample_spacing, 1.0))) - 1;
154 needed, params.segment_obstacle_samples, params.max_segment_obstacle_samples);
158 float minimumClearance(
const core::GlobalTrajectory& trajectory,
159 const algorithms::Costmap& costmap)
161 constexpr float sampleSpacing = 50.F;
163 float smallest = std::numeric_limits<float>::max();
165 const auto sample = [&](
const Eigen::Vector2f& position)
167 const auto value = costmap.value(position);
169 smallest = std::min(smallest,
value.has_value() ?
value.value() : 0.F);
172 const auto& points = trajectory.points();
174 for (std::size_t i = 0; i < points.size(); i++)
176 sample(points[i].waypoint.pose.translation().head<2>());
178 if (i + 1 >= points.size())
183 const Eigen::Vector2f
a = points[i].waypoint.pose.translation().head<2>();
184 const Eigen::Vector2f b = points[i + 1].waypoint.pose.translation().head<2>();
186 std::max(1,
static_cast<int>(std::ceil((b - a).
norm() / sampleSpacing)));
188 for (
int step = 1; step < steps; step++)
190 sample(a + (b - a) * (
static_cast<float>(step) /
static_cast<float>(steps)));
194 return (smallest == std::numeric_limits<float>::max()) ? 0.F : smallest;
197 bool validateTrajectory(
const std::vector<CenterPoint>& traj,
const std::string& label)
200 for (
int i = 0; i < static_cast<int>(traj.size()); ++i)
206 <<
"] : x=" << traj[i].x <<
" y=" << traj[i].y
207 <<
" theta=" << traj[i].theta <<
" v=" << traj[i].v;
214 ceres::Solver::Options makeSolverOptions(
const Params& params,
int maxIterations)
216 ceres::Solver::Options options;
217 options.max_num_iterations = maxIterations;
218 options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
219 options.minimizer_progress_to_stdout =
false;
220 options.num_threads = params.num_threads;
221 options.function_tolerance = 1e-6;
222 options.parameter_tolerance = 1e-8;
223 options.trust_region_strategy_type = ceres::LEVENBERG_MARQUARDT;
224 options.initial_trust_region_radius = params.initial_trust_region_radius;
225 options.max_trust_region_radius = params.max_trust_region_radius;
226 options.min_trust_region_radius = params.min_trust_region_radius;
227 options.min_relative_decrease = 1e-4;
228 options.use_nonmonotonic_steps =
true;
235 double w_pose_smooth;
238 double w_safe_tracking;
250 wrapper(&x, &y, &d_raw);
254 void logResidualBreakdown(
const std::vector<CenterPoint>& traj,
256 const std::vector<CenterPoint>& originalTargets,
257 const Params& params,
258 const std::string& label)
260 const int N =
static_cast<int>(traj.size());
264 double c_obs_wp = 0.0;
265 double c_obs_seg = 0.0;
266 double c_smooth = 0.0;
267 double c_tracking = 0.0;
268 double c_safe_tracking = 0.0;
269 double c_spacing = 0.0;
272 for (
int i = 0; i < N; ++i)
274 double r = evalObstacleCost(wrapper,
277 params.obs_max_distance,
283 for (
int i = 0; i < N - 1; ++i)
285 const int samples = segmentSampleCount(traj[i], traj[i + 1], params);
286 for (
int s = 1;
s <= samples; ++
s)
288 double t =
static_cast<double>(
s) / (samples + 1);
289 double mx = (1.0 - t) * traj[i].x + t * traj[i + 1].x;
290 double my = (1.0 - t) * traj[i].y + t * traj[i + 1].y;
291 double r = evalObstacleCost(wrapper,
294 params.obs_max_distance,
301 for (
int i = 1; i <= N - 2; ++i)
303 double rx = traj[i - 1].x - 2.0 * traj[i].x + traj[i + 1].x;
304 double ry = traj[i - 1].y - 2.0 * traj[i].y + traj[i + 1].y;
305 c_smooth += rx * rx + ry * ry;
309 for (
int i = 1; i <= N - 2; ++i)
311 double dx = traj[i].x - originalTargets[i].x;
312 double dy = traj[i].y - originalTargets[i].y;
313 double d = std::hypot(dx, dy);
314 double excess = d - params.tracking_deadzone;
317 if (excess <= params.tracking_max_deviation - params.tracking_deadzone)
319 double r = params.w_tracking_in_bounds * excess;
324 double over = excess - (params.tracking_max_deviation - params.tracking_deadzone);
325 double r = params.w_tracking_in_bounds * (params.tracking_max_deviation - params.tracking_deadzone)
326 + params.w_tracking_hard * over;
333 for (
int i = 1; i <= N - 2; ++i)
335 double rx = traj[i].x - originalTargets[i].x;
336 double ry = traj[i].y - originalTargets[i].y;
338 double w = params.pass3_w_tracking;
339 c_safe_tracking += (w * rx) * (w * rx) + (w * ry) * (w * ry);
343 double d_avg = computeAverageSpacing(traj);
346 for (
int i = 0; i < N - 1; ++i)
348 double dx = traj[i + 1].x - traj[i].x;
349 double dy = traj[i + 1].y - traj[i].y;
350 double dist_sq = dx * dx + dy * dy;
351 double r = params.pass3_w_spacing * (dist_sq - d_avg * d_avg);
355 double total = c_obs_wp + c_obs_seg + c_smooth + c_tracking + c_safe_tracking + c_spacing;
357 ARMARX_INFO <<
"=== Residual breakdown [" << label <<
"] ===";
358 ARMARX_INFO <<
"Obstacle (waypoints): " << std::scientific << std::setprecision(3) << c_obs_wp;
359 ARMARX_INFO <<
"Obstacle (segments): " << std::scientific << std::setprecision(3) << c_obs_seg;
360 ARMARX_INFO <<
"Position smoothness: " << std::scientific << std::setprecision(3) << c_smooth;
361 ARMARX_INFO <<
"Bounded tracking: " << std::scientific << std::setprecision(3) << c_tracking;
362 ARMARX_INFO <<
"Soft tracking: " << std::scientific << std::setprecision(3) << c_safe_tracking;
363 ARMARX_INFO <<
"Spacing: " << std::scientific << std::setprecision(3) << c_spacing;
364 ARMARX_INFO <<
"TOTAL: " << std::scientific << std::setprecision(3) << total;
367 void runPositionPass(std::vector<CenterPoint>& traj,
369 const std::vector<CenterPoint>& originalTargets,
370 const PassWeights& w,
371 const Params& params,
373 bool pinTightWaypoints =
false)
375 const int N =
static_cast<int>(traj.size());
378 ARMARX_INFO <<
"[SPFA smoothing] need at least 4 points for position pass";
382 normalizeTrajectoryAngles(traj);
385 for (
int i = 1; i < N - 1; ++i)
387 double dx = traj[i].x - traj[i - 1].x;
388 double dy = traj[i].y - traj[i - 1].y;
389 if (std::hypot(dx, dy) < 1.0)
391 double ndx = (i + 1 < N) ? (traj[i + 1].x - traj[i].x) : 1.0;
392 double ndy = (i + 1 < N) ? (traj[i + 1].y - traj[i].y) : 0.0;
393 double nlen = std::hypot(ndx, ndy);
400 traj[i].x += 1.0 * ndx / nlen;
401 traj[i].y += 1.0 * ndy / nlen;
405 ceres::Problem problem;
406 for (
int i = 0; i < N; ++i)
407 problem.AddParameterBlock(&traj[i].x, 4);
409 problem.SetParameterBlockConstant(&traj.front().x);
410 problem.SetParameterBlockConstant(&traj.back().x);
419 for (
int i = 1; i < N - 1; ++i)
422 (params.use_obstacle_freeze or pinTightWaypoints) and
423 queryDistance(wrapper, traj[i].x, traj[i].y) <= params.obstacle_freeze_threshold;
427 problem.SetParameterBlockConstant(&traj[i].x);
432 std::vector<int> constant_indices = {2, 3};
437 if (params.use_obstacle_freeze or pinTightWaypoints)
439 ARMARX_INFO <<
"[SPFA smoothing] Pinned " << frozen <<
" of " << (N - 2)
440 <<
" interior waypoints below "
441 << params.obstacle_freeze_threshold <<
" mm clearance.";
445 for (
int i = 1; i <= N - 2; ++i)
447 problem.AddResidualBlock(
448 new ceres::AutoDiffCostFunction<PositionSmoothResidual, 2, 4, 4, 4>(
457 double d_avg = computeAverageSpacing(traj);
460 for (
int i = 0; i < N - 1; ++i)
462 problem.AddResidualBlock(
463 new ceres::AutoDiffCostFunction<SpacingResidual, 1, 4, 4>(
471 for (
int i = 0; i < N; ++i)
473 problem.AddResidualBlock(
474 new ceres::AutoDiffCostFunction<ObstacleResidual2D, 1, 4>(
477 params.obs_max_distance,
484 for (
int i = 0; i < N - 1; ++i)
488 const int samples = segmentSampleCount(traj[i], traj[i + 1], params);
489 for (
int s = 1;
s <= samples; ++
s)
491 double t =
static_cast<double>(
s) / (samples + 1);
492 problem.AddResidualBlock(
493 new ceres::AutoDiffCostFunction<SegmentObstacleResidual2D, 1, 4, 4>(
496 params.obs_max_distance,
510 std::vector<double> trackingScale(N, 1.0);
512 if (params.use_tracking_clearance_adaptive)
514 for (
int i = 0; i < N; ++i)
517 queryDistance(wrapper, originalTargets[i].x, originalTargets[i].y),
518 params.tracking_fade_start_clearance,
519 params.tracking_fade_end_clearance);
529 for (
int i = 1; i <= N - 2; ++i)
531 problem.AddResidualBlock(
532 new ceres::AutoDiffCostFunction<BoundedTrackingResidual, 1, 4>(
534 originalTargets[i].y,
535 params.tracking_deadzone,
536 params.tracking_max_deviation,
537 params.w_tracking_in_bounds,
538 params.w_tracking_hard)),
544 if (w.w_tracking > 0.0)
546 for (
int i = 1; i <= N - 2; ++i)
548 const double weight = w.w_tracking * trackingScale[i];
556 problem.AddResidualBlock(
557 new ceres::AutoDiffCostFunction<SoftTrackingResidual, 2, 4>(
559 originalTargets[i].x, originalTargets[i].y, weight)),
565 ceres::Solver::Options options = makeSolverOptions(params, maxIterations);
566 ceres::Solver::Summary summary;
567 ceres::Solve(options, &problem, &summary);
569 normalizeTrajectoryAngles(traj);
572 ARMARX_INFO <<
"[Position pass] Final cost: " << summary.final_cost
573 <<
" iterations: " << summary.num_successful_steps
574 <<
" termination: " << summary.termination_type;
577 bool repairWaypoints(std::vector<CenterPoint>& traj,
578 const algorithms::Costmap& costmap,
580 const std::vector<CenterPoint>& originalTargets,
581 const Params& params)
583 const int N =
static_cast<int>(traj.size());
585 for (
int i = 1; i < N - 1; ++i)
590 double d = queryDistance(wrapper, traj[i].x, traj[i].y);
591 if (d > params.min_clearance)
599 const auto vertex = costmap.findClosestCollisionFreeVertex(
600 Eigen::Vector2f{
static_cast<float>(traj[i].x),
601 static_cast<float>(traj[i].y)},
602 static_cast<float>(params.tracking_max_deviation),
603 static_cast<float>(params.min_clearance));
608 const double dx = vertex->position.x() - originalTargets[i].x;
609 const double dy = vertex->position.y() - originalTargets[i].y;
610 if (std::hypot(dx, dy) > params.tracking_max_deviation + 1e-3)
613 traj[i].x = vertex->position.x();
614 traj[i].y = vertex->position.y();
620 double computeMaxDeviation(
const std::vector<CenterPoint>& traj,
621 const std::vector<CenterPoint>& originalTargets)
624 for (std::size_t i = 0; i < traj.size(); ++i)
626 double dx = traj[i].x - originalTargets[i].x;
627 double dy = traj[i].y - originalTargets[i].y;
628 maxDev = std::max(maxDev, std::hypot(dx, dy));
637 trajectory(trajectory),
647 std::vector<CenterPoint> traj = toCenterTrajectory(trajectory);
648 const std::vector<CenterPoint> originalTargets = traj;
650 if (!validateTrajectory(traj,
"pre-processing"))
652 ARMARX_WARNING <<
"[SPFA smoothing] Invalid trajectory before pre-processing. Aborting.";
653 return {std::nullopt, std::nullopt,
false,
false, {}};
656 const std::vector<std::size_t> trapped =
657 projectTrappedWaypoints(traj, wrapper, costmap, params);
659 if (not trapped.empty())
662 <<
" waypoint(s) sit inside an obstacle and could not be projected "
664 << params.tracking_max_deviation
665 <<
" mm; first at index " << trapped.front()
666 <<
". The optimizer sees no gradient there, so the result will very "
667 "likely fail the collision check.";
672 const std::vector<CenterPoint> preprocessed = traj;
674 if (!validateTrajectory(traj,
"post-projection"))
676 ARMARX_WARNING <<
"[SPFA smoothing] Invalid trajectory after pre-processing. Aborting.";
677 return {std::nullopt, std::nullopt,
false,
false, {}};
680 logResidualBreakdown(traj, wrapper, originalTargets, params,
"after pre-processing");
683 const std::array<PassWeights, 3> passes = {
684 PassWeights{params.pass1_w_obs,
685 params.pass1_w_pose_smooth,
686 params.pass1_w_spacing,
687 params.pass1_w_tracking,
688 params.pass1_w_safe_tracking},
689 PassWeights{params.pass2_w_obs,
690 params.pass2_w_pose_smooth,
691 params.pass2_w_spacing,
692 params.pass2_w_tracking,
693 params.pass2_w_safe_tracking},
694 PassWeights{params.pass3_w_obs,
695 params.pass3_w_pose_smooth,
696 params.pass3_w_spacing,
697 params.pass3_w_tracking,
698 params.pass3_w_safe_tracking}};
700 for (
size_t pass = 0; pass < passes.size(); ++pass)
702 ARMARX_INFO <<
"[SPFA smoothing] Starting position pass " << (pass + 1);
703 runPositionPass(traj, wrapper, originalTargets, passes[pass], params, params.max_iterations);
704 logResidualBreakdown(traj, wrapper, originalTargets, params,
705 "after position pass " + std::to_string(pass + 1));
707 if (!validateTrajectory(traj,
"post-position-pass-" + std::to_string(pass + 1)))
709 ARMARX_WARNING <<
"[SPFA smoothing] NaN/Inf after position pass " << (pass + 1)
710 <<
". Reverting to the pre-processed path.";
716 normalizeTrajectoryAngles(traj);
729 const float inputMinClearance = minimumClearance(trajectory, costmap);
731 const bool judgeAgainstInput =
732 params.allow_degraded_input and
733 inputMinClearance < static_cast<float>(params.min_clearance);
742 constexpr double absoluteFloor = 1.0;
744 const double effectiveLimit =
745 judgeAgainstInput ? std::max(absoluteFloor,
static_cast<double>(inputMinClearance))
746 : params.min_clearance;
748 if (judgeAgainstInput)
750 ARMARX_INFO <<
"[SPFA smoothing] The path handed in already reaches "
751 << inputMinClearance <<
" mm, inside the " << params.min_clearance
752 <<
" mm limit. Judging the result against that instead, so the "
753 "alternative is not simply a rougher path at the same clearance.";
758 bool collisionFree = checker.
check(smoothedTrajectory);
762 ARMARX_WARNING <<
"[SPFA smoothing] Initial smoothing has collisions. Running repair pass.";
764 if (!repairWaypoints(traj, costmap, wrapper, originalTargets, params))
766 ARMARX_ERROR <<
"[SPFA smoothing] Repair failed: could not project colliding waypoints "
767 "to collision-free positions within max deviation.";
771 PassWeights repairWeights{params.repair_w_obs,
772 params.repair_w_pose_smooth,
773 params.repair_w_spacing,
774 params.repair_w_tracking,
775 params.repair_w_safe_tracking};
776 runPositionPass(traj, wrapper, originalTargets, repairWeights, params, params.repair_max_iterations);
777 logResidualBreakdown(traj, wrapper, originalTargets, params,
"after repair pass");
779 smoothedTrajectory = toGlobalTrajectory(traj);
780 collisionFree = checker.
check(smoothedTrajectory);
795 if (not collisionFree and not params.use_obstacle_freeze)
797 ARMARX_WARNING <<
"[SPFA smoothing] Still colliding. Retrying with tight waypoints "
809 std::vector<CenterPoint> pinned = originalTargets;
811 for (
const PassWeights& pass : passes)
813 runPositionPass(pinned,
818 params.max_iterations,
824 if (checker.
check(candidate))
828 smoothedTrajectory = candidate;
829 collisionFree =
true;
833 const double maxDeviation = computeMaxDeviation(traj, originalTargets);
834 const bool deviationOk = maxDeviation <= params.tracking_max_deviation + 1e-3;
836 if (collisionFree && deviationOk)
838 ARMARX_INFO <<
"[SPFA smoothing] Smoothed trajectory is collision-free. Max deviation: "
839 << maxDeviation <<
" mm";
844 ARMARX_WARNING <<
"[SPFA smoothing] Smoothed trajectory still has collisions after repair.";
846 ARMARX_WARNING <<
"[SPFA smoothing] Max deviation too large: " << maxDeviation
847 <<
" mm (limit: " << params.tracking_max_deviation <<
" mm).";
850 for (
int i = 0; i < static_cast<int>(traj.size()); ++i)
852 ARMARX_DEBUG << std::fixed << std::setprecision(3) <<
"i=" << i <<
" : x=" << traj[i].x
853 <<
" y=" << traj[i].y <<
" theta=" << traj[i].theta <<
" v=" << traj[i].v;
861 std::vector<std::size_t> repairedWaypoints;
862 bool geometryValid =
true;
866 if (not violations.empty())
868 ARMARX_WARNING <<
"[nav-guard] path-fold-detected: the smoothed path reverses "
870 << violations.size() <<
" waypoint(s), first at index "
871 << violations.front()
872 <<
". Repairing by removing them; a fold collapses the spline tangent "
873 "downstream and stalls the reparametrization there.";
880 const bool repairedIsCollisionFree = checker.
check(candidate);
883 if (repairedIsCollisionFree and repairedIsMonotone)
886 << repairedWaypoints.size() <<
" waypoint(s), "
887 << smoothedTrajectory.
points().size() <<
" -> "
888 << candidate.
points().size()
889 <<
" points, still collision-free.";
891 smoothedTrajectory = candidate;
895 ARMARX_WARNING <<
"[nav-guard] path-fold-unrepairable: removing the folded "
897 << (repairedIsCollisionFree ?
"leave the path folded"
898 :
"cut a corner into an obstacle")
899 <<
". Rejecting the smoothed path; the caller falls back to the "
902 repairedWaypoints.clear();
903 geometryValid =
false;
908 const bool success = collisionFree && deviationOk;
910 return {smoothedTrajectory,
911 preprocessedTrajectory,
916 minimumClearance(smoothedTrajectory, costmap),
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.
#define ARMARX_VERBOSE
The logging level for verbose information.
double s(double t, double s0, double v0, double a0, double j)
double a(double t, 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.
T obstacleBarrier(const T &d_raw, const double weight, const double max_dist, const double clearance)
Obstacle cost: a single parabola in the distance to the nearest obstacle.
double trackingScaleFromClearance(double clearance, double fadeStartClearance, double fadeEndClearance)
Scale for the tracking weight, from the clearance at a waypoint's reference position.
client::detail::GlobalPlanningStrategy freeze(const client::GlobalPlanningStrategy &strategy)
std::shared_ptr< Value > value()
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)
double norm(const Point &a)
Bounded tracking residual.