OrientationOptimizer.cpp
Go to the documentation of this file.
2
3#include <math.h>
4
5#include <algorithm>
6#include <cmath>
7#include <cstddef>
8#include <vector>
9
10#include <range/v3/algorithm/for_each.hpp>
11#include <range/v3/range/conversion.hpp>
12#include <range/v3/view/transform.hpp>
13#include <range/v3/view/zip.hpp>
14
15#include <SimoxUtility/math/convert/mat4f_to_xyyaw.h>
16#include <SimoxUtility/math/convert/rpy_to_mat3f.h>
17#include <SimoxUtility/math/periodic/periodic_clamp.h>
18#include <VirtualRobot/Random.h>
19
22
25
26#include <ceres/ceres.h>
27#include <ceres/cost_function.h>
28
30{
32 const Params& params) :
33 trajectory(trajectory), params(params)
34 {
35 }
36
37 inline double
38 angleDiff(const double a, const double b)
39 {
40 double d = a - b;
41
42 // Wrap into (−π, π]
43 while (d <= -M_PI)
44 {
45 d += 2 * M_PI;
46 }
47 while (d > M_PI)
48 {
49 d -= 2 * M_PI;
50 }
51
52 return d;
53 }
54
55 namespace
56 {
57 /**
58 * @brief Passes of the smoothing filter below.
59 *
60 * A binomial filter is variation-diminishing, so each pass can only remove local extrema
61 * from `dyaw/ds`, never add them. What matters is that derivative, not the heading
62 * itself: the achievable speed is `maxVel.angular / max|dyaw/ds|`. Measured on
63 * `wide_passage_diagonal`, whose path tangent has 3 extrema of its own:
64 *
65 * passes dyaw/ds extrema speed cap at 0.5 rad/s max heading shift
66 * 0 14 378 mm/s --
67 * 8 6 787 mm/s 8.0 deg
68 * 32 3 996 mm/s 9.6 deg
69 * 64 3 1060 mm/s 9.9 deg
70 *
71 * 32 is where the profile reaches the path's own 3 extrema and the turn rate stops
72 * capping the speed below the 1000 mm/s linear limit -- i.e. where the angular and
73 * linear limits are matched rather than one throttling the other. Beyond that the gain
74 * flattens while the headings keep drifting from the optimized ones.
75 */
76 constexpr std::size_t orientationSmoothingPasses = 32;
77
78 /**
79 * @brief Remove the oscillation the optimizer leaves in the heading profile.
80 *
81 * The solve minimises a sum of pairwise smoothness and prior terms, and stops at a loose
82 * `function_tolerance`, which leaves a heading whose *rate* reverses far more often than
83 * the path bends: 14 reversals of `dyaw/ds` against the path tangent's 3 on
84 * `wide_passage_diagonal`.
85 *
86 * That is not cosmetic. A `GlobalTrajectory` stores one *linear* speed per waypoint, so
87 * the turn rate it implies is `v * dyaw/ds`; wherever that reaches the platform's
88 * angular limit, it and not the obstacle-aware limit caps the speed. A few wiggly
89 * waypoints therefore slow the whole traverse.
90 *
91 * Ideally the optimizer would not produce this and the pass would not exist. It does, so
92 * this is a post-processing step: a binomial [1/4, 1/2, 1/4] filter on the interior
93 * headings with start and goal pinned, applied in the *increment* domain so a wrap
94 * across +-pi averages correctly.
95 */
96 /// Largest |dyaw| per millimetre over an already-unwrapped profile.
97 double
98 turnRateOf(const std::vector<double>& unwrapped, const std::vector<double>& segmentLengths)
99 {
100 double worst = 0.0;
101
102 for (std::size_t i = 0; i + 1 < unwrapped.size(); i++)
103 {
104 const double length = segmentLengths.at(i);
105
106 if (length > 1e-3)
107 {
108 worst =
109 std::max(worst, std::abs(unwrapped.at(i + 1) - unwrapped.at(i)) / length);
110 }
111 }
112
113 return worst;
114 }
115
116 void
117 smoothOrientations(std::vector<double>& orientations,
118 const std::vector<double>& segmentLengths,
119 const double maxTurnRate)
120 {
121 if (orientations.size() < 3)
122 {
123 return;
124 }
125
126 // Accumulate the shortest-path increments, so the values are continuous even where
127 // the raw angles wrap.
128 std::vector<double> unwrapped(orientations.size());
129 unwrapped.front() = orientations.front();
130 for (std::size_t i = 1; i < orientations.size(); i++)
131 {
132 unwrapped.at(i) =
133 unwrapped.at(i - 1) + angleDiff(orientations.at(i), orientations.at(i - 1));
134 }
135
136 const double start = unwrapped.front();
137 const double goal = unwrapped.back();
138
139 const auto runPasses = [&unwrapped](const std::size_t passes)
140 {
141 for (std::size_t pass = 0; pass < passes; pass++)
142 {
143 const std::vector<double> previous = unwrapped;
144
145 for (std::size_t i = 1; i + 1 < unwrapped.size(); i++)
146 {
147 unwrapped.at(i) = 0.25 * previous.at(i - 1) + 0.5 * previous.at(i) +
148 0.25 * previous.at(i + 1);
149 }
150 }
151 };
152
153 runPasses(orientationSmoothingPasses);
154
155 // Then keep going until the profile is one the base could actually hold, rather than
156 // stopping at a fixed pass count that has no idea how much rotation it is spreading
157 // or how much path it has to spread it over.
158 //
159 // Diffusion with the endpoints pinned converges towards a constant increment per
160 // sample, i.e. the rotation spread over the whole path -- so this terminates whenever
161 // the path is long enough to absorb the turn at all, and the cap catches the case
162 // where it is not. Spreading costs heading accuracy against the movement direction,
163 // which is why it stops at the limit instead of smoothing to convergence.
164 //
165 // Checked in blocks: the rate moves slowly and evaluating it every pass would cost
166 // more than the passes.
167 constexpr std::size_t passBlock = 64;
168 constexpr std::size_t maxAdditionalPasses = 200000;
169
170 std::size_t additional = 0;
171 while (maxTurnRate > 0.0 and turnRateOf(unwrapped, segmentLengths) > maxTurnRate and
172 additional < maxAdditionalPasses)
173 {
174 runPasses(passBlock);
175 additional += passBlock;
176 }
177
178 const double achieved = turnRateOf(unwrapped, segmentLengths);
179
180 if (maxTurnRate > 0.0 and achieved > maxTurnRate)
181 {
182 // The path is too short for the rotation it is being asked to carry. Reported
183 // rather than hidden: downstream this becomes a speed cap, and on the robot it
184 // looked like the controller misbehaving.
185 ARMARX_WARNING << "[nav-guard] orientation-unturnable: the heading profile still "
186 << "turns " << achieved << " rad/mm after " << additional
187 << " smoothing passes, against a limit of " << maxTurnRate
188 << ". The path is too short for the rotation it has to carry; the "
189 << "reparametrization will have to slow down for it.";
190 }
191 else if (additional > 0)
192 {
193 ARMARX_VERBOSE << "Orientation profile needed " << additional
194 << " extra smoothing passes to reach " << achieved << " rad/mm.";
195 }
196
197 // The endpoints are commanded, not optimized: they must survive untouched.
198 unwrapped.front() = start;
199 unwrapped.back() = goal;
200
201 orientations = unwrapped;
202 }
203 } // namespace
204
207 {
208 namespace r = ::ranges;
209 namespace rv = ::ranges::views;
210
211 ARMARX_IMPORTANT << VAROUT(params.startGoalDistanceThreshold);
212
213 const auto toYaw = [](const core::GlobalTrajectoryPoint& pt) -> double
214 { return simox::math::mat4f_to_xyyaw(pt.waypoint.pose.matrix()).z(); };
215
216 //orientations vector contains variable values that are changed during optimization. these
217 // values are changed to fit the given optimization criteria as good as possible
218 std::vector<double> orientations =
219 trajectory.points() | ranges::views::transform(toYaw) | ranges::to_vector;
220
221 //the optimization tries to stay close to the movement direction, thus the original
222 // orientation is copied (it is expected that original orientation values are oriented
223 // so that they face along the trajectory)
224 const std::vector<double> inMovementDir = orientations;
225
226 //calculate angle increment necessary for initial equidistant scattering of orientations
227 const double startOrientation = orientations.front();
228 const double goalOrientation = orientations.back();
229
230 ARMARX_DEBUG << VAROUT(startOrientation);
231 ARMARX_DEBUG << VAROUT(goalOrientation);
232
233 const std::size_t nOrientationStartGoalInfluence = std::ceil(
234 (orientations.size() - 1) * params.startGoalDistanceThreshold / trajectory.length());
235 ARMARX_DEBUG << VAROUT(nOrientationStartGoalInfluence);
236
237 const double signedAngleDiff = angleDiff(goalOrientation, startOrientation);
238
239 ARMARX_DEBUG << VAROUT(signedAngleDiff);
240
241 const double signedAngleDiffDesired = [&]() -> double
242 {
243 switch (params.predefinedRotationDirection)
244 {
246 ARMARX_VERBOSE << "Clockwise";
247 if (signedAngleDiff <= 0)
248 {
249 return signedAngleDiff;
250 }
251 return -2 * M_PI + signedAngleDiff;
252
254 ARMARX_VERBOSE << "CounterClockwise";
255 if (signedAngleDiff >= 0)
256 {
257 return signedAngleDiff;
258 }
259 return 2 * M_PI + signedAngleDiff;
260 default:
261 ARMARX_VERBOSE << "Unspecified";
262 return signedAngleDiff;
263 }
264 }();
265
266
267 if (orientations.size() < 2 * nOrientationStartGoalInfluence)
268 {
269 // we just interpolate between start and end
270
271 const double angleIncrement = signedAngleDiffDesired / (orientations.size() - 1);
272
273 //equidistant orientations are used as starting value for the optimization of the
274 // orientations in the first iteration
275 const std::vector<double> equidistantOrientations = [&]
276 {
277 std::vector<double> vec{orientations.front()};
278 for (std::size_t i = 1; i < orientations.size() - 1; i++)
279 {
280 vec.push_back(startOrientation + angleIncrement * i);
281 }
282 vec.push_back(orientations.back());
283 return vec;
284 }();
285 orientations = equidistantOrientations;
286 }
287 else
288 {
289 // here, we ignore the rotation direction
290
291 // we obtain the first part in which we interpolate between the start and the inMovementDir(nOrientationStartGoalInfluence)
292 {
293 const double signedAngleDiff = angleDiff(
294 inMovementDir.at(nOrientationStartGoalInfluence - 1), startOrientation);
295
296 const double angleIncrement = signedAngleDiff / (nOrientationStartGoalInfluence);
297
298 for (std::size_t i = 1; i < nOrientationStartGoalInfluence; i++)
299 {
300 orientations.at(i) = startOrientation + angleIncrement * i;
301 }
302 }
303
304 // the intermediate points are initialized with inMovementDir
305 {
306 for (std::size_t i = nOrientationStartGoalInfluence;
307 i < orientations.size() - nOrientationStartGoalInfluence;
308 i++)
309 {
310 orientations.at(i) = inMovementDir.at(i);
311 }
312 }
313
314 // we obtain the last part in which we interpolate between the end and the inMovementDir(n-nOrientationStartGoalInfluence)
315 {
316 const double signedAngleDiff = angleDiff(
317 goalOrientation,
318 inMovementDir.at(orientations.size() - nOrientationStartGoalInfluence));
319
320 const double angleIncrement = signedAngleDiff / (nOrientationStartGoalInfluence);
321
322 for (std::size_t i = 1; i < nOrientationStartGoalInfluence; i++)
323 {
324 orientations.at(orientations.size() - nOrientationStartGoalInfluence + i) =
325 goalOrientation - angleIncrement * (nOrientationStartGoalInfluence - i);
326 }
327 }
328 }
329
330
331 ARMARX_DEBUG << "Equidistant";
332 for (const auto& ori : orientations)
333 {
334 ARMARX_DEBUG << ori;
335 }
336
337 /* //COMMENT IN FOR TEST CASE PLOTTING
338 std::vector<double> initial = orientations;
339 std::vector<double> prior = inMovementDir; */
340
341 const float movementDirWeightStep =
342 (params.movementDirWeightEnd - params.movementDirWeightStart) / (params.iterations - 1);
343
344 //iteratively optimize the orientations with increasing movementDirWeight, thus at first ignoring
345 // the criteria to look in the movement direction and slowly considering this more and more
346 // with each iteration
347 // for (int it = 0; it < params.iterations; it++)
348 {
349 const float movementDirWeight = params.movementDirWeightEnd;
350 // params.iterations == 1 ? params.movementDirWeightEnd
351 // : params.movementDirWeightStart + it * movementDirWeightStep;
352
353 // There used to be a +-0.05 rad perturbation of every interior orientation here, to
354 // break the symmetry of a perfectly aligned set of waypoints "which the minimizer
355 // would otherwise not move off".
356 //
357 // It was not the minimizer. `periodicDiff` used to be `acos` of a dot product, whose
358 // derivative is infinite where the two angles agree, so the analytic jacobian guarded
359 // the singularity by returning *zero* there -- suppressing the gradient at exactly
360 // the aligned configurations the noise was added to escape. With the `atan2`
361 // formulation the derivative is a clean +-1 everywhere and there is nothing to escape.
362 //
363 // Measured on a straight 3184 mm run, with and without the perturbation: identical
364 // results to six decimal places for every goal orientation tried, and the aligned
365 // case now converges at iteration 0 with a cost of exactly zero instead of spending
366 // three iterations grinding the injected noise back out of a correct answer.
367 //
368 // Removing it also removes the last source of run-to-run variation: the solve is now
369 // deterministic because there is no randomness in it, rather than because a PRNG was
370 // pinned to a fixed seed.
371
372 //interpolate orientation in walking direction and current orientation at walkingDirAlpha.
373 // interpolated values are used as starting values for next iteration
374 for (std::size_t i = 1; i < orientations.size() - 1; i++)
375 {
376 const float walkingDirAlpha = 0.2;
377
378 const Eigen::Vector2f vMovementDir =
379 walkingDirAlpha *
380 (Eigen::Rotation2Df(inMovementDir.at(i)) * Eigen::Vector2f::UnitX());
381
382 const Eigen::Vector2f vStartGoal =
383 (1.0 - walkingDirAlpha) *
384 (Eigen::Rotation2Df(orientations.at(i)) * Eigen::Vector2f::UnitX());
385
386 const Eigen::Vector2f vCombined = [&]() -> Eigen::Vector2f
387 {
388 if (movementDirWeight > 0) // should rotate into movement direction?
389 {
390 return vMovementDir + vStartGoal;
391 }
392
393 return vStartGoal;
394 }();
395
396 // FIXME reconsider this
397 // orientations.at(i) = std::atan2(vCombined.y(), vCombined.x());
398 //
399 // With the perturbation gone this loop has no effect: `vCombined` is computed and
400 // discarded, and the line above is the only thing that ever used it. Left in
401 // place rather than deleted because it is someone's unfinished intent, not my
402 // dead code -- but it is dead, and it is the reason `movementDirWeight` and
403 // `walkingDirAlpha` are computed for nothing.
404 (void)vCombined;
405 }
406
407 /* //COMMENT IN FOR TEST CASE PLOTTING
408 initial = orientations;
409 prior = inMovementDir; */
410
411 const std::size_t nPoints = orientations.size();
412
413 ceres::Problem problem;
414
415 ARMARX_VERBOSE << orientations.size() - 2 << " orientations to optimize";
416
417
418 // Define Optimization Criteria
419 {
420 // TODO https://ceres-solver.googlesource.com/ceres-solver/+/master/examples/slam/pose_graph_2d/pose_graph_2d.cc
421 // ceres::LocalParameterization* angle_local_parameterization =
422 // ceres::AngleLocalParameterization::Create();
423
424 // keeps the new orientations somewhat close to the orientation in movement direction
425 if (movementDirWeight > 0.F)
426 {
427 ARMARX_VERBOSE << "Optimize: movement dir enabled.";
428 for (size_t i = 1; i < (orientations.size() - 1); i++)
429 {
430 float actualMovementDirWeight = movementDirWeight;
431 //the first and last four points should not consider it too much to look in the
432 // direction of the trajectory as the robot should not move too much when
433 // close to start or goal
434 if (i < 5)
435 {
436 actualMovementDirWeight *= i / 5.;
437 }
438 else if ((orientations.size() - i - 1) < 5)
439 {
440 actualMovementDirWeight *= (orientations.size() - i - 1) / 5.;
441 }
442
443 ceres::CostFunction* movementDirCostFunction =
444 new OrientationPriorCostFunctor(inMovementDir.at(i),
445 actualMovementDirWeight);
446
447 ARMARX_DEBUG << "Adding OrientationPriorCostFunctor to optimize " << i;
448 problem.AddResidualBlock(
449 movementDirCostFunction, nullptr, &orientations.at(i));
450 }
451 }
452
453 // smooth waypoint orientation, no start and goal nodes involved!
454 if (params.smoothnessWeight > 0.F)
455 {
456 ARMARX_VERBOSE << "Enabled SmoothOrientationCost";
457 for (size_t i = 2; i < (orientations.size() - 2); i++)
458 {
459 ceres::CostFunction* smoothCostFunction =
460 new SmoothOrientationCostFunctor(params.smoothnessWeight);
461
462 ARMARX_DEBUG << "Addding SmoothOrientationCostFunctor to optimize " << i - 1
463 << ", " << i << " and " << i + 1;
464 problem.AddResidualBlock(smoothCostFunction,
465 nullptr,
466 &orientations.at(i - 1),
467 &orientations.at(i),
468 &orientations.at(i + 1));
469 }
470 }
471
472 // within a certain range close to the start, the robot shouldn't change its orientation
473 if (params.priorStartWeight > 0.F)
474 {
475 ARMARX_DEBUG << "prior start enabled";
476 const auto connectedPointsInRangeStart =
477 trajectory.allConnectedPointsInRange(0, params.startGoalDistanceThreshold);
478
479 ARMARX_DEBUG << VAROUT(connectedPointsInRangeStart.size());
480
481 for (const size_t i : connectedPointsInRangeStart)
482 {
483 // skip the points that belong to the second half of the trajectory
484 if (i >= orientations.size() / 2)
485 {
486 continue;
487 }
488
489 ceres::CostFunction* priorCostFunction = new OrientationPriorCostFunctor(
490 inMovementDir.front(), params.priorStartWeight);
491
492 ARMARX_DEBUG << "Addding OrientationPriorCostFunctor(start) to optimize "
493 << i;
494 problem.AddResidualBlock(priorCostFunction, nullptr, &orientations.at(i));
495 }
496 }
497
498 // within a certain range close to the end, the robot shouldn't change its orientation
499 if (params.priorEndWeight > 0.F)
500 {
501 ARMARX_VERBOSE << "prior end enabled";
502
503 const auto connectedPointsInRangeGoal = trajectory.allConnectedPointsInRange(
504 trajectory.poses().size() - 1, params.startGoalDistanceThreshold);
505
506 ARMARX_DEBUG << VAROUT(connectedPointsInRangeGoal.size());
507
508 for (const size_t i : connectedPointsInRangeGoal)
509 {
510 // skip the points that belong to the first half of the trajectory
511 if (i < orientations.size() / 2)
512 {
513 continue;
514 }
515
516 ceres::CostFunction* priorCostFunction = new OrientationPriorCostFunctor(
517 inMovementDir.back(), params.priorEndWeight);
518
519 ARMARX_DEBUG << "Addding OrientationPriorCostFunctor to optimize " << i;
520 problem.AddResidualBlock(priorCostFunction, nullptr, &orientations.at(i));
521 }
522 }
523
524
525 // smooth waypoint orientation, involving start
526 if (params.smoothnessWeightStartGoal > 0.F and nPoints > 3)
527 {
528 ARMARX_VERBOSE << "Enabled SmoothOrientationFixedPreCost";
529
530 ceres::CostFunction* smoothCostFunction =
531 new SmoothOrientationFixedPreCostFunctor(orientations.front(),
532 params.smoothnessWeightStartGoal);
533
535 << "Addding SmoothOrientationFixedPreCostFunctor to optimize 1 and 2";
536 problem.AddResidualBlock(
537 smoothCostFunction, nullptr, &orientations.at(1), &orientations.at(2));
538 }
539
540 // smooth waypoint orientation, involving goal
541 if (params.smoothnessWeightStartGoal > 0.F and nPoints > 3)
542 {
543 ARMARX_VERBOSE << "Enabled SmoothOrientationFixedNextCost";
544
545 ceres::CostFunction* smoothCostFunction =
546 new SmoothOrientationFixedNextCostFunctor(orientations.back(),
547 params.smoothnessWeightStartGoal);
548
549 ARMARX_DEBUG << "Addding SmoothOrientationFixedPreCostFunctor to optimize "
550 << orientations.size() - 3 << " and " << orientations.size() - 2;
551 problem.AddResidualBlock(smoothCostFunction,
552 nullptr,
553 &orientations.at(orientations.size() - 3),
554 &orientations.at(orientations.size() - 2));
555 }
556 }
557
558
559 // Run the solver!
560 ceres::Solver::Options options;
561 options.linear_solver_type = ceres::DENSE_QR;
562 // Ceres 2.2 already defaults to 1, but planning has to be reproducible, so state it
563 // rather than inherit it from whichever Ceres happens to be linked.
564 options.num_threads = 1;
565 options.minimizer_progress_to_stdout = true;
566 options.max_num_iterations = 100;
567 options.function_tolerance = 0.01;
568 // options.check_gradients = false;
569 // options.trust_region_minimizer_iterations_to_dump = {0, 1, 2, 5, 10, 20};
570 // options.max_trust_region_radius = 0.05;
571 // options.initial_trust_region_radius = 0.01;
572
573 ceres::Solver::Summary summary;
574 Solve(options, &problem, &summary);
575
576 // orientations = inMovementDir;
577
578 ARMARX_VERBOSE << summary.FullReport() << "\n";
579
580 if (not summary.IsSolutionUsable())
581 {
582 ARMARX_ERROR << "Orientation optimization failed!";
583 // TODO write to file
584 }
585 }
586
587 // no matter what, we have to ensure that the start and goal are still as they were
588 // FIXME ARMARX_CHECK
589
590 // temporary fix: overwrite the values
591 orientations.front() = startOrientation;
592 orientations.back() = goalOrientation;
593
594 // The smoothing needs the geometry, not just the angles: how far a heading may turn is
595 // a question about distance travelled, and a profile that is fine over six metres is
596 // unexecutable over one.
597 const std::vector<double> segmentLengths = [this]
598 {
599 std::vector<double> lengths;
600 const auto& pts = trajectory.points();
601 lengths.reserve(pts.empty() ? 0 : pts.size() - 1);
602
603 for (std::size_t i = 0; i + 1 < pts.size(); i++)
604 {
605 lengths.push_back((pts.at(i + 1).waypoint.pose.translation() -
606 pts.at(i).waypoint.pose.translation())
607 .norm());
608 }
609
610 return lengths;
611 }();
612
613 smoothOrientations(orientations, segmentLengths, params.maxTurnRate);
614
615 ARMARX_DEBUG << "Final";
616 for (const auto& ori : orientations)
617 {
618 ARMARX_DEBUG << ori;
619 }
620
621 const auto applyOrientation = [](const auto& p) -> core::GlobalTrajectoryPoint
622 {
623 core::GlobalTrajectoryPoint tp = p.first;
624 const float yaw = p.second;
625
626 tp.waypoint.pose.linear() =
627 Eigen::AngleAxisf(yaw, Eigen::Vector3f::UnitZ()).toRotationMatrix();
628
629 return tp;
630 };
631
632 // TODO(fabian.reister): could also be in-place
633 const auto modifiedTrajectory = rv::zip(trajectory.points(), orientations) |
634 rv::transform(applyOrientation) | r::to_vector;
635
637 .trajectory = modifiedTrajectory,
638
639 /* //COMMENT IN FOR TEST CASE PLOTTING
640 .initial = initial,
641 .prior = prior */
642
643 };
644 }
645
646
647} // namespace armarx::navigation::global_planning::optimization
#define M_PI
Definition MathTools.h:17
#define VAROUT(x)
OrientationOptimizer(const core::GlobalTrajectory &trajectory, const Params &params)
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:188
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_DEBUG
The logging level for output that is only interesting while debugging.
Definition Logging.h:182
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
#define ARMARX_VERBOSE
The logging level for verbose information.
Definition Logging.h:185
double maxTurnRate(const core::GlobalTrajectory &trajectory, const GeometryLimits &limits)
The largest |dyaw| / segment length in the trajectory [rad/mm].
double angleDiff(const double a, const double b)