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