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