Scene.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 "Scene.h"
23
24#include "reparametrization.h"
25
26#include <algorithm>
27#include <fstream>
28#include <limits>
29#include <stdexcept>
30#include <string>
31
32#include <SimoxUtility/json/json.hpp>
33
37
39
41{
42
43 namespace
44 {
45
46 Eigen::Vector2f
47 toVector2f(const nlohmann::json& j)
48 {
49 ARMARX_CHECK_EQUAL(j.size(), 2) << "Expected a two-element array, got " << j.dump();
50 return Eigen::Vector2f{j.at(0).get<float>(), j.at(1).get<float>()};
51 }
52
54 toPose(const nlohmann::json& j)
55 {
56 const float yaw = j.value("yaw", 0.F);
57
58 core::Pose pose = core::Pose::Identity();
59 pose.translation() << j.at("x").get<float>(), j.at("y").get<float>(), 0.F;
60 pose.linear() = Eigen::AngleAxisf(yaw, Eigen::Vector3f::UnitZ()).toRotationMatrix();
61
62 return pose;
63 }
64
65 } // namespace
66
67 float
68 Box::distanceTo(const Eigen::Vector2f& p) const
69 {
70 // Rotate the query point into the box's own frame, about the box centre, so the
71 // axis-aligned test below applies to the rotated box too. Exactly the old behaviour
72 // when yaw is zero.
73 Eigen::Vector2f q = p;
74 if (yaw != 0.F)
75 {
76 const Eigen::Vector2f centre = 0.5F * (min + max);
77 q = Eigen::Rotation2Df{-yaw} * (p - centre) + centre;
78 }
79
80 // Component-wise overshoot beyond the box on either side; zero inside.
81 const Eigen::Vector2f d = (min - q).cwiseMax(q - max).cwiseMax(Eigen::Vector2f::Zero());
82 return d.norm();
83 }
84
85 Config
86 readConfig(const std::filesystem::path& filename)
87 {
88 ARMARX_CHECK(std::filesystem::exists(filename))
89 << "Scene description " << QUOTED(filename.string()) << " does not exist.";
90
91 std::ifstream ifs{filename};
92 const nlohmann::json j = nlohmann::json::parse(ifs);
93
94 Config config;
95
96 // Scene.
97 {
98 const nlohmann::json& jScene = j.at("scene");
99
100 config.scene.bounds.min = toVector2f(jScene.at("bounds").at("min"));
101 config.scene.bounds.max = toVector2f(jScene.at("bounds").at("max"));
102 config.scene.cellSize = jScene.at("cell_size").get<float>();
103
105 ARMARX_CHECK_GREATER(config.scene.bounds.max.x(), config.scene.bounds.min.x());
106 ARMARX_CHECK_GREATER(config.scene.bounds.max.y(), config.scene.bounds.min.y());
107
108 for (const nlohmann::json& jObstacle : jScene.at("obstacles"))
109 {
110 const std::string type = jObstacle.value("type", std::string{"box"});
111 if (type != "box")
112 {
113 throw std::invalid_argument("Unsupported obstacle type `" + type +
114 "`. Only `box` is supported.");
115 }
116
117 const Box box{.min = toVector2f(jObstacle.at("min")),
118 .max = toVector2f(jObstacle.at("max")),
119 .yaw = jObstacle.value("yaw", 0.F)};
120
121 ARMARX_CHECK_GREATER_EQUAL(box.max.x(), box.min.x());
122 ARMARX_CHECK_GREATER_EQUAL(box.max.y(), box.min.y());
123
124 config.scene.obstacles.push_back(box);
125 }
126 }
127
128 config.start = toPose(j.at("start"));
129 config.goal = toPose(j.at("goal"));
130
131 // Costmap.
132 {
133 const nlohmann::json jCostmap = j.value("costmap", nlohmann::json::object());
134
135 config.costmapParams.cellSize = config.scene.cellSize;
136 config.costmapParams.robotRadius = jCostmap.value("robot_radius", 0.F);
137 config.costmapParams.binaryGrid = false;
138
139 config.costmapMaxDistance = jCostmap.value("max_distance", config.costmapMaxDistance);
140
142 }
143
144 // Planner.
145 {
146 const nlohmann::json jSpfa = j.value("spfa", nlohmann::json::object());
147 auto& algo = config.plannerParams.algo;
148
150 jSpfa.value("obstacle_distance_costs", algo.obstacleDistanceCosts);
151 algo.obstacleCostExponent =
152 jSpfa.value("obstacle_cost_exponent", algo.obstacleCostExponent);
153 algo.obstacleMaxDistance =
154 jSpfa.value("obstacle_max_distance", algo.obstacleMaxDistance);
155 algo.obstacleDistanceWeight =
156 jSpfa.value("obstacle_distance_weight", algo.obstacleDistanceWeight);
157
159 jSpfa.value("enable_position_smoothing",
162 jSpfa.value("enable_final_velocity_clamp",
164 }
165
166 // General navigation stack config.
167 {
168 const nlohmann::json jGeneral = j.value("general", nlohmann::json::object());
169 core::GeneralConfig& general = config.generalConfig;
170
171 general.maxVel.linear = jGeneral.value("max_linear_velocity", general.maxVel.linear);
172 general.maxVel.angular = jGeneral.value("max_angular_velocity", general.maxVel.angular);
173 general.enableRampingStart =
174 jGeneral.value("enable_ramping_start", general.enableRampingStart);
175 general.enableRampingEnd =
176 jGeneral.value("enable_ramping_end", general.enableRampingEnd);
177 general.enableRampingCorners =
178 jGeneral.value("enable_ramping_corners", general.enableRampingCorners);
179 general.rampLength = jGeneral.value("ramp_length", general.rampLength);
180 general.cornerVelocity = jGeneral.value("corner_velocity", general.cornerVelocity);
181 general.boundaryVelocity =
182 jGeneral.value("boundary_velocity", general.boundaryVelocity);
183 general.cornerLimit = jGeneral.value("corner_limit", general.cornerLimit);
184 }
185
186 // Low-level controller simulation.
187 {
188 const nlohmann::json jSim = j.value("simulation", nlohmann::json::object());
189 auto& sim = config.simulationParams;
190 auto& ctrl = sim.controller;
191
192 config.simulate = jSim.value("enabled", config.simulate);
193
194 sim.dt = jSim.value("dt", sim.dt);
195 sim.maxDuration = jSim.value("max_duration", sim.maxDuration);
196 sim.goalDistanceThreshold =
197 jSim.value("goal_distance_threshold", sim.goalDistanceThreshold);
198
199 ctrl.alpha = jSim.value("alpha", ctrl.alpha);
200 ctrl.velocityFactor = jSim.value("velocity_factor", ctrl.velocityFactor);
201 ctrl.maxSegmentsAhead = jSim.value("max_segments_ahead", ctrl.maxSegmentsAhead);
202 ctrl.orientationWeight = jSim.value("orientation_weight", ctrl.orientationWeight);
203 ctrl.coupleLinearAndAngularLimits =
204 jSim.value("couple_linear_and_angular_limits",
205 ctrl.coupleLinearAndAngularLimits);
206 ctrl.enableAngularFeedforward =
207 jSim.value("enable_angular_feedforward", ctrl.enableAngularFeedforward);
208
209 ctrl.pidPos.Kp = jSim.value("pid_pos_kp", ctrl.pidPos.Kp);
210 ctrl.pidPos.Ki = jSim.value("pid_pos_ki", ctrl.pidPos.Ki);
211 ctrl.pidPos.Kd = jSim.value("pid_pos_kd", ctrl.pidPos.Kd);
212 ctrl.pidOri.Kp = jSim.value("pid_ori_kp", ctrl.pidOri.Kp);
213 ctrl.pidOri.Ki = jSim.value("pid_ori_ki", ctrl.pidOri.Ki);
214 ctrl.pidOri.Kd = jSim.value("pid_ori_kd", ctrl.pidOri.Kd);
215
216 // The controller's own limits default to the navigation stack's.
217 ctrl.limits.linear = jSim.value("limit_linear", config.generalConfig.maxVel.linear);
218 ctrl.limits.angular = jSim.value("limit_angular", config.generalConfig.maxVel.angular);
219
220 // What the simulated mass can deliver comes from the package data directory; the
221 // scene may override it to compare against an unconstrained base.
222 // Selects config/platform/PlatformDynamics<Robot>.json, spelled as the rest of
223 // the codebase does: `Armar7`, `Armar6`, `ArmarDE`.
224 config.robot = jSim.value("robot", std::string{"Armar7"});
226 sim.dynamics.maxLinearAcceleration =
227 jSim.value("max_linear_acceleration", sim.dynamics.maxLinearAcceleration);
228 sim.dynamics.maxAngularAcceleration =
229 jSim.value("max_angular_acceleration", sim.dynamics.maxAngularAcceleration);
230
231 // Device-side command ramp. Defaults mirror Armar7a/parts/Platform.xml.
232 {
233 const nlohmann::json jRamp =
234 jSim.value("rate_limit", nlohmann::json::object());
235 auto& ramp = sim.rateLimit;
236
237 ramp.enabled = jRamp.value("enabled", ramp.enabled);
238 ramp.dt = jRamp.value("dt", ramp.dt);
239 ramp.maxVelocity = jRamp.value("max_velocity", ramp.maxVelocity);
240 ramp.maxAcceleration = jRamp.value("max_acceleration", ramp.maxAcceleration);
241 ramp.maxDeceleration = jRamp.value("max_deceleration", ramp.maxDeceleration);
242 ramp.maxAngularVelocity =
243 jRamp.value("max_angular_velocity", ramp.maxAngularVelocity);
244 ramp.maxAngularAcceleration =
245 jRamp.value("max_angular_acceleration", ramp.maxAngularAcceleration);
246 ramp.maxAngularDeceleration =
247 jRamp.value("max_angular_deceleration", ramp.maxAngularDeceleration);
248 ramp.directionPreserving =
249 jRamp.value("direction_preserving", ramp.directionPreserving);
250 }
251
252 if (jSim.contains("model"))
253 {
254 const std::string model = jSim.at("model");
255 if (model == "omni_wheel_torque")
256 {
258 }
259 else if (model == "omni_wheel_velocity")
260 {
262 }
263 else if (model == "mecanum_torque")
264 {
265 sim.dynamics.model = simulation::PlatformModel::MecanumTorque;
266 }
267 else if (model == "cartesian")
268 {
269 sim.dynamics.model = simulation::PlatformModel::Cartesian;
270 }
271 else
272 {
273 // Previously an unrecognised name silently became Cartesian, which looks
274 // like a working run against the wrong model -- exactly how `mecanum_torque`
275 // went unnoticed here after being added to the config parser.
276 throw std::invalid_argument(
277 "Unknown platform model `" + model +
278 "`. Expected `cartesian`, `omni_wheel_velocity`, `omni_wheel_torque` "
279 "or `mecanum_torque`.");
280 }
281 }
282 }
283
284 // Velocity parametrization.
285 {
286 const nlohmann::json jParam =
287 j.value("reparametrization", nlohmann::json::object());
288
289 config.parametrization =
290 parametrizationModeFromString(jParam.value("mode", std::string{"none"}));
292 jParam.value("num_samples", config.parametrizationSamples);
294 jParam.value("torque_fraction", config.parametrizationTorqueFraction);
295 }
296
297 return config;
298 }
299
301 buildCostmap(const Config& config)
302 {
303 // Reuse the builder's sizing so the grid dimensions cannot drift from what the
304 // costmap's index <-> position math expects.
306 config.scene.bounds, config.costmapParams);
307
308 ARMARX_INFO << "Costmap grid: " << grid.rows() << " rows (x) x " << grid.cols()
309 << " cols (y), cell size " << config.costmapParams.cellSize << " mm.";
310
311 const float cellSize = config.costmapParams.cellSize;
312 const Eigen::Vector2f& boundsMin = config.scene.bounds.min;
313
314 for (int ix = 0; ix < grid.rows(); ix++)
315 {
316 for (int iy = 0; iy < grid.cols(); iy++)
317 {
318 // Rows correspond to x, columns to y (see Costmap::toPositionLocal).
319 const Eigen::Vector2f cellCenter{
320 boundsMin.x() + static_cast<float>(ix) * cellSize + cellSize / 2,
321 boundsMin.y() + static_cast<float>(iy) * cellSize + cellSize / 2};
322
323 float distance = std::numeric_limits<float>::max();
324 for (const Box& obstacle : config.scene.obstacles)
325 {
326 distance = std::min(distance, obstacle.distanceTo(cellCenter));
327 }
328
329 const float clearance = distance - config.costmapParams.robotRadius;
330 grid(ix, iy) = std::clamp(clearance, 0.F, config.costmapMaxDistance);
331 }
332 }
333
334 return algorithms::Costmap(grid, config.costmapParams, config.scene.bounds);
335 }
336
337} // namespace armarx::navigation::analysis
#define QUOTED(x)
static Eigen::MatrixXf createUniformGrid(const SceneBounds &sceneBounds, const Costmap::Parameters &parameters)
#define ARMARX_CHECK_GREATER(lhs, rhs)
This macro evaluates whether lhs is greater (>) than rhs and if it turns out to be false it will thro...
#define ARMARX_CHECK(expression)
Shortcut for ARMARX_CHECK_EXPRESSION.
#define ARMARX_CHECK_GREATER_EQUAL(lhs, rhs)
This macro evaluates whether lhs is greater or equal (>=) rhs and if it turns out to be false it will...
#define ARMARX_CHECK_EQUAL(lhs, rhs)
This macro evaluates whether lhs is equal (==) rhs and if it turns out to be false it will throw an E...
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define q
This file is part of ArmarX.
Definition io.cpp:36
core::TrajectoryParametrization parametrizationModeFromString(const std::string &name)
Parse the --parametrization choice. Throws on an unknown name.
Config readConfig(const std::filesystem::path &filename)
Read the scene description. Throws on malformed input.
Definition Scene.cpp:86
algorithms::Costmap buildCostmap(const Config &config)
Build a costmap holding, per cell, the distance to the closest obstacle reduced by the robot radius (...
Definition Scene.cpp:301
Eigen::Isometry3f Pose
Definition basic_types.h:31
@ 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.
double distance(const Point &a, const Point &b)
Definition point.hpp:95
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 distanceTo(const Eigen::Vector2f &p) const
Distance from p to the box surface. Zero if p is inside the box.
Definition Scene.cpp:68
float yaw
Rotation about the box centre [rad].
Definition Scene.h:52
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
int parametrizationSamples
Waypoints the reparametrization should return.
Definition Scene.h:111
bool simulate
Simulate the low-level controller following the planned trajectory.
Definition Scene.h:88
std::string robot
Platform name selecting PlatformDynamics<robot>.json, spelled as the rest of the codebase does: Armar...
Definition Scene.h:95
algorithms::Costmap::Parameters costmapParams
Definition Scene.h:78
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
float costmapMaxDistance
Obstacle distances are clipped to this value [mm].
Definition Scene.h:81
simulation::TrajectoryFollowingSimulation::Parameters simulationParams
Definition Scene.h:90
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
float boundaryVelocity
Speed the profile is floored at [mm/s].
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
static PlatformDynamics FromPackagePath(const std::string &robot="Armar7")
Read the dynamics from config/platform/PlatformDynamics<Robot>.json.
traj_ctrl::global::TrajectoryFollowingControllerParams controller
Parameters of the trajectory following controller, including alpha and limits.