io.cpp
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#include "io.h"
23
24#include <algorithm>
25#include <cmath>
26#include <cstddef>
27#include <fstream>
28#include <vector>
29
30#include <SimoxUtility/json/json.hpp>
31
34
36{
37
38 namespace
39 {
40
41 nlohmann::json
42 toJson(const Eigen::Vector2f& v)
43 {
44 return nlohmann::json::array({v.x(), v.y()});
45 }
46
47 /// Wheel-count agnostic: the array is as long as the drive has wheels.
48 nlohmann::json
49 toJson(const Eigen::VectorXf& v)
50 {
51 nlohmann::json a = nlohmann::json::array();
52 for (Eigen::Index i = 0; i < v.size(); i++)
53 {
54 a.push_back(v[i]);
55 }
56 return a;
57 }
58
59 /// Same, as booleans -- the saturation flags are stored as 0/1 ints.
60 nlohmann::json
61 toJsonFlags(const Eigen::ArrayXi& v)
62 {
63 nlohmann::json a = nlohmann::json::array();
64 for (Eigen::Index i = 0; i < v.size(); i++)
65 {
66 a.push_back(v[i] != 0);
67 }
68 return a;
69 }
70
71 float
72 yawOf(const core::Pose& pose)
73 {
74 return std::atan2(pose.linear()(1, 0), pose.linear()(0, 0));
75 }
76
77 nlohmann::json
78 toJson(const core::Pose& pose)
79 {
80 return nlohmann::json{{"x", pose.translation().x()},
81 {"y", pose.translation().y()},
82 {"yaw", yawOf(pose)}};
83 }
84
85 nlohmann::json
86 toJson(const Scene& scene)
87 {
88 nlohmann::json jObstacles = nlohmann::json::array();
89 for (const Box& obstacle : scene.obstacles)
90 {
91 jObstacles.push_back(nlohmann::json{
92 {"type", "box"},
93 {"min", toJson(obstacle.min)},
94 {"max", toJson(obstacle.max)},
95 {"yaw", obstacle.yaw}});
96 }
97
98 return nlohmann::json{
99 {"bounds",
100 nlohmann::json{{"min", toJson(scene.bounds.min)},
101 {"max", toJson(scene.bounds.max)}}},
102 {"cell_size", scene.cellSize},
103 {"obstacles", jObstacles}};
104 }
105
106 nlohmann::json
107 toJson(const algorithms::Costmap& costmap)
108 {
109 const algorithms::Costmap::Grid& grid = costmap.getGrid();
110
111 nlohmann::json jGrid = nlohmann::json::array();
112 for (int ix = 0; ix < grid.rows(); ix++)
113 {
114 std::vector<float> row(static_cast<std::size_t>(grid.cols()));
115 for (int iy = 0; iy < grid.cols(); iy++)
116 {
117 row[static_cast<std::size_t>(iy)] = grid(ix, iy);
118 }
119
120 jGrid.push_back(row);
121 }
122
123 return nlohmann::json{
124 {"cell_size", costmap.params().cellSize},
125 {"robot_radius", costmap.params().robotRadius},
126 {"bounds",
127 nlohmann::json{{"min", toJson(costmap.getLocalSceneBounds().min)},
128 {"max", toJson(costmap.getLocalSceneBounds().max)}}},
129 {"rows", grid.rows()},
130 {"cols", grid.cols()},
131 {"grid", jGrid}};
132 }
133
134 /// A velocity counts as violating the limit only beyond this relative slack.
135 constexpr float violationTolerance = 1e-3F;
136
137 /// ... and beyond this absolute slack [mm/s].
138 ///
139 /// The limit is sampled from a costmap with finite cells, so moving a waypoint by a
140 /// fraction of a cell changes the limit it is compared against. A resampled trajectory
141 /// therefore reports a handful of violations of one or two mm/s that say nothing about
142 /// the planner -- they are the difference between two lookups, not a real excess.
143 constexpr float violationToleranceAbsolute = 5.F;
144
145 nlohmann::json
148 {
149 nlohmann::json jPoints = nlohmann::json::array();
150
151 std::size_t violations = 0;
152 float maxViolation = 0.F;
153
154 for (const core::GlobalTrajectoryPoint& point : trajectory.points())
155 {
156 const core::Position& position = point.waypoint.pose.translation();
157 const float permissible = limit.at(Eigen::Vector2f{position.head<2>()});
158 const float excess = point.velocity - permissible;
159
160 if (excess > std::max(violationTolerance * permissible,
161 violationToleranceAbsolute))
162 {
163 violations++;
164 maxViolation = std::max(maxViolation, excess);
165 }
166
167 jPoints.push_back(nlohmann::json{{"x", position.x()},
168 {"y", position.y()},
169 {"yaw", yawOf(point.waypoint.pose)},
170 {"velocity", point.velocity},
171 {"velocity_limit", permissible}});
172 }
173
174 return nlohmann::json{
175 {"points", jPoints},
176 {"length", trajectory.length()},
177 {"duration",
179 {"velocity_limit_violations", violations},
180 {"max_velocity_limit_violation", maxViolation}};
181 }
182
183 nlohmann::json
185 const simulation::PlatformDynamics& dynamics)
186 {
187 nlohmann::json jSamples = nlohmann::json::array();
188
189 for (const auto& sample : simulated.samples)
190 {
191 const core::Position& position = sample.global_T_robot.translation();
192
193 jSamples.push_back(
194 nlohmann::json{{"t", sample.time},
195 {"x", position.x()},
196 {"y", position.y()},
197 {"yaw", yawOf(sample.global_T_robot)},
198 {"speed", sample.speed},
199 {"vx", sample.velocityGlobal.x()},
200 {"vy", sample.velocityGlobal.y()},
201 {"commanded_speed", sample.commandedVelocityGlobal.norm()},
202 {"angular_velocity", sample.commandedAngular},
203 {"required_linear_acceleration",
204 sample.requiredLinearAcceleration},
205 {"linear_acceleration", sample.linearAcceleration},
206 {"required_angular_acceleration",
207 sample.requiredAngularAcceleration},
208 {"angular_acceleration", sample.angularAcceleration},
209 {"acceleration_saturated", sample.accelerationSaturated},
210 {"required_wheel_accelerations", toJson(sample.requiredWheelAccelerations)},
211 {"wheel_velocities", toJson(sample.wheelVelocities)},
212 {"wheel_saturated", toJsonFlags(sample.wheelSaturated)},
213 {"required_motor_torques", toJson(sample.requiredMotorTorques)},
214 {"applied_motor_torques", toJson(sample.appliedMotorTorques)},
215 {"motor_saturated", toJsonFlags(sample.motorSaturated)},
216 {"wheel_speed_saturated", toJsonFlags(sample.wheelSpeedSaturated)},
217 {"drop_point_velocity", sample.dropPointVelocity},
218 {"tracking_error", sample.trackingError},
219 {"distance_to_goal", sample.distanceToGoal},
220 {"orientation_error", sample.orientationError}});
221 }
222
223 const std::string modelName = [&]() -> std::string
224 {
225 switch (dynamics.model)
226 {
228 return "omni_wheel_torque";
230 return "mecanum_torque";
232 return "omni_wheel_velocity";
234 return "cartesian";
235 }
236
237 return "unknown";
238 }();
239
240 return nlohmann::json{
241 {"samples", jSamples},
242 {"reached_goal", simulated.reachedGoal},
243 {"settled_short_of_goal", simulated.settledShortOfGoal},
244 {"final_distance_to_goal", simulated.finalDistanceToGoal},
245 {"max_goal_overshoot", simulated.maxGoalOvershoot},
246 {"max_required_linear_acceleration", simulated.maxRequiredLinearAcceleration},
247 {"max_required_angular_acceleration", simulated.maxRequiredAngularAcceleration},
248 {"saturated_cycles", simulated.saturatedCycles},
249 {"motor_torque_saturated_cycles", simulated.motorTorqueSaturatedCycles},
250 {"wheel_speed_saturated_cycles", simulated.wheelSpeedSaturatedCycles},
251 {"max_required_motor_torque", simulated.maxRequiredMotorTorque},
252 {"platform_mass", simulated.platformMass},
253 {"platform_yaw_inertia", simulated.platformYawInertia},
254 {"mass_matrix",
255 {{simulated.massMatrix(0, 0),
256 simulated.massMatrix(0, 1),
257 simulated.massMatrix(0, 2)},
258 {simulated.massMatrix(1, 0),
259 simulated.massMatrix(1, 1),
260 simulated.massMatrix(1, 2)},
261 {simulated.massMatrix(2, 0),
262 simulated.massMatrix(2, 1),
263 simulated.massMatrix(2, 2)}}},
264 {"max_tracking_error", simulated.maxTrackingError},
265 {"max_linear_acceleration", dynamics.maxLinearAcceleration},
266 {"max_angular_acceleration", dynamics.maxAngularAcceleration},
267 {"model", modelName},
268 {"max_wheel_acceleration", dynamics.omniWheel.maxWheelAcceleration},
269 {"max_wheel_deceleration", dynamics.omniWheel.maxWheelDeceleration},
270 {"motor_max_torque", dynamics.omniWheelTorque.motorMaxTorque},
271 {"gear_ratio", dynamics.omniWheelTorque.gearRatio}};
272 }
273
274 } // namespace
275
276 void
277 writeResult(const std::filesystem::path& filename,
278 const Config& config,
279 const algorithms::Costmap& costmap,
280 const std::optional<global_planning::GlobalPlannerResult>& result,
282 const std::optional<simulation::TrajectoryFollowingSimulation::Result>& simulated,
283 const nlohmann::json& reference,
284 const CommandRampCheck& rampCheck,
285 const double parametrizationSeconds)
286 {
287 nlohmann::json j{
288 {"success", result.has_value()},
289 {"scene", toJson(config.scene)},
290 {"start", toJson(config.start)},
291 {"goal", toJson(config.goal)},
292 {"costmap", toJson(costmap)},
293 {"planner",
294 nlohmann::json{
295 {"obstacle_distance_costs", config.plannerParams.algo.obstacleDistanceCosts},
296 {"obstacle_cost_exponent", config.plannerParams.algo.obstacleCostExponent},
297 {"obstacle_max_distance", config.plannerParams.algo.obstacleMaxDistance},
298 {"obstacle_distance_weight", config.plannerParams.algo.obstacleDistanceWeight},
299 {"max_linear_velocity", config.generalConfig.maxVel.linear},
300 {"max_angular_velocity", config.generalConfig.maxVel.angular},
301 {"enable_position_smoothing", config.plannerParams.enablePositionSmoothing},
302 {"enable_final_velocity_clamp",
304 {"parametrization", toString(config.parametrization)}}}};
305
306 if (result.has_value())
307 {
308 j["trajectory"] = toJson(result->trajectory, limit);
309 j["timings"] = result->timings;
310
311 // Deliberately not folded into `timings`: that is the planner's own stage
312 // breakdown, which the reader sums, and the parametrization runs after plan().
313 j["parametrization_seconds"] = parametrizationSeconds;
314
315 // The bounds the reparametrization actually solved against, as opposed to the
316 // configured ones they are derived from. They still differ enough to mislead: the
317 // acceleration bound is min(accel, decel) -- deceleration is deliberately not spent
318 // here, it is reserved for the reactive safety guard -- against the 500 mm/s^2 the
319 // simulation reports as its own limit, and the wheel bound is referred to the wheel
320 // rather than the motor. Without these a reader cannot tell which constraint shaped
321 // the profile.
322 const auto& ramp = config.simulationParams.rateLimit;
323 const auto& drive = config.simulationParams.dynamics.omniWheelTorque;
324
325 const float inputSpeedRpm =
326 std::min(drive.motorNominalSpeedRpm, drive.gearboxMaxInputSpeedRpm);
327 // As `LoadDriveParams`: a measured wheel limit supersedes the rating.
328 const float maxWheelVelocity =
329 std::isfinite(drive.maxWheelVelocity)
330 ? drive.maxWheelVelocity
331 : inputSpeedRpm / 60.F / drive.gearRatio * 2.F * static_cast<float>(M_PI);
332
333 j["parametrization_limits"] = nlohmann::json{
334 {"max_velocity", config.generalConfig.maxVel.linear},
335 {"max_angular_velocity", config.generalConfig.maxVel.angular},
336 {"max_acceleration", ramp.maxAcceleration},
337 {"max_deceleration", ramp.maxDeceleration},
338 // Radius of the disc the acceleration is held inside. Approximated by an
339 // inscribed polygon of `acceleration_polygon_sides`; keep that in sync with
340 // `N_DIRECTIONS` in the python package's `reparametrization/acceleration.py`.
341 {"acceleration_radius", std::min(ramp.maxAcceleration, ramp.maxDeceleration)},
342 {"acceleration_polygon_sides", 32},
343 // Which acceleration the radius bounds. The ramp acts on the commanded *body*
344 // twist, so bounding the world frame would leave the real limit exceeded.
345 {"acceleration_frame", "body"},
346 {"max_wheel_velocity", maxWheelVelocity},
347 {"motor_max_torque",
348 drive.motorMaxTorque * config.parametrizationTorqueFraction}};
349
350 nlohmann::json jGridPath = nlohmann::json::array();
351 for (const core::Position& position : result->gridPath)
352 {
353 jGridPath.push_back(
354 nlohmann::json{{"x", position.x()}, {"y", position.y()}});
355 }
356 j["grid_path"] = jGridPath;
357 j["position_smoothing_applied"] = result->positionSmoothingApplied;
358 }
359
360 if (simulated.has_value())
361 {
362 j["simulation"] = toJson(simulated.value(), config.simulationParams.dynamics);
363 }
364
365 {
366 nlohmann::json jSpans = nlohmann::json::array();
367 for (const auto& [from, to] : rampCheck.spans)
368 {
369 jSpans.push_back(nlohmann::json::array({from, to}));
370 }
371
372 j["velocity_profile_exceeds_command_ramp"] =
373 nlohmann::json{{"violations", rampCheck.violations},
374 {"worst_demand", rampCheck.worstDemand},
375 {"worst_limit", rampCheck.worstLimit},
376 {"worst_arc_length", rampCheck.worstArcLength},
377 {"terminal_stopping_distance",
378 rampCheck.terminalStoppingDistance},
379 {"terminal_velocity_exceeds_boundary",
381 {"spans", jSpans}};
382 }
383
384 if (not reference.is_null())
385 {
386 j["reparametrization_reference"] = reference;
387 }
388
389 std::ofstream ofs{filename};
390 ARMARX_CHECK(ofs.good()) << "Cannot write to " << QUOTED(filename.string()) << ".";
391
392 ofs << j.dump(2) << std::endl;
393 }
394
395} // namespace armarx::navigation::analysis
#define M_PI
Definition MathTools.h:17
#define QUOTED(x)
const SceneBounds & getLocalSceneBounds() const noexcept
Definition Costmap.cpp:165
const Parameters & params() const noexcept
Definition Costmap.cpp:302
The maximum permissible linear velocity as a function of the distance to the closest obstacle.
float at(const Eigen::Vector2f &position) const
The limit at a position in the costmap's global frame.
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
This file is part of ArmarX.
Definition io.cpp:36
std::string toString(const core::TrajectoryParametrization mode)
void writeResult(const std::filesystem::path &filename, const Config &config, const algorithms::Costmap &costmap, const std::optional< global_planning::GlobalPlannerResult > &result, const algorithms::ObstacleAwareVelocityLimit &limit, const std::optional< simulation::TrajectoryFollowingSimulation::Result > &simulated, const nlohmann::json &reference, const CommandRampCheck &rampCheck, const double parametrizationSeconds)
Write scene, costmap and planning result as JSON for the python plotting tool.
Definition io.cpp:277
Eigen::Isometry3f Pose
Definition basic_types.h:31
Eigen::Vector3f Position
Definition basic_types.h:36
@ 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.
bool isfinite(const std::vector< T, Ts... > &v)
Definition algorithm.h:366
float cellSize
How big each cell is in the uniform grid.
Definition Costmap.h:31
A box obstacle in the ground plane, optionally rotated about its own centre.
Definition Scene.h:41
float yaw
Rotation about the box centre [rad].
Definition Scene.h:52
How far a velocity profile asks for more than the device command ramp can deliver.
bool terminalVelocityExceedsBoundary
Whether the profile ends above boundaryVelocity, i.e. faster than by design.
float worstDemand
Largest demanded tangential acceleration [mm/s^2], and where along the path it is.
float worstLimit
The bound that was exceeded there [mm/s^2].
std::vector< std::pair< float, float > > spans
Arc-length spans in which the demand exceeds the ramp, for the plot.
std::size_t violations
Waypoints demanding more than the ramp can deliver.
float terminalStoppingDistance
Distance the ramp needs to stop from the profile's final velocity [mm].
Everything the application needs, as read from the scene description file.
Definition Scene.h:71
core::GeneralConfig generalConfig
Definition Scene.h:85
global_planning::SPFAParams plannerParams
Definition Scene.h:83
core::TrajectoryParametrization parametrization
How the velocities along the planned path are (re-)assigned before simulating.
Definition Scene.h:101
float parametrizationTorqueFraction
Fraction of the motor torque the reparametrization may use.
Definition Scene.h:114
simulation::TrajectoryFollowingSimulation::Parameters simulationParams
Definition Scene.h:90
The synthetic scene: what the world looks like, independent of the planner.
Definition Scene.h:60
algorithms::SceneBounds bounds
Definition Scene.h:61
float cellSize
Edge length of a costmap cell [mm].
Definition Scene.h:64
std::vector< Box > obstacles
Definition Scene.h:66
algorithms::spfa::ShortestPathFasterAlgorithm::Parameters algo
Definition SPFA.h:67
bool enableFinalVelocityClamp
Re-apply the obstacle-aware velocity limit after orientation optimization.
Definition SPFA.h:85
bool enablePositionSmoothing
Diagnostic switches for the post-processing stages.
Definition SPFA.h:77
float motorMaxTorque
Torque bound per motor [N m]. Default is the 7.0 A MaxCurrent cap x 62.4 mNm/A.
float gearRatio
Motor revolutions per wheel revolution. 65536 counts/wheel-rev / 4096 counts/motor-rev.
PlatformDynamics dynamics
What the simulated mass can actually deliver.
CommandRateLimit rateLimit
The device-side command ramp between the controller and the platform.
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.