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