TrajectoryFollowingController.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <limits>
6#include <vector>
7
8#include <SimoxUtility/math/convert/mat4f_to_pos.h>
9#include <SimoxUtility/math/convert/mat4f_to_rpy.h>
10
14#include <ArmarXCore/interface/serialization/Eigen/Eigen_fdi.h>
15
18
23#include <armarx/navigation/trajectory_control/global/aron/TrajectoryFollowingControllerParams.aron.generated.h>
26
28{
29 // TrajectoryFollowingControllerParams
30
36
39 {
40 arondto::TrajectoryFollowingControllerParams dto;
41
44
45 return dto.toAron();
46 }
47
50 {
51 arondto::TrajectoryFollowingControllerParams dto;
52 dto.fromAron(dict);
53
56
57 return bo;
58 }
59
60 // TrajectoryFollowingController
61
63 params(params),
64 pidPos(params.pidPos.Kp,
65 params.pidPos.Ki,
66 params.pidPos.Kd,
67 std::numeric_limits<double>::max(),
68 std::numeric_limits<double>::max(),
69 false,
70 std::vector<bool>{false, false, false}),
71 pidPosTarget(params.pidPos.Kp,
72 params.pidPos.Ki,
73 params.pidPos.Kd,
74 std::numeric_limits<double>::max(),
75 std::numeric_limits<double>::max(),
76 false,
77 std::vector<bool>{false, false, false}),
78 pidOri(params.pidOri.Kp,
79 params.pidOri.Ki,
80 params.pidOri.Kd,
81 std::numeric_limits<double>::max(),
82 std::numeric_limits<double>::max(),
83 false,
84 std::vector<bool>{true, true, true}),
85 pidOriTarget(params.pidOri.Kp,
86 params.pidOri.Ki,
87 params.pidOri.Kd,
88 std::numeric_limits<double>::max(),
89 std::numeric_limits<double>::max(),
90 false,
91 std::vector<bool>{true, true, true})
92 {
93 ARMARX_IMPORTANT << "Trajectory following controller params: "
94 << VAROUT(params.limits.linear) << ", " << VAROUT(params.limits.angular);
95 }
96
99 {
100 // Zero is a legitimate limit -- the safety guard produces it -- and so is a non-finite
101 // twist arriving from a non-finite pose. Neither may throw: this runs in the 100 Hz
102 // `additionalTask()`, and one escaping exception ends that thread permanently while the
103 // RT loop keeps executing the twist it was last handed.
104 if (params.limits.linear <= 0 or params.limits.angular <= 0 or
105 not std::isfinite(params.limits.linear) or not std::isfinite(params.limits.angular) or
106 not twist.linear.allFinite() or not twist.angular.allFinite())
107 {
108 return core::Twist{.linear = Eigen::Vector3f::Zero(),
109 .angular = Eigen::Vector3f::Zero()};
110 }
111
112 const core::AngularVelocity angularLimit = Eigen::Vector3f::Ones() * params.limits.angular;
113
114 // for all entries, scale should be less than 1
115 // velocity limit is for total cartesian velocity, not a limit for each coordinate direction
116 const float scalePos = twist.linear.norm() / params.limits.linear;
117 const auto scaleOri = twist.angular.cwiseAbs().cwiseQuotient(angularLimit.cwiseAbs());
118
119 const float scaleMax = std::max(scalePos, scaleOri.maxCoeff());
120
121 if (scaleMax < 1.0F) // both linear and angular velocity in bounds?
122 {
123 return twist;
124 }
125
126 // scale such that no limit is violated
127 if (params.coupleLinearAndAngularLimits)
128 {
129 twist.linear /= scaleMax;
130 twist.angular /= scaleMax;
131 }
132 else
133 {
134 if (scalePos >= 1.0F)
135 {
136 twist.linear /= scalePos;
137 }
138 if (scaleOri.maxCoeff() >= 1.0F)
139 {
140 twist.angular /= scaleOri.maxCoeff();
141 }
142 }
143
144 // constexpr float eps = 0.001;
145
146 // pedantic checks
147 // ARMARX_CHECK_LESS_EQUAL(std::abs(twist.linear.x()), params.limits.linear + eps);
148 //ARMARX_CHECK_LESS_EQUAL(std::abs(twist.linear.y()), params.limits.linear + eps);
149 //ARMARX_CHECK_LESS_EQUAL(std::abs(twist.linear.z()), params.limits.linear + eps);
150 //ARMARX_CHECK_LESS_EQUAL(std::abs(twist.angular.x()), params.limits.angular + eps);
151 //ARMARX_CHECK_LESS_EQUAL(std::abs(twist.angular.y()), params.limits.angular + eps);
152 //ARMARX_CHECK_LESS_EQUAL(std::abs(twist.angular.z()), params.limits.angular + eps);
153
154 return twist;
155 }
156
159 {
160 twist.linear *= params.velocityFactor;
161 twist.angular *= params.velocityFactor;
162
163 return twist;
164 }
165
166 void
168 {
170 << "Scaling factor for velocity may not be negative, but is " << p.velocityFactor;
172 << "Scaling factor for velocity may not be > 1, but is " << p.velocityFactor;
173
174 // only changes to these to parameters are actually relevant for this class
175 params.velocityFactor = p.velocityFactor;
176 params.limits = p.limits;
177 params.orientationWeight = p.orientationWeight;
178 params.maxSegmentsAhead = p.maxSegmentsAhead;
179 params.coupleLinearAndAngularLimits = p.coupleLinearAndAngularLimits;
180 params.enableAngularFeedforward = p.enableAngularFeedforward;
181 params.lookaheadDistance = p.lookaheadDistance;
182 params.angularFeedforwardFraction = p.angularFeedforwardFraction;
183 params.minFeedforwardVelocityFraction = p.minFeedforwardVelocityFraction;
184 }
185
186 namespace
187 {
188 /**
189 * Shared prefix for every guard warning, so one `grep '[nav-guard]'` over a robot log
190 * shows which guards fired during a run. The token after it names the guard.
191 */
192 constexpr const char* guardTag = "[nav-guard] ";
193
194 float
195 signedShortestAngularDiff(const float target, const float current)
196 {
197 float diff = target - current;
198 while (diff > M_PIf32)
199 {
200 diff -= 2.0f * M_PIf32;
201 }
202 while (diff < -M_PIf32)
203 {
204 diff += 2.0f * M_PIf32;
205 }
206 return diff;
207 }
208 } // namespace
209
210 void
212 {
213 lastTrajectoryId_ = std::nullopt;
214 lastProjectionIndex_ = 0;
215 }
216
219 const core::Pose& global_T_robot)
220 {
221 using simox::math::mat4f_to_pos;
222 using simox::math::mat4f_to_rpy;
223
224 const core::Pose currentPose(global_T_robot);
225 const float currentOrientation = mat4f_to_rpy(currentPose.matrix()).z();
226
227 if (trajectory.points().empty())
228 {
229 ARMARX_INFO << "Trajectory is empty.";
231 .twist = core::Twist::Zero(),
232 .dropPoint = {.waypoint = {.pose = core::Pose::Identity()}, .velocity = 0},
233 .isFinalSegment = true,
234 .currentOrientation = currentOrientation,
235 .desiredOrientation = currentOrientation,
236 .orientationError = 0,
237 .positionError = 0};
238 }
239
240 // Detect new trajectory and reset progress if needed
241 const TrajectoryId currentTrajectoryId{
242 .numPoints = trajectory.points().size(),
243 .firstTranslation = trajectory.points().front().waypoint.pose.translation(),
244 .lastTranslation = trajectory.points().back().waypoint.pose.translation()};
245
246 if (not lastTrajectoryId_.has_value() or lastTrajectoryId_.value() != currentTrajectoryId)
247 {
248 lastTrajectoryId_ = currentTrajectoryId;
249 lastProjectionIndex_ = 0;
250 guards_ = GuardState{};
251 ARMARX_VERBOSE << "New trajectory detected, resetting progress.";
252 }
253
254 const std::size_t startSegment =
255 (lastProjectionIndex_ > 0) ? lastProjectionIndex_ - 1 : 0;
256
257 const auto projectedPose = trajectory.getProjection(
258 currentPose,
260 startSegment,
261 static_cast<std::size_t>(params.maxSegmentsAhead),
262 params.orientationWeight,
263 params.lookaheadDistance);
264
265 lastProjectionIndex_ = projectedPose.indexBefore;
266
267
268 pidPos.update(mat4f_to_pos(currentPose.matrix()),
269 mat4f_to_pos(projectedPose.projection.waypoint.pose.matrix()));
270 pidOri.update(mat4f_to_rpy(currentPose.matrix()),
271 mat4f_to_rpy(projectedPose.projection.waypoint.pose.matrix()));
272
273 const float desiredOrientation =
274 mat4f_to_rpy(projectedPose.projection.waypoint.pose.matrix()).z();
275
276 float ffAngular = 0.0f;
277 float cappedFfVel = 0.0f;
278 bool angularFeedforwardSaturated = false;
280
281 const core::Twist twist = [&]() -> core::Twist
282 {
283 // on the final segment, bahavior differs
284 if (projectedPose.segment == core::Projection::Segment::FINAL)
285 {
286
287 ARMARX_VERBOSE << deactivateSpam(1) << "final segment";
288 // TODO fairly inefficient to do this every time
289 pidPos.reset();
290 pidOri.reset();
291
292 pidPosTarget.update(currentPose.translation(),
293 trajectory.points().back().waypoint.pose.translation());
294
295 pidOriTarget.update(
296 mat4f_to_rpy(currentPose.matrix()),
297 mat4f_to_rpy(trajectory.points().back().waypoint.pose.matrix()));
298
299 return core::Twist{.linear = pidPosTarget.getControlValue(),
300 .angular = pidOriTarget.getControlValue()};
301 }
302
303 // pidPosTarget not used yet
304 // TODO fairly inefficient to do this every time
305 pidPosTarget.reset();
306
307 // the "standard" case following the trajectory
308 const Eigen::Vector3f segmentDelta =
309 projectedPose.wayPointAfter.waypoint.pose.translation() -
310 projectedPose.wayPointBefore.waypoint.pose.translation();
311
312 // `Eigen::normalized()` is `v / v.norm()`, i.e. 0/0 for a degenerate segment, and the
313 // NaN would travel all the way to the platform's velocity target. Coincident
314 // waypoints reach here from the planner, from `getSubTrajectory` after an emergency
315 // stop, and from any reparametrization that emits a repeated sample.
316 const bool degenerateSegment = not(segmentDelta.norm() > 0.F);
317
318 if (degenerateSegment)
319 {
321
322 if (not guards_.zeroLengthSegment)
323 {
324 guards_.zeroLengthSegment = true;
325 ARMARX_WARNING << guardTag << "zero-length-segment: trajectory segment "
326 << QUOTED(projectedPose.indexBefore)
327 << " has zero length. Dropping the feed-forward for it and "
328 "following on the orientation feedback alone.";
329 }
330 else
331 {
332 ARMARX_WARNING << deactivateSpam(1) << guardTag
333 << "zero-length-segment: still at segment "
334 << QUOTED(projectedPose.indexBefore) << ".";
335 }
336
337 return core::Twist{.linear = pidPos.getControlValue(),
338 .angular = pidOri.getControlValue()};
339 }
340
341 const Eigen::Vector3f desiredMovementDirection = segmentDelta.normalized();
342
343 const float ffVel = projectedPose.projection.velocity;
344
345 // Was `ARMARX_CHECK_FINITE` / `ARMARX_CHECK_LESS(ffVel, 3000)`. Both diagnose a real
346 // defect upstream, but throwing here kills the control thread, so they report and
347 // fall back instead. 3 m/s is still the sanity bound the original check used.
348 constexpr float implausibleVelocity = 3000.F;
349
350 if (not std::isfinite(ffVel) or ffVel >= implausibleVelocity)
351 {
353
354 if (not guards_.nonFiniteTwist)
355 {
356 guards_.nonFiniteTwist = true;
357 ARMARX_WARNING << guardTag << "nonfinite-twist: implausible feed-forward "
358 << "velocity " << QUOTED(ffVel) << " at segment "
359 << QUOTED(projectedPose.indexBefore)
360 << ". Dropping the feed-forward for this cycle.";
361 }
362
363 return core::Twist{.linear = pidPos.getControlValue(),
364 .angular = pidOri.getControlValue()};
365 }
366
367 // --- Angular feedforward based on segment orientation rate ---
368 cappedFfVel = ffVel;
369 if (params.enableAngularFeedforward)
370 {
371 const float yawBefore =
372 mat4f_to_rpy(projectedPose.wayPointBefore.waypoint.pose.matrix()).z();
373 const float yawAfter =
374 mat4f_to_rpy(projectedPose.wayPointAfter.waypoint.pose.matrix()).z();
375 const float angularDelta = signedShortestAngularDiff(yawAfter, yawBefore);
376
377 const float segmentLength =
378 (projectedPose.wayPointAfter.waypoint.pose.translation() -
379 projectedPose.wayPointBefore.waypoint.pose.translation())
380 .norm();
381
382 // The old gate was `segmentLength > 1.0f`, in millimetres. TOPP-RA emits a fixed
383 // 500 samples regardless of path length, so any path shorter than ~500 mm fell
384 // below it and the whole angular feed-forward silently vanished -- and did so
385 // discontinuously at that threshold. Only a genuinely degenerate segment needs
386 // excluding, and the zero-length guard above already handles that.
387 constexpr float minSegmentLength = 1e-3F; // [mm]
388
389 const float turnRatePerLength =
390 (segmentLength > minSegmentLength) ? (angularDelta / segmentLength) : 0.0f;
391
392 // How much of the angular budget the feed-forward may spend. Leaving headroom is
393 // what lets `pidOri` still correct a heading error: at the full limit the
394 // feed-forward saturates on every capped turn and the loop settles at a standing
395 // error of `limits.angular / pidOri.Kp` -- 57 deg with the shipped gain of 1.
396 const float ffAngularLimit =
397 std::clamp(params.angularFeedforwardFraction, 0.F, 1.F) * params.limits.angular;
398
399 const float requiredAngularRate = turnRatePerLength * ffVel;
400
401 // Cap linear velocity if the turn is too sharp for the robot
402 if (std::abs(requiredAngularRate) > ffAngularLimit and ffAngularLimit > 0.F)
403 {
404 // Substituting, this is `ffAngularLimit / |turnRatePerLength|` -- `ffVel`
405 // cancels out. Without a floor it goes to zero as the turn sharpens, which is
406 // the robot creeping while it spins.
407 const float minFfVel =
408 std::clamp(params.minFeedforwardVelocityFraction, 0.F, 1.F) * ffVel;
409
410 cappedFfVel = std::max(
411 minFfVel, ffVel * (ffAngularLimit / std::abs(requiredAngularRate)));
412 }
413
414 // Actual angular feedforward at the (possibly capped) linear speed, never above
415 // the share of the budget it is allowed -- the floor above can otherwise put it
416 // back over the limit.
417 ffAngular = std::clamp(
418 turnRatePerLength * cappedFfVel, -ffAngularLimit, ffAngularLimit);
419
420 // Substituting the cap gives `cappedFfVel = omega_max * L / |dyaw|` -- `ffVel`
421 // cancels -- and therefore `ffAngular = sign(dyaw) * omega_max` exactly. The
422 // feed-forward then owns the entire angular budget and `pidOri` has none left to
423 // correct a heading error, so the robot settles at a standing offset of
424 // `omega_max / Kp` while its linear command collapses to `omega_max / |yaw'|`.
425 angularFeedforwardSaturated =
426 ffAngularLimit > 0.F and std::abs(requiredAngularRate) > ffAngularLimit;
427
428 ARMARX_VERBOSE << deactivateSpam(1) << "FF angular " << ffAngular;
429 }
430
431 const auto feedforwardVelocity = desiredMovementDirection * cappedFfVel;
432
433 ARMARX_VERBOSE << deactivateSpam(1) << "Feed forward direction "
434 << feedforwardVelocity.normalized();
435 ARMARX_VERBOSE << deactivateSpam(1) << "Feed forward velocity " << feedforwardVelocity;
436 ARMARX_VERBOSE << deactivateSpam(1) << "Control value " << pidPos.getControlValue();
437
438 core::AngularVelocity angularFF = Eigen::Vector3f::Zero();
439 angularFF.z() = ffAngular;
440 return core::Twist{.linear = pidPos.getControlValue() + feedforwardVelocity,
441 .angular = pidOri.getControlValue() + angularFF};
442 }();
443
444 // The cap firing is not by itself an anomaly -- it fires on any sharp turn -- so warning
445 // per cycle would flood the log and prove nothing. Only the pathological signature is
446 // reported: the yaw rate pinned *and* the linear command collapsed, held for long enough
447 // that it is not a corner being negotiated.
448 if (angularFeedforwardSaturated and cappedFfVel < 0.25F * projectedPose.projection.velocity)
449 {
450 guards_.angularSaturatedCycles++;
451
452 constexpr std::size_t sustainedCycles = 50; // ~0.5 s at the 100 Hz task rate
453
454 if (guards_.angularSaturatedCycles >= sustainedCycles and
455 not guards_.angularFeedforwardSaturated)
456 {
457 guards_.angularFeedforwardSaturated = true;
459 << guardTag << "ff-angular-saturated: the angular feed-forward has been "
460 << "pinned at the limit for " << guards_.angularSaturatedCycles
461 << " cycles while the linear command collapsed. segment="
462 << QUOTED(projectedPose.indexBefore) << " ffVel="
463 << QUOTED(projectedPose.projection.velocity)
464 << " cappedFfVel=" << QUOTED(cappedFfVel) << " ffAngular="
465 << QUOTED(ffAngular) << " limitAngular=" << QUOTED(params.limits.angular)
466 << ". The robot is turning on the spot rather than following the path.";
467 }
468 }
469 else
470 {
471 guards_.angularSaturatedCycles = 0;
472 }
473
474 // Ahead of `applyTwistLimits`, not after it: a NaN reaching the limiter used to trip an
475 // `ARMARX_CHECK` there, which is the throw this whole guard exists to prevent.
476 core::Twist twistChecked = twist;
477
478 if (not twistChecked.linear.allFinite() or not twistChecked.angular.allFinite())
479 {
481 twistChecked = core::Twist::Zero();
482
483 if (not guards_.nonFiniteTwist)
484 {
485 guards_.nonFiniteTwist = true;
486 ARMARX_WARNING << guardTag
487 << "nonfinite-twist: the computed twist was not finite at segment "
488 << QUOTED(projectedPose.indexBefore)
489 << ". Commanding zero. A non-finite pose or velocity reached the "
490 "controller from the trajectory or the localization.";
491 }
492 else
493 {
494 ARMARX_WARNING << deactivateSpam(1) << guardTag << "nonfinite-twist: still zero.";
495 }
496 }
497
498 const auto twistLimited = applyTwistLimits(twistChecked);
499 ARMARX_VERBOSE << deactivateSpam(1) << "Twist limited linear "
500 << twistLimited.linear.transpose();
501 ARMARX_VERBOSE << deactivateSpam(1) << "Twist limited angular "
502 << twistLimited.angular.transpose();
503
504 const auto twistScaled = applyVelocityFactor(twistLimited);
505 ARMARX_VERBOSE << deactivateSpam(1) << "Twist scaled linear "
506 << twistScaled.linear.transpose();
507 ARMARX_VERBOSE << deactivateSpam(1) << "Twist scaled angular "
508 << twistScaled.angular.transpose();
509
510 // convert to the robot's base frame
511 const auto& twistGlobal = twistScaled;
512
513 core::Twist twistLocal; // NOLINT: reassigned by the non-finite guard below
514 twistLocal.linear = global_T_robot.linear().inverse() * twistGlobal.linear;
515 // TODO if not in 2D, then this must be changed!
516 twistLocal.angular = twistGlobal.angular;
517
518
519 const bool isFinalSegment = projectedPose.segment == core::Projection::Segment::FINAL;
520
521 // Last line of defence. Everything above guards its own inputs, but this runs in the
522 // 100 Hz `additionalTask()`, whose `runTask` callback `RunningTaskBase::run()` wraps in a
523 // single try/catch -- one escaping throw ends that thread for good while the RT loop keeps
524 // executing the last twist it was handed. Commanding zero is always safe; throwing is not.
525 if (not twistLocal.linear.allFinite() or not twistLocal.angular.allFinite())
526 {
528 twistLocal = core::Twist::Zero();
529
530 if (not guards_.nonFiniteTwist)
531 {
532 guards_.nonFiniteTwist = true;
533 ARMARX_WARNING << guardTag
534 << "nonfinite-twist: the computed twist was not finite at segment "
535 << QUOTED(projectedPose.indexBefore)
536 << ". Commanding zero. This means a non-finite pose or velocity "
537 "reached the controller from the trajectory or the localization.";
538 }
539 else
540 {
541 ARMARX_WARNING << deactivateSpam(1) << guardTag << "nonfinite-twist: still zero.";
542 }
543 }
544
546 .twist = twistLocal,
547 .dropPoint = projectedPose.projection,
548 .isFinalSegment = isFinalSegment,
549 .currentOrientation = currentOrientation,
550 .desiredOrientation = desiredOrientation,
551 // Wrapped: the raw difference spikes by up to 2*pi on every crossing of the +-pi
552 // branch cut, which is exactly where a heading-tracking plot is read most carefully.
553 .orientationError =
554 std::abs(signedShortestAngularDiff(desiredOrientation, currentOrientation)),
555 .positionError = (global_T_robot.translation() -
556 trajectory.points().back().waypoint.pose.translation())
557 .head<2>()
558 .norm(),
559 .ffAngular = ffAngular,
560 .cappedFfVel = cappedFfVel,
561 .projectionIndex = projectedPose.indexBefore,
562 .angularFeedforwardSaturated = angularFeedforwardSaturated,
563 .guard = guard};
564 }
565
566} // namespace armarx::navigation::traj_ctrl::global
SpamFilterDataPtr deactivateSpam(SpamFilterDataPtr const &spamFilter, float deactivationDurationSec, const std::string &identifier, bool deactivate)
Definition Logging.cpp:75
#define VAROUT(x)
#define QUOTED(x)
TrajectoryControllerResult control(const core::GlobalTrajectory &trajectory, const core::Pose &global_T_robot) override
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:188
#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
std::shared_ptr< Dict > DictPtr
Definition Dict.h:42
Eigen::Isometry3f Pose
Definition basic_types.h:31
Eigen::Vector3f AngularVelocity
Definition basic_types.h:44
void fromAron(const arondto::TrajectoryControllerParams &dto, TrajectoryControllerParams &bo)
GuardVerdict
Which guard, if any, altered this cycle's output.
@ ZeroLengthSegment
The projected segment had zero length, so the feed-forward direction was undefined.
@ NonFiniteTwist
The twist came out non-finite and was replaced by zero.
void toAron(arondto::TrajectoryControllerParams &dto, const TrajectoryControllerParams &bo)
std::vector< T > max(const std::vector< T > &v1, const std::vector< T > &v2)
bool isfinite(const std::vector< T, Ts... > &v)
Definition algorithm.h:366
double norm(const Point &a)
Definition point.hpp:102
float minFeedforwardVelocityFraction
Floor on the capped linear feed-forward, as a fraction of the requested velocity.
float angularFeedforwardFraction
Fraction of limits.angular the angular feed-forward may use. 1 lets it saturate.
static TrajectoryFollowingControllerParams FromAron(const aron::data::DictPtr &dict)
float lookaheadDistance
Lookahead as a distance [mm]. 0 keeps the pure maxSegmentsAhead behaviour.