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