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"}, {"min", toJson(obstacle.min)}, {"max", toJson(obstacle.max)}});
93 }
94
95 return nlohmann::json{
96 {"bounds",
97 nlohmann::json{{"min", toJson(scene.bounds.min)},
98 {"max", toJson(scene.bounds.max)}}},
99 {"cell_size", scene.cellSize},
100 {"obstacles", jObstacles}};
101 }
102
103 nlohmann::json
104 toJson(const algorithms::Costmap& costmap)
105 {
106 const algorithms::Costmap::Grid& grid = costmap.getGrid();
107
108 nlohmann::json jGrid = nlohmann::json::array();
109 for (int ix = 0; ix < grid.rows(); ix++)
110 {
111 std::vector<float> row(static_cast<std::size_t>(grid.cols()));
112 for (int iy = 0; iy < grid.cols(); iy++)
113 {
114 row[static_cast<std::size_t>(iy)] = grid(ix, iy);
115 }
116
117 jGrid.push_back(row);
118 }
119
120 return nlohmann::json{
121 {"cell_size", costmap.params().cellSize},
122 {"robot_radius", costmap.params().robotRadius},
123 {"bounds",
124 nlohmann::json{{"min", toJson(costmap.getLocalSceneBounds().min)},
125 {"max", toJson(costmap.getLocalSceneBounds().max)}}},
126 {"rows", grid.rows()},
127 {"cols", grid.cols()},
128 {"grid", jGrid}};
129 }
130
131 /// A velocity counts as violating the limit only beyond this relative slack.
132 constexpr float violationTolerance = 1e-3F;
133
134 /// ... and beyond this absolute slack [mm/s].
135 ///
136 /// The limit is sampled from a costmap with finite cells, so moving a waypoint by a
137 /// fraction of a cell changes the limit it is compared against. A resampled trajectory
138 /// therefore reports a handful of violations of one or two mm/s that say nothing about
139 /// the planner -- they are the difference between two lookups, not a real excess.
140 constexpr float violationToleranceAbsolute = 5.F;
141
142 nlohmann::json
145 {
146 nlohmann::json jPoints = nlohmann::json::array();
147
148 std::size_t violations = 0;
149 float maxViolation = 0.F;
150
151 for (const core::GlobalTrajectoryPoint& point : trajectory.points())
152 {
153 const core::Position& position = point.waypoint.pose.translation();
154 const float permissible = limit.at(Eigen::Vector2f{position.head<2>()});
155 const float excess = point.velocity - permissible;
156
157 if (excess > std::max(violationTolerance * permissible,
158 violationToleranceAbsolute))
159 {
160 violations++;
161 maxViolation = std::max(maxViolation, excess);
162 }
163
164 jPoints.push_back(nlohmann::json{{"x", position.x()},
165 {"y", position.y()},
166 {"yaw", yawOf(point.waypoint.pose)},
167 {"velocity", point.velocity},
168 {"velocity_limit", permissible}});
169 }
170
171 return nlohmann::json{
172 {"points", jPoints},
173 {"length", trajectory.length()},
174 {"duration",
176 {"velocity_limit_violations", violations},
177 {"max_velocity_limit_violation", maxViolation}};
178 }
179
180 nlohmann::json
182 const simulation::PlatformDynamics& dynamics)
183 {
184 nlohmann::json jSamples = nlohmann::json::array();
185
186 for (const auto& sample : simulated.samples)
187 {
188 const core::Position& position = sample.global_T_robot.translation();
189
190 jSamples.push_back(
191 nlohmann::json{{"t", sample.time},
192 {"x", position.x()},
193 {"y", position.y()},
194 {"yaw", yawOf(sample.global_T_robot)},
195 {"speed", sample.speed},
196 {"vx", sample.velocityGlobal.x()},
197 {"vy", sample.velocityGlobal.y()},
198 {"commanded_speed", sample.commandedVelocityGlobal.norm()},
199 {"angular_velocity", sample.commandedAngular},
200 {"required_linear_acceleration",
201 sample.requiredLinearAcceleration},
202 {"linear_acceleration", sample.linearAcceleration},
203 {"required_angular_acceleration",
204 sample.requiredAngularAcceleration},
205 {"angular_acceleration", sample.angularAcceleration},
206 {"acceleration_saturated", sample.accelerationSaturated},
207 {"required_wheel_accelerations", toJson(sample.requiredWheelAccelerations)},
208 {"wheel_velocities", toJson(sample.wheelVelocities)},
209 {"wheel_saturated", toJsonFlags(sample.wheelSaturated)},
210 {"required_motor_torques", toJson(sample.requiredMotorTorques)},
211 {"applied_motor_torques", toJson(sample.appliedMotorTorques)},
212 {"motor_saturated", toJsonFlags(sample.motorSaturated)},
213 {"wheel_speed_saturated", toJsonFlags(sample.wheelSpeedSaturated)},
214 {"drop_point_velocity", sample.dropPointVelocity},
215 {"tracking_error", sample.trackingError},
216 {"distance_to_goal", sample.distanceToGoal},
217 {"orientation_error", sample.orientationError}});
218 }
219
220 const std::string modelName = [&]() -> std::string
221 {
222 switch (dynamics.model)
223 {
225 return "omni_wheel_torque";
227 return "mecanum_torque";
229 return "omni_wheel_velocity";
231 return "cartesian";
232 }
233
234 return "unknown";
235 }();
236
237 return nlohmann::json{
238 {"samples", jSamples},
239 {"reached_goal", simulated.reachedGoal},
240 {"settled_short_of_goal", simulated.settledShortOfGoal},
241 {"final_distance_to_goal", simulated.finalDistanceToGoal},
242 {"max_goal_overshoot", simulated.maxGoalOvershoot},
243 {"max_required_linear_acceleration", simulated.maxRequiredLinearAcceleration},
244 {"max_required_angular_acceleration", simulated.maxRequiredAngularAcceleration},
245 {"saturated_cycles", simulated.saturatedCycles},
246 {"motor_torque_saturated_cycles", simulated.motorTorqueSaturatedCycles},
247 {"wheel_speed_saturated_cycles", simulated.wheelSpeedSaturatedCycles},
248 {"max_required_motor_torque", simulated.maxRequiredMotorTorque},
249 {"platform_mass", simulated.platformMass},
250 {"platform_yaw_inertia", simulated.platformYawInertia},
251 {"mass_matrix",
252 {{simulated.massMatrix(0, 0),
253 simulated.massMatrix(0, 1),
254 simulated.massMatrix(0, 2)},
255 {simulated.massMatrix(1, 0),
256 simulated.massMatrix(1, 1),
257 simulated.massMatrix(1, 2)},
258 {simulated.massMatrix(2, 0),
259 simulated.massMatrix(2, 1),
260 simulated.massMatrix(2, 2)}}},
261 {"max_tracking_error", simulated.maxTrackingError},
262 {"max_linear_acceleration", dynamics.maxLinearAcceleration},
263 {"max_angular_acceleration", dynamics.maxAngularAcceleration},
264 {"model", modelName},
265 {"max_wheel_acceleration", dynamics.omniWheel.maxWheelAcceleration},
266 {"max_wheel_deceleration", dynamics.omniWheel.maxWheelDeceleration},
267 {"motor_max_torque", dynamics.omniWheelTorque.motorMaxTorque},
268 {"gear_ratio", dynamics.omniWheelTorque.gearRatio}};
269 }
270
271 } // namespace
272
273 void
274 writeResult(const std::filesystem::path& filename,
275 const Config& config,
276 const algorithms::Costmap& costmap,
277 const std::optional<global_planning::GlobalPlannerResult>& result,
279 const std::optional<simulation::TrajectoryFollowingSimulation::Result>& simulated,
280 const nlohmann::json& reference,
281 const CommandRampCheck& rampCheck,
282 const double parametrizationSeconds)
283 {
284 nlohmann::json j{
285 {"success", result.has_value()},
286 {"scene", toJson(config.scene)},
287 {"start", toJson(config.start)},
288 {"goal", toJson(config.goal)},
289 {"costmap", toJson(costmap)},
290 {"planner",
291 nlohmann::json{
292 {"obstacle_distance_costs", config.plannerParams.algo.obstacleDistanceCosts},
293 {"obstacle_cost_exponent", config.plannerParams.algo.obstacleCostExponent},
294 {"obstacle_max_distance", config.plannerParams.algo.obstacleMaxDistance},
295 {"obstacle_distance_weight", config.plannerParams.algo.obstacleDistanceWeight},
296 {"max_linear_velocity", config.generalConfig.maxVel.linear},
297 {"max_angular_velocity", config.generalConfig.maxVel.angular},
298 {"enable_position_smoothing", config.plannerParams.enablePositionSmoothing},
299 {"enable_final_velocity_clamp",
301 {"parametrization", toString(config.parametrization)}}}};
302
303 if (result.has_value())
304 {
305 j["trajectory"] = toJson(result->trajectory, limit);
306 j["timings"] = result->timings;
307
308 // Deliberately not folded into `timings`: that is the planner's own stage
309 // breakdown, which the reader sums, and the parametrization runs after plan().
310 j["parametrization_seconds"] = parametrizationSeconds;
311
312 // The bounds the reparametrization actually solved against, as opposed to the
313 // configured ones they are derived from. They still differ enough to mislead: the
314 // acceleration bound is min(accel, decel) -- deceleration is deliberately not spent
315 // here, it is reserved for the reactive safety guard -- against the 500 mm/s^2 the
316 // simulation reports as its own limit, and the wheel bound is referred to the wheel
317 // rather than the motor. Without these a reader cannot tell which constraint shaped
318 // the profile.
319 const auto& ramp = config.simulationParams.rateLimit;
320 const auto& drive = config.simulationParams.dynamics.omniWheelTorque;
321
322 const float inputSpeedRpm =
323 std::min(drive.motorNominalSpeedRpm, drive.gearboxMaxInputSpeedRpm);
324
325 j["parametrization_limits"] = nlohmann::json{
326 {"max_velocity", config.generalConfig.maxVel.linear},
327 {"max_angular_velocity", config.generalConfig.maxVel.angular},
328 {"max_acceleration", ramp.maxAcceleration},
329 {"max_deceleration", ramp.maxDeceleration},
330 // Radius of the disc the acceleration is held inside. Approximated by an
331 // inscribed polygon of `acceleration_polygon_sides`; keep that in sync with
332 // `N_DIRECTIONS` in the python package's `reparametrization/acceleration.py`.
333 {"acceleration_radius", std::min(ramp.maxAcceleration, ramp.maxDeceleration)},
334 {"acceleration_polygon_sides", 32},
335 // Which acceleration the radius bounds. The ramp acts on the commanded *body*
336 // twist, so bounding the world frame would leave the real limit exceeded.
337 {"acceleration_frame", "body"},
338 {"max_wheel_velocity",
339 inputSpeedRpm / 60.F / drive.gearRatio * 2.F * static_cast<float>(M_PI)},
340 {"motor_max_torque",
341 drive.motorMaxTorque * config.parametrizationTorqueFraction}};
342
343 nlohmann::json jGridPath = nlohmann::json::array();
344 for (const core::Position& position : result->gridPath)
345 {
346 jGridPath.push_back(
347 nlohmann::json{{"x", position.x()}, {"y", position.y()}});
348 }
349 j["grid_path"] = jGridPath;
350 j["position_smoothing_applied"] = result->positionSmoothingApplied;
351 }
352
353 if (simulated.has_value())
354 {
355 j["simulation"] = toJson(simulated.value(), config.simulationParams.dynamics);
356 }
357
358 {
359 nlohmann::json jSpans = nlohmann::json::array();
360 for (const auto& [from, to] : rampCheck.spans)
361 {
362 jSpans.push_back(nlohmann::json::array({from, to}));
363 }
364
365 j["velocity_profile_exceeds_command_ramp"] =
366 nlohmann::json{{"violations", rampCheck.violations},
367 {"worst_demand", rampCheck.worstDemand},
368 {"worst_limit", rampCheck.worstLimit},
369 {"worst_arc_length", rampCheck.worstArcLength},
370 {"terminal_stopping_distance",
371 rampCheck.terminalStoppingDistance},
372 {"terminal_velocity_exceeds_boundary",
374 {"spans", jSpans}};
375 }
376
377 if (not reference.is_null())
378 {
379 j["reparametrization_reference"] = reference;
380 }
381
382 std::ofstream ofs{filename};
383 ARMARX_CHECK(ofs.good()) << "Cannot write to " << QUOTED(filename.string()) << ".";
384
385 ofs << j.dump(2) << std::endl;
386 }
387
388} // 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:274
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.
float cellSize
How big each cell is in the uniform grid.
Definition Costmap.h:31
An axis-aligned box obstacle in the ground plane.
Definition Scene.h:41
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:63
core::GeneralConfig generalConfig
Definition Scene.h:77
global_planning::SPFAParams plannerParams
Definition Scene.h:75
core::TrajectoryParametrization parametrization
How the velocities along the planned path are (re-)assigned before simulating.
Definition Scene.h:93
float parametrizationTorqueFraction
Fraction of the motor torque the reparametrization may use.
Definition Scene.h:106
simulation::TrajectoryFollowingSimulation::Parameters simulationParams
Definition Scene.h:82
The synthetic scene: what the world looks like, independent of the planner.
Definition Scene.h:52
algorithms::SceneBounds bounds
Definition Scene.h:53
float cellSize
Edge length of a costmap cell [mm].
Definition Scene.h:56
std::vector< Box > obstacles
Definition Scene.h:58
algorithms::spfa::ShortestPathFasterAlgorithm::Parameters algo
Definition SPFA.h:53
bool enableFinalVelocityClamp
Re-apply the obstacle-aware velocity limit after orientation optimization.
Definition SPFA.h:71
bool enablePositionSmoothing
Diagnostic switches for the post-processing stages.
Definition SPFA.h:63
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.