residuals.h
Go to the documentation of this file.
1#include <algorithm>
2#include <cmath>
3#include <iomanip>
4#include <iostream>
5#include <istream>
6#include <vector>
7
9
12
13#include <ceres/ceres.h>
14
15#if CERES_VERSION_MAJOR >= 2 && CERES_VERSION_MINOR >= 1
16# include <ceres/manifold.h>
17 using SubsetConstraint = ceres::SubsetManifold;
18# define SET_CONSTRAINT(problem, param, constraint) \
19 (problem).SetManifold((param), (constraint))
20#else
21# include <ceres/local_parameterization.h>
22 using SubsetConstraint = ceres::SubsetParameterization;
23# define SET_CONSTRAINT(problem, param, constraint) \
24 (problem).SetParameterization((param), (constraint))
25#endif
26
28{
29 // Used ChatGPT for initial draft
30 // Full CHOMP-like smoother using Ceres with spacing residual and correct angle handling.
31 //
32 // NOTE: This is a compact but complete example. Adapt the centerToRobot transform, SDF grid source,
33 // weights, and solver options to your codebase.
34
35
36 // -----------------------------
37 // Data structures
38 // -----------------------------
40 {
41 double x, y, theta, v;
42 };
43
44 // -----------------------------
45 // Utilities: angle diff + normalize
46 // -----------------------------
47 template <typename T>
48 T
49 angleDiff(const T& a, const T& b)
50 {
51 // returns normalized a - b in [-pi, pi]
52 T diff = a - b;
53 return ceres::atan2(ceres::sin(diff), ceres::cos(diff));
54 }
55
56 inline double
58 {
59 return std::atan2(std::sin(a), std::cos(a));
60 }
61
62 static void
63 normalizeTrajectoryAngles(std::vector<CenterPoint>& traj)
64 {
65 for (auto& p : traj)
66 p.theta = normalizeAngle(p.theta);
67 }
68
69 inline double
70 smoothstep(double edge0, double edge1, double x)
71 {
72 if (x <= edge0)
73 return 0.0;
74 if (x >= edge1)
75 return 1.0;
76 double t = (x - edge0) / (edge1 - edge0);
77 return t * t * (3.0 - 2.0 * t);
78 }
79
80 inline double
82 double fade_start_clearance,
83 double fade_end_clearance)
84 {
85 // 1.0 in tight space (low clearance), 0.0 in open space (high clearance)
86 return 1.0 - smoothstep(fade_start_clearance, fade_end_clearance, clearance);
87 }
88
89 namespace detail
90 {
91 template <typename T>
92 inline T
94 {
95 if constexpr (std::is_arithmetic_v<T>)
96 return std::exp(x);
97 else
98 return ceres::exp(x);
99 }
100
101 template <typename T>
102 inline T
104 {
105 if constexpr (std::is_arithmetic_v<T>)
106 return std::log(x);
107 else
108 return ceres::log(x);
109 }
110 } // namespace detail
111
112 // Smooth minimum: differentiable approximation of min().
113 // alpha → ∞ → exact min; alpha → 0 → average.
114 // Uses a min-pivot for numerical stability: all exponents are <= 0,
115 // so overflow is impossible even when one sample is deeply negative.
116 template <typename T>
117 T
118 smoothMin(const std::vector<T>& values, double alpha)
119 {
120 if (values.empty())
121 return T(0);
122 if (values.size() == 1)
123 return values[0];
124
125 T min_val = values[0];
126 for (const auto& v : values)
127 if (v < min_val)
128 min_val = v;
129
130 T sum = T(0);
131 for (const auto& v : values)
132 {
133 T arg = -alpha * (v - min_val); // arg <= 0, never overflows
134 sum += detail::expForSmoothMin(arg);
135 }
136
137 return min_val - detail::logForSmoothMin(sum) / alpha;
138 }
139
140 // -----------------------------
141 // Discrete orientation helpers for fuzzy obstacle queries
142 // -----------------------------
143 static std::pair<int, int>
144 getDiscreteOrientationBracket(double theta_rad, int num_orientations)
145 {
146 double deg = theta_rad * 180.0 / M_PI;
147 // wrap to [0, 360)
148 while (deg < 0.0)
149 deg += 360.0;
150 while (deg >= 360.0)
151 deg -= 360.0;
152
153 double deg_per = 360.0 / num_orientations;
154 int lower = static_cast<int>(std::floor(deg / deg_per));
155 int upper = lower + 1;
156 if (lower < 0)
157 lower += num_orientations;
158 if (upper >= num_orientations)
159 upper -= num_orientations;
160 return {lower, upper};
161 }
162
163 // -----------------------------
164 // Residuals
165 // -----------------------------
166
167 // Pose second-difference smoothness residual (x,y,theta)
169 {
170 PoseSmoothResidual(double pos_weight = 1.0, double orientation_weight = 1.0) :
171 pos_w(pos_weight), ori_w(orientation_weight)
172 {
173 }
174
175 template <typename T>
176 bool
177 operator()(const T* const c_prev,
178 const T* const c_i,
179 const T* const c_next,
180 T* residual) const
181 {
182 residual[0] = T(pos_w) * (c_prev[0] - T(2.0) * c_i[0] + c_next[0]); // x
183 residual[1] = T(pos_w) * (c_prev[1] - T(2.0) * c_i[1] + c_next[1]); // y
184 // theta second-diff using angleDiff to handle wrap
185 T dprev = angleDiff(c_prev[2], c_i[2]);
186 T dnext = angleDiff(c_i[2], c_next[2]);
187 residual[2] = T(ori_w) * (dprev - dnext);
188 return true;
189 }
190
191 double pos_w;
192 double ori_w;
193 };
194
195 // Residual to enforce smooth orientation transition at start and end of trajectory
197 {
199 {
200 }
201
202 template <typename T>
203 bool
204 operator()(const T* const theta0, const T* const theta1, T* residuals) const
205 {
206
207 T d = angleDiff(theta1[2], theta0[2]);
208 residuals[0] = T(w_) * d;
209 return true;
210 }
211
212 double w_;
213 };
214
215 // Residual to pull theta toward the pre-processed (hill-climbed) orientation.
216 // This replaces the old theta-blending inside ObstacleResidual with an
217 // explicit, tunable cost that keeps theta from jumping wildly without
218 // hiding the true collision gradient.
220 {
221 ThetaTrackingResidual(double w, double theta_target) : w_(w), theta_target_(theta_target) {}
222
223 template <typename T>
224 bool operator()(const T* const node, T* residual) const
225 {
226 residual[0] = T(w_) * angleDiff(node[2], T(theta_target_));
227 return true;
228 }
229
230 double w_;
232 };
233
234 // Accel/decel limit residual: symmetric per-segment rate limit.
235 // Penalizes only if the velocity change across a segment exceeds the bounds.
237 {
238 AccelDecelLimitResidual(double max_accel, double max_decel, double weight) :
239 max_accel_(max_accel), max_decel_(max_decel), w(weight)
240 {
241 }
242
243 template <typename T>
244 bool
245 operator()(const T* const c_i, const T* const c_ip1, T* residual) const
246 {
247 T dv = c_ip1[3] - c_i[3];
248 if (dv > T(max_accel_))
249 residual[0] = T(w) * (dv - T(max_accel_));
250 else if (dv < -T(max_decel_))
251 residual[0] = T(w) * (-dv - T(max_decel_));
252 else
253 residual[0] = T(0.0);
254 return true;
255 }
256
259 double w;
260 };
261
262 // Nominal velocity residual: pulls v_i toward the cruise speed.
264 {
265 NominalVelocityResidual(double v_nominal, double weight) :
266 v_nominal_(v_nominal), w(weight)
267 {
268 }
269
270 template <typename T>
271 bool
272 operator()(const T* const c_i, T* residual) const
273 {
274 residual[0] = T(w) * (c_i[3] - T(v_nominal_));
275 return true;
276 }
277
279 double w;
280 };
281
282 // Velocity proximity residual: penalizes high speed near obstacles.
283 // danger is precomputed in [0,1] from the obstacle distance.
284 // Only speeds above min_velocity are penalized.
286 {
287 VelocityProximityResidual(double danger, double min_velocity, double weight) :
288 danger_(danger), min_velocity_(min_velocity), w(weight)
289 {
290 }
291
292 template <typename T>
293 bool
294 operator()(const T* const c_i, T* residual) const
295 {
296 T v_above_min = c_i[3] - T(min_velocity_);
297 if (v_above_min < T(0))
298 v_above_min = T(0);
299 residual[0] = T(w) * v_above_min * T(danger_);
300 return true;
301 }
302
303 double danger_;
305 double w;
306 };
307
308 // Jerk residual (optional) for tighter smoothness on pose (uses 4 consecutive centers)
310 {
311 PoseJerkResidual(double weight = 1.0) : w(weight)
312 {
313 }
314
315 template <typename T>
316 bool
317 operator()(const T* const c_m2,
318 const T* const c_m1,
319 const T* const c_i,
320 const T* const c_p1,
321 T* residual) const
322 {
323 residual[0] = T(w) * (c_m2[0] - T(3.0) * c_m1[0] + T(3.0) * c_i[0] - c_p1[0]);
324 residual[1] = T(w) * (c_m2[1] - T(3.0) * c_m1[1] + T(3.0) * c_i[1] - c_p1[1]);
325 // approximate angle jerk (small-angle assumption)
326 T a_m2 = c_m2[2], a_m1 = c_m1[2], a_i = c_i[2], a_p1 = c_p1[2];
327 T r = angleDiff(a_m2, a_i) - T(3.0) * angleDiff(a_m1, a_i) +
328 T(3.0) * angleDiff(a_i, a_i) - angleDiff(a_p1, a_i);
329 residual[2] = T(w) * r;
330 // T d1 = angleDiff(a_m1, a_i);
331 // T d2 = angleDiff(a_i, a_p1);
332 // T d3 = angleDiff(a_p1, a_p2);
333 //
334 // T jerk = d1 - T(2.0)*d2 + d3;
335 // residual[2] = T(w) * jerk;
336
337 return true;
338 }
339
340 double w;
341 };
342
343 // obstacle residual using Costmap3DWrapper; weighted
345 {
347 double weight,
348 double max_dist,
349 double clearance,
350 double barrier_eps,
351 bool use_fuzzy_orientation = true,
352 int num_orientations = 72,
353 int fuzzy_window_bins = 2,
354 double fuzzy_alpha = 5.0) :
355 wrapper_(wrapper),
356 w_(weight),
357 max_dist_(max_dist),
358 clearance_(clearance),
359 barrier_eps_(barrier_eps),
360 use_fuzzy_orientation_(use_fuzzy_orientation),
361 num_orientations_(num_orientations),
362 fuzzy_window_bins_(fuzzy_window_bins),
363 fuzzy_alpha_(fuzzy_alpha)
364 {
365 }
366
367 template <typename T>
368 bool
369 operator()(const T* const node, T* residual) const
370 {
371 // Evaluate obstacle cost at the current Ceres theta so the optimizer
372 // sees the true collision landscape. A separate ThetaTrackingResidual
373 // keeps theta from jumping wildly without hiding the gradient.
374 T theta_query = node[2];
375 T obs_dist_raw;
376
378 {
379 // Sample orientations in a window around theta_query and take the
380 // smooth minimum. Each sample orientation is a differentiable
381 // function of theta, so the obstacle residual propagates a strong
382 // theta gradient that pushes the robot away from colliding orientations.
383 const double bin_size = 2.0 * M_PI / static_cast<double>(num_orientations_);
384 std::vector<T> samples;
385 samples.reserve(2 * fuzzy_window_bins_ + 1);
386 for (int k = -fuzzy_window_bins_; k <= fuzzy_window_bins_; ++k)
387 {
388 T theta_sample = theta_query + T(static_cast<double>(k) * bin_size);
389 T d_sample;
390 wrapper_(&node[0], &node[1], &theta_sample, &d_sample);
391 samples.push_back(d_sample);
392 }
393 obs_dist_raw = smoothMin(samples, fuzzy_alpha_);
394 }
395 else
396 {
397 wrapper_(&node[0], &node[1], &theta_query, &obs_dist_raw);
398 }
399
400 // Defensive guard: if the wrapper ever returns non-finite, treat as deep
401 // obstacle so the residual stays finite and Ceres does not abort.
402 if (!ceres::isfinite(obs_dist_raw))
403 {
404 residual[0] = T(w_) * T(100.0);
405 return true;
406 }
407
408 // Keep deeply negative values bounded for numerical stability.
409 const T min_allowed = T(-3.0) * T(max_dist_);
410 if (obs_dist_raw < min_allowed)
411 obs_dist_raw = min_allowed;
412
413 // Shift the distance field so the optimizer treats 'clearance' mm away
414 // from the true safety margin as the new zero boundary.
415 T obs_dist = obs_dist_raw - T(clearance_);
416
417 if (obs_dist >= T(0.0))
418 {
419 // Outside the clearance margin: no obstacle cost.
420 residual[0] = T(0.0);
421 }
422 else
423 {
424 // Inside the clearance margin: smooth one-sided barrier.
425 // Zero at the boundary, ~linear deep inside, capped to keep the
426 // residual finite for very deep penetrations.
427 T penetration = -obs_dist;
428 const T max_pen = T(3.0) * T(max_dist_);
429 if (penetration > max_pen)
430 penetration = max_pen;
431
432 const T eps = T(barrier_eps_);
433 // Smooth hinge: r(p) = 0.5 * (sqrt(p^2 + eps^2) + p) - eps/2
434 // r(0) = 0, r'(0) = 0.5, r(p) -> p for p >> eps.
435 T r = T(0.5) * (ceres::sqrt(penetration * penetration + eps * eps) + penetration)
436 - T(0.5) * eps;
437 residual[0] = T(w_) * r;
438 }
439 return true;
440 }
441
443 double w_;
444 double max_dist_;
451 };
452
453 // Midpoint obstacle residual: evaluates obstacle cost at the midpoint
454 // between two consecutive waypoints. This is essential because the
455 // straight segment between two free waypoints can cut through an
456 // obstacle that neither endpoint touches.
458 {
460 double weight,
461 double max_dist,
462 double clearance,
463 double barrier_eps,
464 bool use_fuzzy_orientation = true,
465 int num_orientations = 72,
466 int fuzzy_window_bins = 2,
467 double fuzzy_alpha = 5.0) :
468 wrapper_(wrapper),
469 w_(weight),
470 max_dist_(max_dist),
471 clearance_(clearance),
472 barrier_eps_(barrier_eps),
473 use_fuzzy_orientation_(use_fuzzy_orientation),
474 num_orientations_(num_orientations),
475 fuzzy_window_bins_(fuzzy_window_bins),
476 fuzzy_alpha_(fuzzy_alpha)
477 {
478 }
479
480 template <typename T>
481 bool
482 operator()(const T* const c_i, const T* const c_ip1, T* residual) const
483 {
484 T mx = (c_i[0] + c_ip1[0]) / T(2.0);
485 T my = (c_i[1] + c_ip1[1]) / T(2.0);
486 T mtheta = c_i[2] + angleDiff(c_ip1[2], c_i[2]) / T(2.0);
487
488 // Evaluate obstacle cost at the current Ceres midpoint theta so the
489 // optimizer sees the true collision landscape.
490 T theta_query = mtheta;
491 T obs_dist_raw;
492
494 {
495 // Sample orientations in a window around the midpoint theta and
496 // take the smooth minimum. This propagates a strong theta gradient
497 // so the segment can rotate away from collisions.
498 const double bin_size = 2.0 * M_PI / static_cast<double>(num_orientations_);
499 std::vector<T> samples;
500 samples.reserve(2 * fuzzy_window_bins_ + 1);
501 for (int k = -fuzzy_window_bins_; k <= fuzzy_window_bins_; ++k)
502 {
503 T theta_sample = theta_query + T(static_cast<double>(k) * bin_size);
504 T d_sample;
505 wrapper_(&mx, &my, &theta_sample, &d_sample);
506 samples.push_back(d_sample);
507 }
508 obs_dist_raw = smoothMin(samples, fuzzy_alpha_);
509 }
510 else
511 {
512 wrapper_(&mx, &my, &theta_query, &obs_dist_raw);
513 }
514
515 // Defensive guard: if the wrapper ever returns non-finite, treat as deep
516 // obstacle so the residual stays finite and Ceres does not abort.
517 if (!ceres::isfinite(obs_dist_raw))
518 {
519 residual[0] = T(w_) * T(100.0);
520 return true;
521 }
522
523 // Keep deeply negative values bounded for numerical stability.
524 const T min_allowed = T(-3.0) * T(max_dist_);
525 if (obs_dist_raw < min_allowed)
526 obs_dist_raw = min_allowed;
527
528 T obs_dist = obs_dist_raw - T(clearance_);
529
530 if (obs_dist >= T(0.0))
531 {
532 // Outside the clearance margin: no obstacle cost.
533 residual[0] = T(0.0);
534 }
535 else
536 {
537 // Inside the clearance margin: smooth one-sided barrier.
538 T penetration = -obs_dist;
539 const T max_pen = T(3.0) * T(max_dist_);
540 if (penetration > max_pen)
541 penetration = max_pen;
542
543 const T eps = T(barrier_eps_);
544 T r = T(0.5) * (ceres::sqrt(penetration * penetration + eps * eps) + penetration)
545 - T(0.5) * eps;
546 residual[0] = T(w_) * r;
547 }
548 return true;
549 }
550
552 double w_;
553 double max_dist_;
560 };
561
562 // Tracking residual to original center traj: (x,y,theta,v)
564 {
566 double yr,
567 double thetar,
568 double vr,
569 double wx,
570 double wy,
571 double wth,
572 double wv) :
573 xr_(xr), yr_(yr), thetar_(thetar), vr_(vr), wx_(wx), wy_(wy), wth_(wth), wv_(wv)
574 {
575 }
576
577 template <typename T>
578 bool
579 operator()(const T* const c_i, T* residual) const
580 {
581 residual[0] = T(wx_) * (c_i[0] - T(xr_));
582 residual[1] = T(wy_) * (c_i[1] - T(yr_));
583 residual[2] = T(wth_) * angleDiff(c_i[2], T(thetar_));
584 residual[3] = T(wv_) * (c_i[3] - T(vr_));
585 return true;
586 }
587
588 double xr_, yr_, thetar_, vr_;
589 double wx_, wy_, wth_, wv_;
590 };
591
592 // Velocity limit hinge residual: penalize v > vmax
594 {
595 VelLimitResidual(double vmax, double weight = 1.0) : vmax_(vmax), w(weight)
596 {
597 }
598
599 template <typename T>
600 bool
601 operator()(const T* const c_i, T* residual) const
602 {
603 T v = c_i[3];
604 T diff = v - T(vmax_);
605 if (diff > T(0))
606 residual[0] = T(w) * diff;
607 else
608 residual[0] = T(0.0);
609 return true;
610 }
611
612 double vmax_;
613 double w;
614 };
615
616 // Robot smoothness: second-diff applied to robot pose derived from center.
617 // The mapping center->robot is templated for AutoDiff.
619 {
620 RobotSmoothResidual(double weight = 1.0, double dx = -0.5, double dy = 0.0) :
621 w(weight), dx_(dx), dy_(dy)
622 {
623 }
624
625 template <typename T>
626 inline void
627 centerToRobotTemplated(const T* const c, T& rx, T& ry, T& rtheta) const
628 {
629 T cx = c[0], cy = c[1], cth = c[2];
630 T ccos = ceres::cos(cth), csin = ceres::sin(cth);
631 rx = cx + ccos * T(dx_) - csin * T(dy_);
632 ry = cy + csin * T(dx_) + ccos * T(dy_);
633 rtheta = cth; // robot orientation equals center orientation here; adapt if needed
634 }
635
636 template <typename T>
637 bool
638 operator()(const T* const c_prev,
639 const T* const c_i,
640 const T* const c_next,
641 T* residual) const
642 {
643 T rpx, rpy, rpth, rix, riy, rith, rnx, rny, rnth;
644 centerToRobotTemplated(c_prev, rpx, rpy, rpth);
645 centerToRobotTemplated(c_i, rix, riy, rith);
646 centerToRobotTemplated(c_next, rnx, rny, rnth);
647 residual[0] = T(w) * (rpx - T(2.0) * rix + rnx);
648 residual[1] = T(w) * (rpy - T(2.0) * riy + rny);
649 // Proper theta second-diff via angleDiff:
650 residual[2] = T(w) * (angleDiff(rpth, rith) - angleDiff(rnth, rith));
651 return true;
652 }
653
654 double w;
655 double dx_, dy_;
656 };
657
658 // Robot-pose proximity to a reference robot base pose.
659 // Smooth one-sided hinge: negligible cost inside the deadband,
660 // linearly-growing cost outside. The center->robot transform matches
661 // RobotSmoothResidual (dx = -0.5, dy = 0.0).
663 {
665 double ry0,
666 double threshold_mm,
667 double weight,
668 double eps_mm) :
669 rx0_(rx0), ry0_(ry0), threshold_(threshold_mm), w_(weight), eps_(eps_mm)
670 {
671 }
672
673 template <typename T>
674 inline void
675 centerToRobot(const T* const c, T& rx, T& ry) const
676 {
677 T cx = c[0], cy = c[1], cth = c[2];
678 T ccos = ceres::cos(cth), csin = ceres::sin(cth);
679 rx = cx + ccos * T(-0.5) - csin * T(0.0);
680 ry = cy + csin * T(-0.5) + ccos * T(0.0);
681 }
682
683 template <typename T>
684 bool
685 operator()(const T* const c_i, T* residual) const
686 {
687 T rx, ry;
688 centerToRobot(c_i, rx, ry);
689
690 T dx = rx - T(rx0_);
691 T dy = ry - T(ry0_);
692 // Regularize the distance to avoid sqrt(0) and the resulting
693 // division-by-zero in its derivative at the initial trajectory.
694 const T dist_eps = T(1e-6);
695 T dist = ceres::sqrt(dx * dx + dy * dy + dist_eps * dist_eps);
696
697 T y = dist - T(threshold_);
698 residual[0] = T(w_) * T(0.5)
699 * (ceres::sqrt(y * y + T(eps_ * eps_)) + y);
700 return true;
701 }
702
703 double rx0_, ry0_;
705 };
706
707 // Spacing residual: keeps |p_{i+1} - p_i| close to average spacing d_avg
708 // Uses squared distance so the Ceres cost is (dist^2 - d_avg^2)^2,
709 // which is a true quadratic in (x,y) and avoids the sqrt(0) singularity.
711 {
712 SpacingResidual(double d_avg, double weight) : d_avg_(d_avg), w(weight)
713 {
714 }
715
716 template <typename T>
717 bool
718 operator()(const T* const c_i, const T* const c_ip1, T* residual) const
719 {
720 T dx = c_ip1[0] - c_i[0];
721 T dy = c_ip1[1] - c_i[1];
722 T dist_sq = dx * dx + dy * dy;
723 T target_sq = T(d_avg_ * d_avg_);
724 residual[0] = T(w) * (dist_sq - target_sq);
725 return true;
726 }
727
728 double d_avg_;
729 double w;
730 };
731
732 // Soft spacing residual: linear penalty on relative deviation up to a
733 // configurable threshold, then an additional quadratic penalty beyond it.
734 // This lets the optimizer redistribute segment lengths moderately (e.g. at
735 // corridor exits) without the explosive stiffness of (dist^2 - d_avg^2)^2.
737 {
739 double linear_weight,
740 double quadratic_weight,
741 double relative_deviation_threshold) :
742 d_avg_(d_avg),
743 linear_w_(linear_weight),
744 quadratic_w_(quadratic_weight),
745 threshold_(relative_deviation_threshold)
746 {
747 }
748
749 template <typename T>
750 bool
751 operator()(const T* const c_i, const T* const c_ip1, T* residual) const
752 {
753 T dx = c_ip1[0] - c_i[0];
754 T dy = c_ip1[1] - c_i[1];
755 T dist_sq = dx * dx + dy * dy;
756 // Regularise sqrt to avoid a singular derivative at dist == 0.
757 T dist = ceres::sqrt(dist_sq + T(1e-12));
758 T rel_dev = ceres::abs(dist / T(d_avg_) - T(1.0));
759
760 T r = T(linear_w_) * rel_dev;
761 T excess = rel_dev - T(threshold_);
762 if (excess > T(0.0))
763 {
764 r += T(quadratic_w_) * excess * excess;
765 }
766 residual[0] = r;
767 return true;
768 }
769
770 double d_avg_;
771 double linear_w_;
774 };
775
776 // -----------------------------
777 // Options & helpers
778 // -----------------------------
779 static double
780 segment_length(const CenterPoint& a, const CenterPoint& b)
781 {
782 return std::hypot(b.x - a.x, b.y - a.y);
783 }
784
785 static double
786 computeAverageSpacing(const std::vector<CenterPoint>& traj)
787 {
788 if (traj.size() < 2)
789 return 0.0;
790 double sum = 0.0;
791 for (size_t i = 0; i + 1 < traj.size(); ++i)
792 sum += segment_length(traj[i], traj[i + 1]);
793 return sum / double(traj.size() - 1);
794 }
795
796 // -----------------------------
797 // Diagnostic: log per-residual cost breakdown
798 // -----------------------------
799 static void
800 logResidualBreakdown(const std::vector<CenterPoint>& traj,
801 const Costmap3DWrapper& wrapper,
802 const std::vector<CenterPoint>& targets,
803 const std::vector<double>& tracking_scales,
804 const io::SmoothingParams& opts)
805 {
806 const int N = static_cast<int>(traj.size());
807 double c_obs = 0.0, c_mid_obs = 0.0, c_pose_smooth = 0.0, c_start_rot = 0.0,
808 c_vel_smooth = 0.0, c_jerk = 0.0, c_robot_smooth = 0.0, c_spacing = 0.0,
809 c_tracking = 0.0, c_robot_pose_proximity = 0.0;
810
811 auto evalObsAtTheta = [&](double x, double y, double th) -> double
812 {
813 double d_raw;
814 wrapper(&x, &y, &th, &d_raw);
815 double d = d_raw - opts.clearance;
816 if (d < 0.0)
817 {
818 double pen = -d;
819 double max_pen = 3.0 * opts.obs_max_distance;
820 if (pen > max_pen)
821 pen = max_pen;
822 const double eps = opts.obs_barrier_eps_mm;
823 // Smooth one-sided barrier: zero at the boundary, ~linear deep inside.
824 const double r = 0.5 * (std::sqrt(pen * pen + eps * eps) + pen) - 0.5 * eps;
825 return opts.w_obs * r;
826 }
827 return 0.0;
828 };
829
830 auto evalObsFuzzy = [&](double x, double y, double theta_query) -> double
831 {
832 if (!opts.use_fuzzy_orientation)
833 return evalObsAtTheta(x, y, theta_query);
834
835 double d_raw;
836 if (opts.fuzzy_orientation_window_bins > 0)
837 {
838 // Sample orientations in a window around theta_query and take the
839 // smooth minimum, matching the behaviour of ObstacleResidual.
840 const double bin_size = 2.0 * M_PI / 72.0;
841 std::vector<double> samples;
842 samples.reserve(2 * opts.fuzzy_orientation_window_bins + 1);
843 for (int k = -opts.fuzzy_orientation_window_bins;
844 k <= opts.fuzzy_orientation_window_bins;
845 ++k)
846 {
847 double theta_sample = theta_query + static_cast<double>(k) * bin_size;
848 double d_sample;
849 wrapper(&x, &y, &theta_sample, &d_sample);
850 samples.push_back(d_sample);
851 }
852 d_raw = smoothMin(samples, opts.fuzzy_orientation_alpha);
853 }
854 else
855 {
856 wrapper(&x, &y, &theta_query, &d_raw);
857 }
858
859 // Deep-penetration cap for stability.
860 const double min_allowed = -3.0 * opts.obs_max_distance;
861 if (d_raw < min_allowed)
862 d_raw = min_allowed;
863
864 double d = d_raw - opts.clearance;
865 if (d < 0.0)
866 {
867 double pen = -d;
868 double max_pen = 3.0 * opts.obs_max_distance;
869 if (pen > max_pen)
870 pen = max_pen;
871 const double eps = opts.obs_barrier_eps_mm;
872 // Smooth one-sided barrier: zero at the boundary, ~linear deep inside.
873 const double r = 0.5 * (std::sqrt(pen * pen + eps * eps) + pen) - 0.5 * eps;
874 return opts.w_obs * r;
875 }
876 return 0.0;
877 };
878
879 // Obstacle (waypoint)
880 if (opts.use_obs)
881 {
882 for (int i = 0; i < N; ++i)
883 {
884 double r = evalObsFuzzy(traj[i].x, traj[i].y, traj[i].theta);
885 c_obs += r * r;
886 }
887 for (int i = 0; i < N - 1; ++i)
888 {
889 double midTheta =
890 normalizeAngle(traj[i].theta
891 + angleDiff(traj[i + 1].theta, traj[i].theta) / 2.0);
892 double mx = (traj[i].x + traj[i + 1].x) / 2.0;
893 double my = (traj[i].y + traj[i + 1].y) / 2.0;
894 double r = evalObsFuzzy(mx, my, midTheta);
895 c_mid_obs += r * r;
896 }
897 }
898
899 // Pose smoothness
900 for (int i = 1; i <= N - 2; ++i)
901 {
902 double rx = traj[i - 1].x - 2.0 * traj[i].x + traj[i + 1].x;
903 double ry = traj[i - 1].y - 2.0 * traj[i].y + traj[i + 1].y;
904 double dprev = normalizeAngle(traj[i - 1].theta - traj[i].theta);
905 double dnext = normalizeAngle(traj[i].theta - traj[i + 1].theta);
906 double rth = dprev - dnext;
907 c_pose_smooth += (opts.w_pose_smooth * rx) * (opts.w_pose_smooth * rx)
908 + (opts.w_pose_smooth * ry) * (opts.w_pose_smooth * ry)
909 + (opts.w_orientation_smooth * rth) * (opts.w_orientation_smooth * rth);
910 }
911
912 // Start/end rotation
913 if (N >= 2)
914 {
915 double d = normalizeAngle(traj[1].theta - traj[0].theta);
916 c_start_rot += (opts.w_boundary * d) * (opts.w_boundary * d);
917 d = normalizeAngle(traj[N - 1].theta - traj[N - 2].theta);
918 c_start_rot += (opts.w_boundary * d) * (opts.w_boundary * d);
919 }
920
921 // Accel/decel limits
922 for (int i = 0; i < N - 1; ++i)
923 {
924 double dv = traj[i + 1].v - traj[i].v;
925 if (dv > opts.max_accel_per_segment)
926 c_vel_smooth += (opts.w_accel_decel_limit * (dv - opts.max_accel_per_segment))
927 * (opts.w_accel_decel_limit * (dv - opts.max_accel_per_segment));
928 else if (dv < -opts.max_decel_per_segment)
929 c_vel_smooth += (opts.w_accel_decel_limit * (-dv - opts.max_decel_per_segment))
930 * (opts.w_accel_decel_limit * (-dv - opts.max_decel_per_segment));
931 }
932
933 // Nominal velocity
934 double v_nominal = (N >= 3) ? traj[1].v : traj.front().v;
935 for (int i = 1; i < N - 1; ++i)
936 {
937 double rv = traj[i].v - v_nominal;
938 c_vel_smooth += (opts.w_nominal_velocity * rv) * (opts.w_nominal_velocity * rv);
939 }
940
941 // Velocity proximity
942 for (int i = 1; i < N - 1; ++i)
943 {
944 double d = 0.0;
945 wrapper(&traj[i].x, &traj[i].y, &traj[i].theta, &d);
946 double danger = std::clamp((opts.slow_down_distance - d) / opts.slow_down_distance,
947 0.0,
948 1.0);
949 double v_above_min = std::max(traj[i].v - opts.min_velocity_near_obstacle, 0.0);
950 c_vel_smooth +=
951 (opts.w_velocity_proximity * v_above_min * danger)
952 * (opts.w_velocity_proximity * v_above_min * danger);
953 }
954
955 // Pose jerk
956 if (opts.use_jerk && N >= 4)
957 {
958 for (int i = 2; i <= N - 2; ++i)
959 {
960 double rx = traj[i - 2].x - 3.0 * traj[i - 1].x + 3.0 * traj[i].x - traj[i + 1].x;
961 double ry = traj[i - 2].y - 3.0 * traj[i - 1].y + 3.0 * traj[i].y - traj[i + 1].y;
962 double r = normalizeAngle(traj[i - 2].theta - traj[i].theta)
963 - 3.0 * normalizeAngle(traj[i - 1].theta - traj[i].theta)
964 + 3.0 * normalizeAngle(traj[i].theta - traj[i].theta)
965 - normalizeAngle(traj[i + 1].theta - traj[i].theta);
966 c_jerk += (opts.w_pose_jerk * rx) * (opts.w_pose_jerk * rx)
967 + (opts.w_pose_jerk * ry) * (opts.w_pose_jerk * ry)
968 + (opts.w_pose_jerk * r) * (opts.w_pose_jerk * r);
969 }
970 }
971
972 // Robot smoothness
973 if (opts.use_robot_smooth)
974 {
975 for (int i = 1; i <= N - 2; ++i)
976 {
977 auto c2r = [](const CenterPoint& c, double& rx, double& ry, double& rth)
978 {
979 double ccos = std::cos(c.theta), csin = std::sin(c.theta);
980 rx = c.x + ccos * (-0.5) - csin * (0.0);
981 ry = c.y + csin * (-0.5) + ccos * (0.0);
982 rth = c.theta;
983 };
984 double rpx, rpy, rpth, rix, riy, rith, rnx, rny, rnth;
985 c2r(traj[i - 1], rpx, rpy, rpth);
986 c2r(traj[i], rix, riy, rith);
987 c2r(traj[i + 1], rnx, rny, rnth);
988 double rx = opts.w_robot_smooth * (rpx - 2.0 * rix + rnx);
989 double ry = opts.w_robot_smooth * (rpy - 2.0 * riy + rny);
990 double rth = opts.w_robot_smooth
991 * (normalizeAngle(rpth - rith) - normalizeAngle(rnth - rith));
992 c_robot_smooth += rx * rx + ry * ry + rth * rth;
993 }
994 }
995
996 // Spacing
997 double d_avg = 0.0;
998 for (int i = 0; i + 1 < N; ++i)
999 d_avg += std::hypot(traj[i + 1].x - traj[i].x, traj[i + 1].y - traj[i].y);
1000 if (N > 1)
1001 d_avg /= (N - 1);
1002 if (d_avg <= 1e-9)
1003 d_avg = 0.1;
1004 for (int i = 0; i + 1 < N; ++i)
1005 {
1006 double dist = std::hypot(traj[i + 1].x - traj[i].x, traj[i + 1].y - traj[i].y);
1007 if (opts.use_soft_spacing)
1008 {
1009 double rel_dev = std::abs(dist / d_avg - 1.0);
1010 double r = opts.w_spacing_linear * rel_dev;
1011 double excess = rel_dev - opts.spacing_relative_deviation_threshold;
1012 if (excess > 0.0)
1013 {
1014 r += opts.w_spacing_quadratic * excess * excess;
1015 }
1016 c_spacing += r * r;
1017 }
1018 else
1019 {
1020 double r = opts.w_spacing * (dist * dist - d_avg * d_avg);
1021 c_spacing += r * r;
1022 }
1023 }
1024
1025 // Tracking
1026 if (opts.use_tracking && static_cast<int>(targets.size()) == N)
1027 {
1028 for (int i = 0; i < N; ++i)
1029 {
1030 double s = tracking_scales[i];
1031 double wx = opts.w_track * s, wy = opts.w_track * s,
1032 wth = opts.w_track * 300.0 * s, wv = opts.w_track * 0.2 * s;
1033 double rx = wx * (traj[i].x - targets[i].x);
1034 double ry = wy * (traj[i].y - targets[i].y);
1035 double rth = wth * normalizeAngle(traj[i].theta - targets[i].theta);
1036 double rv = wv * (traj[i].v - targets[i].v);
1037 c_tracking += rx * rx + ry * ry + rth * rth + rv * rv;
1038 }
1039 }
1040
1041 // Robot-pose proximity to pre-processed trajectory
1042 if (opts.use_robot_pose_proximity && static_cast<int>(targets.size()) == N)
1043 {
1044 auto c2r = [](const CenterPoint& c, double& rx, double& ry)
1045 {
1046 double ccos = std::cos(c.theta), csin = std::sin(c.theta);
1047 rx = c.x + ccos * (-0.5) - csin * (0.0);
1048 ry = c.y + csin * (-0.5) + ccos * (0.0);
1049 };
1050 const double eps_sq = opts.robot_pose_proximity_eps_mm
1051 * opts.robot_pose_proximity_eps_mm;
1052 for (int i = 1; i <= N - 2; ++i)
1053 {
1054 double rx0, ry0, rx, ry;
1055 c2r(targets[i], rx0, ry0);
1056 c2r(traj[i], rx, ry);
1057 const double dx = rx - rx0;
1058 const double dy = ry - ry0;
1059 const double dist = std::hypot(dx, dy);
1060 const double y = dist - opts.robot_pose_proximity_threshold_mm;
1061 const double r = opts.w_robot_pose_proximity * tracking_scales[i] * 0.5
1062 * (std::sqrt(y * y + eps_sq) + y);
1063 c_robot_pose_proximity += r * r;
1064 }
1065 }
1066
1067 double total = c_obs + c_mid_obs + c_pose_smooth + c_start_rot + c_vel_smooth + c_jerk
1068 + c_robot_smooth + c_spacing + c_tracking + c_robot_pose_proximity;
1069
1070 ARMARX_INFO << "=== Residual cost breakdown ===";
1071 auto pct = [&](double c) -> double { return total > 0.0 ? c / total * 100.0 : 0.0; };
1072 ARMARX_INFO << "Obstacle (waypoint): " << std::scientific << std::setprecision(3)
1073 << c_obs << " (" << std::fixed << std::setprecision(1) << pct(c_obs) << "%)";
1074 ARMARX_INFO << "Obstacle (midpoint): " << std::scientific << std::setprecision(3)
1075 << c_mid_obs << " (" << std::fixed << std::setprecision(1) << pct(c_mid_obs)
1076 << "%)";
1077 ARMARX_INFO << "Pose smoothness: " << std::scientific << std::setprecision(3)
1078 << c_pose_smooth << " (" << std::fixed << std::setprecision(1)
1079 << pct(c_pose_smooth) << "%)";
1080 ARMARX_INFO << "Start/end rotation: " << std::scientific << std::setprecision(3)
1081 << c_start_rot << " (" << std::fixed << std::setprecision(1)
1082 << pct(c_start_rot) << "%)";
1083 ARMARX_INFO << "Velocity smoothness: " << std::scientific << std::setprecision(3)
1084 << c_vel_smooth << " (" << std::fixed << std::setprecision(1)
1085 << pct(c_vel_smooth) << "%)";
1086 ARMARX_INFO << "Pose jerk: " << std::scientific << std::setprecision(3)
1087 << c_jerk << " (" << std::fixed << std::setprecision(1) << pct(c_jerk)
1088 << "%)";
1089 ARMARX_INFO << "Robot smoothness: " << std::scientific << std::setprecision(3)
1090 << c_robot_smooth << " (" << std::fixed << std::setprecision(1)
1091 << pct(c_robot_smooth) << "%)";
1092 ARMARX_INFO << "Spacing: " << std::scientific << std::setprecision(3)
1093 << c_spacing << " (" << std::fixed << std::setprecision(1) << pct(c_spacing)
1094 << "%)";
1095 ARMARX_INFO << "Tracking: " << std::scientific << std::setprecision(3)
1096 << c_tracking << " (" << std::fixed << std::setprecision(1)
1097 << pct(c_tracking) << "%)";
1098 ARMARX_INFO << "Robot-pose proximity: " << std::scientific << std::setprecision(3)
1099 << c_robot_pose_proximity << " (" << std::fixed << std::setprecision(1)
1100 << pct(c_robot_pose_proximity) << "%)";
1101 ARMARX_INFO << "TOTAL: " << std::scientific << std::setprecision(3)
1102 << total;
1103 }
1104
1105 // -----------------------------
1106 // Main optimizer
1107 // -----------------------------
1108 void
1109 optimizeTrajectoryCeres(std::vector<CenterPoint>& traj,
1110 const Costmap3DWrapper& costmap_wrapper,
1111 const std::vector<CenterPoint>& traj_targets,
1112 const io::SmoothingParams& opts)
1113 {
1114 const int N = static_cast<int>(traj.size());
1115 if (N < 4)
1116 {
1117 ARMARX_INFO << "[optimizeTrajectoryCeres] need at least 4 points\n";
1118 return;
1119 }
1120
1121 normalizeTrajectoryAngles(traj);
1122
1123 // Deduplicate: if two consecutive waypoints (start or end excluded)
1124 // are closer than 1 mm, nudge the later one by 1 mm to avoid a
1125 // zero-length segment and the associated sqrt(0) singularity.
1126 for (int i = 1; i < N - 1; ++i)
1127 {
1128 double dx = traj[i].x - traj[i - 1].x;
1129 double dy = traj[i].y - traj[i - 1].y;
1130 if (std::hypot(dx, dy) < 1.0)
1131 {
1132 double ndx = (i + 1 < N) ? (traj[i + 1].x - traj[i].x) : 1.0;
1133 double ndy = (i + 1 < N) ? (traj[i + 1].y - traj[i].y) : 0.0;
1134 double nlen = std::hypot(ndx, ndy);
1135 if (nlen < 1e-6)
1136 {
1137 ndx = 1.0;
1138 ndy = 0.0;
1139 nlen = 1.0;
1140 }
1141 traj[i].x += 1.0 * ndx / nlen;
1142 traj[i].y += 1.0 * ndy / nlen;
1143 }
1144 }
1145
1146 // Per-waypoint tracking weight scale based on pre-processed target clearance.
1147 // Strong tracking in tight corridors, fading to zero in open space so the
1148 // smoothness residual can straighten the trajectory without fighting the
1149 // pre-processed path.
1150 std::vector<double> tracking_scales(N, 1.0);
1153 {
1154 for (int i = 0; i < N; ++i)
1155 {
1156 double d = 0.0;
1157 costmap_wrapper(&traj_targets[i].x,
1158 &traj_targets[i].y,
1159 &traj_targets[i].theta,
1160 &d);
1161 tracking_scales[i] = trackingScaleFromClearance(
1162 d,
1165 }
1166 }
1167
1168 // Freeze interior waypoints whose pre-processed clearance is below the
1169 // threshold. These points are already in a safe corridor centre and
1170 // should not be moved by the smoother.
1171 std::vector<bool> freeze_pose(N, false);
1172 if (opts.use_obstacle_freeze)
1173 {
1174 for (int i = 1; i <= N - 2; ++i)
1175 {
1176 double d = 0.0;
1177 costmap_wrapper(&traj_targets[i].x,
1178 &traj_targets[i].y,
1179 &traj_targets[i].theta,
1180 &d);
1181 freeze_pose[i] = (d <= opts.obstacle_freeze_threshold);
1182 }
1183
1184 std::string frozen_indices;
1185 for (int i = 1; i <= N - 2; ++i)
1186 {
1187 if (freeze_pose[i])
1188 {
1189 if (!frozen_indices.empty())
1190 frozen_indices += ", ";
1191 frozen_indices += std::to_string(i);
1192 }
1193 }
1194 ARMARX_INFO << "[Pass 1] Frozen waypoints (clearance <= "
1195 << opts.obstacle_freeze_threshold << " mm): ["
1196 << (frozen_indices.empty() ? "none" : frozen_indices) << "]";
1197 }
1198
1199 // ==================== PASS 1: Optimize poses (x, y, theta) ====================
1200 {
1201 ceres::Problem problem;
1202 for (int i = 0; i < N; ++i)
1203 problem.AddParameterBlock(&traj[i].x, 4);
1204
1205 // Hold start and end fully constant.
1206 problem.SetParameterBlockConstant(&traj.front().x);
1207 problem.SetParameterBlockConstant(&traj.back().x);
1208
1209 // In Pass 1 only x, y, theta are optimized; keep v constant on interior waypoints.
1210 // Waypoints close to obstacles are held fully constant.
1211 for (int i = 1; i < N - 1; ++i)
1212 {
1213 if (freeze_pose[i])
1214 {
1215 problem.SetParameterBlockConstant(&traj[i].x);
1216 }
1217 else
1218 {
1219 std::vector<int> constant_v = {3};
1220 SET_CONSTRAINT(problem, &traj[i].x, new SubsetConstraint(4, constant_v));
1221 }
1222 }
1223
1224 // 1) Pose smoothness
1225 for (int i = 1; i <= N - 2; ++i)
1226 {
1227 problem.AddResidualBlock(
1228 new ceres::AutoDiffCostFunction<PoseSmoothResidual, 3, 4, 4, 4>(
1230 nullptr,
1231 &traj[i - 1].x,
1232 &traj[i].x,
1233 &traj[i + 1].x);
1234 }
1235
1236 // Start and end rotation smoothness
1237 problem.AddResidualBlock(
1238 new ceres::AutoDiffCostFunction<StartRotationResidual, 1, 4, 4>(
1240 nullptr,
1241 &traj[0].x,
1242 &traj[1].x);
1243 problem.AddResidualBlock(
1244 new ceres::AutoDiffCostFunction<StartRotationResidual, 1, 4, 4>(
1246 nullptr,
1247 &traj[N - 2].x,
1248 &traj[N - 1].x);
1249
1250 // 2) Optional jerk residuals
1251 if (opts.use_jerk && N >= 4)
1252 {
1253 for (int i = 2; i <= N - 2; ++i)
1254 {
1255 problem.AddResidualBlock(
1256 new ceres::AutoDiffCostFunction<PoseJerkResidual, 3, 4, 4, 4, 4>(
1257 new PoseJerkResidual(opts.w_pose_jerk)),
1258 nullptr,
1259 &traj[i - 2].x,
1260 &traj[i - 1].x,
1261 &traj[i].x,
1262 &traj[i + 1].x);
1263 }
1264 }
1265
1266 // 3) Robot smoothness residuals (optional)
1267 if (opts.use_robot_smooth)
1268 {
1269 for (int i = 1; i <= N - 2; ++i)
1270 {
1271 problem.AddResidualBlock(
1272 new ceres::AutoDiffCostFunction<RobotSmoothResidual, 3, 4, 4, 4>(
1273 new RobotSmoothResidual(opts.w_robot_smooth, -0.5, 0.0)),
1274 nullptr,
1275 &traj[i - 1].x,
1276 &traj[i].x,
1277 &traj[i + 1].x);
1278 }
1279 }
1280
1281 // 4) Spacing residuals
1282 double d_avg = computeAverageSpacing(traj);
1283 if (d_avg <= 1e-9)
1284 d_avg = 0.1;
1285 for (int i = 0; i < N - 1; ++i)
1286 {
1287 if (opts.use_soft_spacing)
1288 {
1289 problem.AddResidualBlock(
1290 new ceres::AutoDiffCostFunction<SoftSpacingResidual, 1, 4, 4>(
1291 new SoftSpacingResidual(d_avg,
1292 opts.w_spacing_linear,
1295 nullptr,
1296 &traj[i].x,
1297 &traj[i + 1].x);
1298 }
1299 else
1300 {
1301 problem.AddResidualBlock(
1302 new ceres::AutoDiffCostFunction<SpacingResidual, 1, 4, 4>(
1303 new SpacingResidual(d_avg, opts.w_spacing)),
1304 nullptr,
1305 &traj[i].x,
1306 &traj[i + 1].x);
1307 }
1308 }
1309
1310 // 5) Obstacle residuals (waypoint + midpoint)
1311 if (opts.use_obs)
1312 {
1313 for (int i = 0; i < N; ++i)
1314 {
1315 problem.AddResidualBlock(
1316 new ceres::AutoDiffCostFunction<ObstacleResidual, 1, 4>(
1317 new ObstacleResidual(costmap_wrapper,
1318 opts.w_obs,
1319 opts.obs_max_distance,
1320 opts.clearance,
1321 opts.obs_barrier_eps_mm,
1323 72,
1326 nullptr,
1327 &traj[i].x);
1328 }
1329 for (int i = 0; i < N - 1; ++i)
1330 {
1331 problem.AddResidualBlock(
1332 new ceres::AutoDiffCostFunction<MidpointObstacleResidual, 1, 4, 4>(
1333 new MidpointObstacleResidual(costmap_wrapper,
1334 opts.w_obs,
1335 opts.obs_max_distance,
1336 opts.clearance,
1337 opts.obs_barrier_eps_mm,
1339 72,
1342 nullptr,
1343 &traj[i].x,
1344 &traj[i + 1].x);
1345 }
1346 }
1347
1348 // 5b) Theta tracking: pull interior waypoints toward pre-processed theta
1349 for (int i = 1; i <= N - 2; ++i)
1350 {
1351 problem.AddResidualBlock(
1352 new ceres::AutoDiffCostFunction<ThetaTrackingResidual, 1, 4>(
1353 new ThetaTrackingResidual(opts.w_theta_track * tracking_scales[i],
1354 traj_targets[i].theta)),
1355 nullptr,
1356 &traj[i].x);
1357 }
1358
1359 // 6) Tracking residuals to original (optional)
1360 if (opts.use_tracking)
1361 {
1362 if (static_cast<int>(traj_targets.size()) != N)
1363 {
1364 ARMARX_INFO << "[Pass 1] traj_targets size mismatch; skipping tracking.\n";
1365 }
1366 else
1367 {
1368 for (int i = 0; i < N; ++i)
1369 {
1370 double wx = opts.w_track * tracking_scales[i];
1371 double wy = opts.w_track * tracking_scales[i];
1372 double wth = opts.w_track * 300.0 * tracking_scales[i];
1373 double wv = opts.w_track * 0.2 * tracking_scales[i];
1374 problem.AddResidualBlock(
1375 new ceres::AutoDiffCostFunction<TrackingResidual, 4, 4>(
1376 new TrackingResidual(traj_targets[i].x,
1377 traj_targets[i].y,
1378 traj_targets[i].theta,
1379 traj_targets[i].v,
1380 wx,
1381 wy,
1382 wth,
1383 wv)),
1384 nullptr,
1385 &traj[i].x);
1386 }
1387 }
1388 }
1389
1390 // 7) Robot-pose proximity to pre-processed trajectory (optional)
1391 if (opts.use_robot_pose_proximity)
1392 {
1393 if (static_cast<int>(traj_targets.size()) != N)
1394 {
1395 ARMARX_INFO << "[Pass 1] traj_targets size mismatch; skipping robot-pose proximity.\n";
1396 }
1397 else
1398 {
1399 for (int i = 1; i <= N - 2; ++i)
1400 {
1401 const double cth = traj_targets[i].theta;
1402 const double rx0 = traj_targets[i].x + std::cos(cth) * (-0.5);
1403 const double ry0 = traj_targets[i].y + std::sin(cth) * (-0.5);
1404
1405 problem.AddResidualBlock(
1406 new ceres::AutoDiffCostFunction<RobotPoseProximityResidual, 1, 4>(
1408 ry0,
1411 * tracking_scales[i],
1413 nullptr,
1414 &traj[i].x);
1415 }
1416 }
1417 }
1418
1419 // Solver options
1420 ceres::Solver::Options options;
1421 options.max_num_iterations = opts.max_iterations;
1422 options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
1423 options.minimizer_progress_to_stdout = false;
1424 options.num_threads = opts.num_threads;
1425 options.function_tolerance = 1e-6;
1426 options.parameter_tolerance = 1e-8;
1427 options.trust_region_strategy_type = ceres::LEVENBERG_MARQUARDT;
1428 options.initial_trust_region_radius = opts.initial_trust_region_radius;
1429 options.max_trust_region_radius = opts.max_trust_region_radius;
1430 options.min_trust_region_radius = opts.min_trust_region_radius;
1431 options.min_relative_decrease = 1e-3;
1432
1433 // Pre-solve validation
1434 bool valid = true;
1435 for (int i = 0; i < N; ++i)
1436 {
1437 if (!std::isfinite(traj[i].x) || !std::isfinite(traj[i].y) ||
1438 !std::isfinite(traj[i].theta) || !std::isfinite(traj[i].v))
1439 {
1440 ARMARX_WARNING << "[Pass 1] NaN/Inf in traj[" << i
1441 << "] before solve: x=" << traj[i].x << " y=" << traj[i].y
1442 << " theta=" << traj[i].theta << " v=" << traj[i].v;
1443 valid = false;
1444 }
1445 }
1446 if (!valid)
1447 {
1448 ARMARX_WARNING << "[Pass 1] Pre-solve validation FAILED. Aborting.";
1449 return;
1450 }
1451
1452 ARMARX_INFO << "[Pass 1] Pre-solve residual breakdown:";
1453 logResidualBreakdown(traj, costmap_wrapper, traj_targets, tracking_scales, opts);
1454
1455 ceres::Solver::Summary summary;
1456 ceres::Solve(options, &problem, &summary);
1457 normalizeTrajectoryAngles(traj);
1458
1459 ARMARX_INFO << "[Pass 1] Post-solve residual breakdown:";
1460 logResidualBreakdown(traj, costmap_wrapper, traj_targets, tracking_scales, opts);
1461
1462 bool postSolveValid = true;
1463 for (int i = 0; i < N; ++i)
1464 {
1465 if (!std::isfinite(traj[i].x) || !std::isfinite(traj[i].y) ||
1466 !std::isfinite(traj[i].theta) || !std::isfinite(traj[i].v))
1467 {
1468 ARMARX_WARNING << "[Pass 1] NaN/Inf in traj[" << i
1469 << "] AFTER solve. Reverting to pre-processed trajectory.";
1470 postSolveValid = false;
1471 }
1472 }
1473 if (!postSolveValid)
1474 {
1475 for (int i = 0; i < N; ++i)
1476 traj[i] = traj_targets[i];
1477 ARMARX_WARNING << "[Pass 1] Trajectory reverted to pre-processed safe path.";
1478 return;
1479 }
1480
1481 ARMARX_INFO << summary.FullReport();
1482 ARMARX_INFO << "[Pass 1] Done. Final cost: " << summary.final_cost
1483 << " iterations: " << summary.num_successful_steps << "\n";
1484 }
1485
1486 // ==================== PASS 2: Optimize velocities (v) ====================
1487 {
1488 // Auto-detect nominal cruise speed from the first interior waypoint
1489 double v_nominal = (N >= 3) ? traj[1].v : traj.front().v;
1490
1491 // Set start velocity boundary
1492 traj.front().v = opts.start_velocity;
1493
1494 // Pre-query obstacle danger at each optimized pose
1495 std::vector<double> dangers(N);
1496 for (int i = 0; i < N; ++i)
1497 {
1498 double d = 0.0;
1499 costmap_wrapper(&traj[i].x, &traj[i].y, &traj[i].theta, &d);
1500 dangers[i] = std::clamp(
1501 (opts.slow_down_distance - d) / opts.slow_down_distance, 0.0, 1.0);
1502 }
1503
1504 ceres::Problem problem;
1505 for (int i = 0; i < N; ++i)
1506 {
1507 problem.AddParameterBlock(&traj[i].x, 4);
1508 // Hold x, y, theta constant; only v (index 3) is free
1509 std::vector<int> constant_indices = {0, 1, 2};
1511 problem, &traj[i].x, new SubsetConstraint(4, constant_indices));
1512 }
1513
1514 // Hold start and end fully constant (including v)
1515 problem.SetParameterBlockConstant(&traj.front().x);
1516 problem.SetParameterBlockConstant(&traj.back().x);
1517
1518 // 1) Accel/decel limit residuals (per-segment)
1519 for (int i = 0; i < N - 1; ++i)
1520 {
1521 problem.AddResidualBlock(
1522 new ceres::AutoDiffCostFunction<AccelDecelLimitResidual, 1, 4, 4>(
1525 opts.w_accel_decel_limit)),
1526 nullptr,
1527 &traj[i].x,
1528 &traj[i + 1].x);
1529 }
1530
1531 // 2) Nominal velocity residual (interior waypoints)
1532 for (int i = 1; i < N - 1; ++i)
1533 {
1534 problem.AddResidualBlock(
1535 new ceres::AutoDiffCostFunction<NominalVelocityResidual, 1, 4>(
1536 new NominalVelocityResidual(v_nominal, opts.w_nominal_velocity)),
1537 nullptr,
1538 &traj[i].x);
1539 }
1540
1541 // 3) Velocity proximity residual (interior waypoints)
1542 for (int i = 1; i < N - 1; ++i)
1543 {
1544 problem.AddResidualBlock(
1545 new ceres::AutoDiffCostFunction<VelocityProximityResidual, 1, 4>(
1546 new VelocityProximityResidual(dangers[i],
1548 opts.w_velocity_proximity)),
1549 nullptr,
1550 &traj[i].x);
1551 }
1552
1553 // Solver options (velocity pass)
1554 ceres::Solver::Options options;
1555 options.max_num_iterations = opts.max_iterations;
1556 options.linear_solver_type = ceres::SPARSE_NORMAL_CHOLESKY;
1557 options.minimizer_progress_to_stdout = false;
1558 options.num_threads = opts.num_threads;
1559 options.function_tolerance = 1e-6;
1560 options.parameter_tolerance = 1e-8;
1561 options.trust_region_strategy_type = ceres::LEVENBERG_MARQUARDT;
1562 options.initial_trust_region_radius = opts.initial_trust_region_radius;
1563 options.max_trust_region_radius = opts.max_trust_region_radius;
1564 options.min_trust_region_radius = opts.min_trust_region_radius;
1565 options.min_relative_decrease = 1e-3;
1566
1567 ceres::Solver::Summary summary;
1568 ceres::Solve(options, &problem, &summary);
1569
1570 ARMARX_INFO << summary.FullReport();
1571 ARMARX_INFO << "[Pass 2] Done. Final cost: " << summary.final_cost
1572 << " iterations: " << summary.num_successful_steps << "\n";
1573 }
1574
1575 normalizeTrajectoryAngles(traj);
1576 }
1577
1578} // namespace armarx::navigation::algorithms::orientation_aware::smoothing
#define M_PI
Definition MathTools.h:17
constexpr T c
Brief description of class targets.
Definition targets.h:39
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:181
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:193
double s(double t, double s0, double v0, double a0, double j)
Definition CtrlUtil.h:33
double v(double t, double v0, double a0, double j)
Definition CtrlUtil.h:39
void optimizeTrajectoryCeres(std::vector< CenterPoint > &traj, const Costmap3DWrapper &costmap_wrapper, const std::vector< CenterPoint > &traj_targets, const io::SmoothingParams &opts)
Definition residuals.h:1109
double smoothstep(double edge0, double edge1, double x)
Definition residuals.h:70
T smoothMin(const std::vector< T > &values, double alpha)
Definition residuals.h:118
double trackingScaleFromClearance(double clearance, double fade_start_clearance, double fade_end_clearance)
Definition residuals.h:81
This file offers overloads of toIce() and fromIce() functions for STL container types.
bool isfinite(const std::vector< T, Ts... > &v)
Definition algorithm.h:366
ceres::SubsetParameterization SubsetConstraint
This file is part of ArmarX.
Definition residuals.h:22
#define SET_CONSTRAINT(problem, param, constraint)
Definition residuals.h:23
AccelDecelLimitResidual(double max_accel, double max_decel, double weight)
Definition residuals.h:238
bool operator()(const T *const c_i, const T *const c_ip1, T *residual) const
Definition residuals.h:245
MidpointObstacleResidual(const Costmap3DWrapper &wrapper, double weight, double max_dist, double clearance, double barrier_eps, bool use_fuzzy_orientation=true, int num_orientations=72, int fuzzy_window_bins=2, double fuzzy_alpha=5.0)
Definition residuals.h:459
bool operator()(const T *const c_i, const T *const c_ip1, T *residual) const
Definition residuals.h:482
ObstacleResidual(const Costmap3DWrapper &wrapper, double weight, double max_dist, double clearance, double barrier_eps, bool use_fuzzy_orientation=true, int num_orientations=72, int fuzzy_window_bins=2, double fuzzy_alpha=5.0)
Definition residuals.h:346
bool operator()(const T *const c_m2, const T *const c_m1, const T *const c_i, const T *const c_p1, T *residual) const
Definition residuals.h:317
PoseSmoothResidual(double pos_weight=1.0, double orientation_weight=1.0)
Definition residuals.h:170
bool operator()(const T *const c_prev, const T *const c_i, const T *const c_next, T *residual) const
Definition residuals.h:177
RobotPoseProximityResidual(double rx0, double ry0, double threshold_mm, double weight, double eps_mm)
Definition residuals.h:664
bool operator()(const T *const c_prev, const T *const c_i, const T *const c_next, T *residual) const
Definition residuals.h:638
RobotSmoothResidual(double weight=1.0, double dx=-0.5, double dy=0.0)
Definition residuals.h:620
void centerToRobotTemplated(const T *const c, T &rx, T &ry, T &rtheta) const
Definition residuals.h:627
bool operator()(const T *const c_i, const T *const c_ip1, T *residual) const
Definition residuals.h:751
SoftSpacingResidual(double d_avg, double linear_weight, double quadratic_weight, double relative_deviation_threshold)
Definition residuals.h:738
bool operator()(const T *const c_i, const T *const c_ip1, T *residual) const
Definition residuals.h:718
bool operator()(const T *const theta0, const T *const theta1, T *residuals) const
Definition residuals.h:204
TrackingResidual(double xr, double yr, double thetar, double vr, double wx, double wy, double wth, double wv)
Definition residuals.h:565
VelocityProximityResidual(double danger, double min_velocity, double weight)
Definition residuals.h:287