TrajectoryFollowingSimulation.h
Go to the documentation of this file.
1/**
2 * This file is part of ArmarX.
3 *
4 * ArmarX is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License version 2 as
6 * published by the Free Software Foundation.
7 *
8 * ArmarX is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *
16 * @author Fabian Reister ( fabian dot reister at kit dot edu )
17 * @date 2026
18 * @copyright http://www.gnu.org/licenses/gpl-2.0.txt
19 * GNU General Public License
20 */
21
22#pragma once
23
24#include <cstddef>
25#include <limits>
26#include <vector>
27
28#include <Eigen/Core>
29
30#include <VirtualRobot/IK/platform/MecanumPlatformKinematics.h>
31#include <VirtualRobot/IK/platform/OmniWheelPlatformKinematics.h>
32
37
39{
40
41 /// How the simulated base turns a commanded twist into an achieved one.
42 enum class PlatformModel
43 {
44 /// Bound the magnitude of the twist change. Direction of the change is preserved.
46
47 /// Bound each wheel's angular acceleration separately, so one saturating wheel also
48 /// distorts the direction. Kinematic: the per-wheel bounds are placeholders.
50
51 /// Bound each *motor's torque* and speed, using the robot's real inertia. The physical
52 /// model: required wrench -> wheel torques -> per-motor clip -> achieved acceleration.
54
55 /// As `OmniWheelTorque`, but for a four-wheel mecanum drive.
57 };
58
59 /**
60 * @brief Limits of the omni-directional wheel drive, as on ARMAR-7.
61 *
62 * Wheel velocities follow `VirtualRobot::OmniWheelPlatformKinematics`, whose forward model
63 * carries a factor `2 pi R / n`. Wheel speeds are therefore in **revolutions per second** at
64 * the wheel, not rad/s.
65 */
67 {
68 VirtualRobot::OmniWheelPlatformKinematicsParams kinematics{};
69
70 float maxWheelVelocity{3.82F}; // [rev/s]
71
72 float maxWheelAcceleration{0.64F}; // [rev/s^2]
73
74 float maxWheelDeceleration{1.02F}; // [rev/s^2]
75 };
76
77 /**
78 * @brief Limits of the four-wheel mecanum drive, as on ARMAR-6 and ARMAR-DE.
79 *
80 * Wheel velocities follow `VirtualRobot::MecanumPlatformKinematics`, whose `J()` is
81 * `R/4 * ...` and therefore takes **rad/s** at the wheel -- unlike the omni drive's `C()`,
82 * which takes rev/s. Note also that Simox has y pointing forwards, not x as in the paper,
83 * and orders the wheels `[left front, right front, rear left, rear right]`.
84 */
86 {
87 VirtualRobot::MecanumPlatformKinematicsParams kinematics{};
88
89 float maxWheelVelocity{21.1F}; // [rad/s]
90
91 float maxWheelAcceleration{8.5F}; // [rad/s^2]
92
93 float maxWheelDeceleration{8.5F}; // [rad/s^2]
94 };
95
96 /**
97 * @brief What the ARMAR-7 drive train can actually deliver.
98 *
99 * Motor: Maxon IDX 56 M, 48 V winding. Gearbox: Maxon GPX 52, 16:1. See
100 * `data/armarx_navigation/config/platform/README.md` for the full derivation of every
101 * number here.
102 *
103 * Geometry is *not* repeated: the torque model reuses `OmniWheelDrive::kinematics`.
104 */
106 {
107 /// Motor revolutions per wheel revolution. 65536 counts/wheel-rev / 4096 counts/motor-rev.
108 float gearRatio{16.F};
109
110 /// Torque bound per motor [N m]. Default is the 7.0 A `MaxCurrent` cap x 62.4 mNm/A.
111 float motorMaxTorque{0.437F};
112
113 /// Torque at `RatedCurrent` (7.34 A x 62.4 mNm/A) [N m]. Recorded for reference.
114 float motorRatedTorque{0.458F};
115
116 /// Continuous motor speed [rpm], IDX 56 M at 48 V.
117 float motorNominalSpeedRpm{6260.F};
118
119 /// Gearbox continuous input speed [rpm]. Binds before the motor on ARMAR-7.
121
122 /// Measured wheel speed limit [rad/s]. Where finite, it supersedes the rating above
123 /// (`min(motorNominalSpeedRpm, gearboxMaxInputSpeedRpm) / gearRatio`), as in
124 /// `LoadDriveParams`.
125 float maxWheelVelocity{std::numeric_limits<float>::infinity()};
126
127 /// Ignored losses are modelled as 1.0, i.e. optimistic.
129
131 };
132
133 /**
134 * @brief The per-axis command ramp the platform device applies below the navigation stack.
135 *
136 * Faithful model of `LinearLimitedAccelerationController` in
137 * `devices/ethercat/platform/armar7_omni/joint_controller/Velocity.cpp`, which rate-limits
138 * `vx`, `vy` and the platform yaw rate **independently** before the inverse kinematics.
139 *
140 * This is the only acceleration limit in the real signal chain: the navigation stack does
141 * not bound the time derivative of the commanded twist (its `alpha` low-pass is configured
142 * to 0 on ARMAR-7), and the simulated platform device in `RobotUnitSimulation` does not
143 * either. Modelling it here is what lets the analysis answer whether the values in
144 * `Armar7a/parts/Platform.xml` actually keep the motors out of saturation.
145 *
146 * Defaults mirror that file.
147 */
149 {
150 bool enabled{true};
151
152 /// Control cycle of the RT loop [s]. `RTUnit` runs at 1 kHz and clamps dt to <= 2 ms.
153 float dt{0.001F};
154
155 float maxVelocity{1500.F}; // [mm/s]
156
157 float maxAcceleration{250.F}; // [mm/s^2]
158
159 float maxDeceleration{400.F}; // [mm/s^2]
160
161 float maxAngularVelocity{1.0F}; // [rad/s]
162
163 float maxAngularAcceleration{0.4F}; // [rad/s^2]
164
165 float maxAngularDeceleration{0.45F}; // [rad/s^2]
166
167 /**
168 * @brief Ramp the two linear axes together instead of independently.
169 *
170 * Mirrors `directionPreservingRamp` in the platform's hardware config. Independent
171 * per-axis ramps let the commanded direction rotate whenever the two targets differ in
172 * magnitude; coupling them bounds `|dv/dt|` and keeps the direction. Default matches the
173 * device default so an unconfigured simulation reproduces the historical behaviour.
174 */
176 };
177
178 /**
179 * @brief Dynamic limits of the simulated platform.
180 *
181 * Nothing in the real stack enforces a bound on how fast the commanded twist may change:
182 * `applyTwistLimits()` bounds the velocity only, and the RT layer forwards the twist
183 * unchanged. (The `armar7_omni` device does rate-limit, but per Cartesian axis, downstream
184 * of the navigation stack.) These limits exist so the simulation can show the consequence —
185 * a base that cannot follow a velocity step lags behind the command and leaves the path.
186 */
188 {
190
191 float maxLinearAcceleration{500.F}; // [mm/s^2]
192
193 float maxAngularAcceleration{0.5F}; // [rad/s^2]
194
196
198
200
201 /**
202 * @brief Read the dynamics from `config/platform/PlatformDynamics<Robot>.json`.
203 *
204 * @param robot Platform name, spelled as the rest of the codebase does:
205 * `Armar7`, `Armar6`, `ArmarDE`.
206 */
207 static PlatformDynamics FromPackagePath(const std::string& robot = "Armar7");
208 };
209
210 /**
211 * @brief Point-mass simulation of the platform following a global trajectory.
212 *
213 * Reproduces the loop that `platform_controller::platform_global_trajectory::Controller`
214 * runs on the robot:
215 *
216 * 1. `TrajectoryFollowingController::control(trajectory, global_T_robot)` produces a twist
217 * in the *robot* frame (`additionalTask()`),
218 * 2. the twist is low-pass filtered with `alpha` (`additionalTask()`),
219 * 3. the filtered twist is handed to the holonomic platform as a velocity target
220 * (`rtRun()`).
221 *
222 * The robot is modelled as a point mass that reaches the commanded velocity instantly, i.e.
223 * **no acceleration limit is imposed**. That is deliberate: the resulting acceleration is an
224 * output of the simulation, so it can be compared against what the base can actually deliver.
225 * Where the required acceleration exceeds that, the real robot lags behind and overshoots —
226 * which the trajectory's velocity profile alone does not reveal.
227 *
228 * The controller and the real-time loop run at different rates on the robot (the additional
229 * task at ~100 Hz, the RT layer faster). Here both run at `dt`, which is the controller rate:
230 * the RT layer only forwards the twist, so it does not affect the trajectory.
231 */
233 {
234 public:
236 {
237 /// Control cycle [s]. `CycleUtil(10)` in the controller's additional task -> 10 ms.
238 float dt{0.01F};
239
240 /// Give up after this much simulated time [s].
241 float maxDuration{120.F};
242
243 /// Consider the goal reached within this distance to the last trajectory point [mm].
245
246 /// Speed below which the robot counts as standing still [mm/s].
247 ///
248 /// The controller's final segment is a pure P law, so it approaches the goal
249 /// asymptotically and can settle just outside `goalDistanceThreshold` -- creeping at
250 /// a few hundredths of a mm/s for the rest of `maxDuration`. Without this the
251 /// simulation would spend most of its samples on a robot that has stopped.
253
254 /// How long the robot has to stay below `settledSpeedThreshold` before giving up [s].
255 float settledDuration{1.F};
256
257 /// Parameters of the trajectory following controller, including `alpha` and limits.
259
260 /// What the simulated mass can actually deliver.
262
263 /// The device-side command ramp between the controller and the platform.
265 };
266
267 struct Sample
268 {
269 float time{0.F}; // [s]
270
271 core::Pose global_T_robot{core::Pose::Identity()};
272
273 /// Filtered twist as commanded to the platform, in the robot frame.
274 Eigen::Vector2f commandedLinearLocal{Eigen::Vector2f::Zero()}; // [mm/s]
275
276 float commandedAngular{0.F}; // [rad/s]
277
278 /// Commanded linear velocity in the global frame, i.e. what the mass is asked for.
279 Eigen::Vector2f commandedVelocityGlobal{Eigen::Vector2f::Zero()}; // [mm/s]
280
281 /// Achieved linear velocity in the global frame, after the acceleration limit.
282 Eigen::Vector2f velocityGlobal{Eigen::Vector2f::Zero()}; // [mm/s]
283
284 float speed{0.F}; // [mm/s] magnitude of the achieved velocity
285
286 /// Acceleration needed to match the command within one cycle, ignoring the limit.
287 float requiredLinearAcceleration{0.F}; // [mm/s^2]
288
289 /// Acceleration actually applied, bounded by `PlatformDynamics`.
290 float linearAcceleration{0.F}; // [mm/s^2]
291
292 float requiredAngularAcceleration{0.F}; // [rad/s^2]
293
294 float angularAcceleration{0.F}; // [rad/s^2]
295
296 /// True when a limit was binding, i.e. the base could not follow the command.
298
299 /// Wheel speeds the command asks for [rev/s]. Only set for the omni-wheel model.
301
302 /// Wheel speeds actually reached [rev/s]. Only set for the omni-wheel model.
303 Eigen::VectorXf wheelVelocities;
304
305 /// Per-wheel acceleration the command asks for [rev/s^2], ignoring the limit.
307
308 /// Which wheels hit their acceleration bound. Only set for `OmniWheelVelocity`.
309 Eigen::ArrayXi wheelSaturated;
310
311 /// Motor torque the command asks for [N m]. Only set for `OmniWheelTorque`.
312 Eigen::VectorXf requiredMotorTorques;
313
314 /// Motor torque after clipping [N m]. Only set for `OmniWheelTorque`.
315 Eigen::VectorXf appliedMotorTorques;
316
317 /// Which motors hit their torque bound. Only set for `OmniWheelTorque`.
318 Eigen::ArrayXi motorSaturated;
319
320 /// Which wheels hit their speed bound. Only set for `OmniWheelTorque`.
321 Eigen::ArrayXi wheelSpeedSaturated;
322
323 /// Velocity of the trajectory point the controller is currently tracking.
324 float dropPointVelocity{0.F}; // [mm/s]
325
326 /// Distance from the robot to the trajectory point it is tracking [mm].
327 /// This is the cross-track error, i.e. how far the robot has drifted off the path.
328 float trackingError{0.F};
329
330 /// Distance to the last trajectory point [mm]. This is what the controller itself
331 /// reports as `positionError`, despite the name.
332 float distanceToGoal{0.F};
333
334 float orientationError{0.F}; // [rad]
335 };
336
337 struct Result
338 {
339 std::vector<Sample> samples;
340
341 bool reachedGoal{false};
342
343 /// Set when the run ended because the robot stopped moving short of the goal.
345
346 /// Distance to the last trajectory point when the run ended [mm].
348
349 /// Furthest the robot ever got *past* the last trajectory point [mm].
350 ///
351 /// The velocity profile is indexed by position, so nothing in it bounds the stopping
352 /// distance the device command ramp actually needs. Overshoot is the visible symptom.
354
355 /// Largest `requiredLinearAcceleration` over the run [mm/s^2].
357
358 /// Largest `requiredAngularAcceleration` over the run [rad/s^2].
360
361 /// Number of control cycles in which any limit was binding.
362 std::size_t saturatedCycles{0};
363
364 /// Cycles in which a motor torque bound was binding. `OmniWheelTorque` only.
366
367 /// Cycles in which a wheel speed bound was binding. `OmniWheelTorque` only.
369
370 /// Largest motor torque demanded over the run [N m]. `OmniWheelTorque` only.
372
373 /// Platform mass and yaw inertia from the robot model. `OmniWheelTorque` only.
374 float platformMass{0.F}; // [kg]
375
376 float platformYawInertia{0.F}; // [kg m^2]
377
378 /// Full mass matrix at the `Home` configuration, `[x, y, yaw]`. The off-diagonal
379 /// terms are the CoM offset from the yaw axis and are not negligible on ARMAR-7.
380 Eigen::Matrix3d massMatrix{Eigen::Matrix3d::Zero()};
381
382 /// Largest cross-track error over the run [mm].
384 };
385
386 explicit TrajectoryFollowingSimulation(const Parameters& params);
387
388 /// Follow `trajectory` starting from `start`, which need not be on the trajectory.
389 Result run(const core::GlobalTrajectory& trajectory, const core::Pose& start) const;
390
391 private:
392 Parameters params_;
393 };
394
395} // namespace armarx::navigation::simulation
Result run(const core::GlobalTrajectory &trajectory, const core::Pose &start) const
Follow trajectory starting from start, which need not be on the trajectory.
Eigen::Isometry3f Pose
Definition basic_types.h:31
This file is part of ArmarX.
PlatformModel
How the simulated base turns a commanded twist into an achieved one.
@ MecanumTorque
As OmniWheelTorque, but for a four-wheel mecanum drive.
@ OmniWheelVelocity
Bound each wheel's angular acceleration separately, so one saturating wheel also distorts the directi...
@ Cartesian
Bound the magnitude of the twist change. Direction of the change is preserved.
@ OmniWheelTorque
Bound each motor's torque and speed, using the robot's real inertia.
The per-axis command ramp the platform device applies below the navigation stack.
float dt
Control cycle of the RT loop [s]. RTUnit runs at 1 kHz and clamps dt to <= 2 ms.
bool directionPreserving
Ramp the two linear axes together instead of independently.
Limits of the four-wheel mecanum drive, as on ARMAR-6 and ARMAR-DE.
VirtualRobot::MecanumPlatformKinematicsParams kinematics
Limits of the omni-directional wheel drive, as on ARMAR-7.
VirtualRobot::OmniWheelPlatformKinematicsParams kinematics
What the ARMAR-7 drive train can actually deliver.
float motorMaxTorque
Torque bound per motor [N m]. Default is the 7.0 A MaxCurrent cap x 62.4 mNm/A.
float gearboxMaxInputSpeedRpm
Gearbox continuous input speed [rpm]. Binds before the motor on ARMAR-7.
float motorRatedTorque
Torque at RatedCurrent (7.34 A x 62.4 mNm/A) [N m]. Recorded for reference.
float motorNominalSpeedRpm
Continuous motor speed [rpm], IDX 56 M at 48 V.
float gearboxEfficiency
Ignored losses are modelled as 1.0, i.e. optimistic.
float gearRatio
Motor revolutions per wheel revolution. 65536 counts/wheel-rev / 4096 counts/motor-rev.
static PlatformDynamics FromPackagePath(const std::string &robot="Armar7")
Read the dynamics from config/platform/PlatformDynamics<Robot>.json.
PlatformDynamics dynamics
What the simulated mass can actually deliver.
float dt
Control cycle [s]. CycleUtil(10) in the controller's additional task -> 10 ms.
float settledSpeedThreshold
Speed below which the robot counts as standing still [mm/s].
CommandRateLimit rateLimit
The device-side command ramp between the controller and the platform.
float goalDistanceThreshold
Consider the goal reached within this distance to the last trajectory point [mm].
float settledDuration
How long the robot has to stay below settledSpeedThreshold before giving up [s].
float finalDistanceToGoal
Distance to the last trajectory point when the run ended [mm].
Eigen::Matrix3d massMatrix
Full mass matrix at the Home configuration, [x, y, yaw].
float maxRequiredMotorTorque
Largest motor torque demanded over the run [N m]. OmniWheelTorque only.
float maxRequiredAngularAcceleration
Largest requiredAngularAcceleration over the run [rad/s^2].
std::size_t motorTorqueSaturatedCycles
Cycles in which a motor torque bound was binding. OmniWheelTorque only.
std::size_t wheelSpeedSaturatedCycles
Cycles in which a wheel speed bound was binding. OmniWheelTorque only.
float platformMass
Platform mass and yaw inertia from the robot model. OmniWheelTorque only.
float maxRequiredLinearAcceleration
Largest requiredLinearAcceleration over the run [mm/s^2].
float maxGoalOvershoot
Furthest the robot ever got past the last trajectory point [mm].
std::size_t saturatedCycles
Number of control cycles in which any limit was binding.
bool settledShortOfGoal
Set when the run ended because the robot stopped moving short of the goal.
float trackingError
Distance from the robot to the trajectory point it is tracking [mm].
Eigen::VectorXf commandedWheelVelocities
Wheel speeds the command asks for [rev/s]. Only set for the omni-wheel model.
float linearAcceleration
Acceleration actually applied, bounded by PlatformDynamics.
bool accelerationSaturated
True when a limit was binding, i.e. the base could not follow the command.
Eigen::VectorXf requiredWheelAccelerations
Per-wheel acceleration the command asks for [rev/s^2], ignoring the limit.
Eigen::ArrayXi wheelSpeedSaturated
Which wheels hit their speed bound. Only set for OmniWheelTorque.
Eigen::VectorXf wheelVelocities
Wheel speeds actually reached [rev/s]. Only set for the omni-wheel model.
Eigen::Vector2f commandedLinearLocal
Filtered twist as commanded to the platform, in the robot frame.
Eigen::ArrayXi motorSaturated
Which motors hit their torque bound. Only set for OmniWheelTorque.
float dropPointVelocity
Velocity of the trajectory point the controller is currently tracking.
Eigen::ArrayXi wheelSaturated
Which wheels hit their acceleration bound. Only set for OmniWheelVelocity.
Eigen::Vector2f commandedVelocityGlobal
Commanded linear velocity in the global frame, i.e. what the mass is asked for.
Eigen::Vector2f velocityGlobal
Achieved linear velocity in the global frame, after the acceleration limit.
Eigen::VectorXf requiredMotorTorques
Motor torque the command asks for [N m]. Only set for OmniWheelTorque.
float requiredLinearAcceleration
Acceleration needed to match the command within one cycle, ignoring the limit.
Eigen::VectorXf appliedMotorTorques
Motor torque after clipping [N m]. Only set for OmniWheelTorque.