main.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
17#include <chrono>
18#include <cstdlib>
19#include <filesystem>
20#include <fstream>
21#include <iostream>
22#include <optional>
23#include <random>
24#include <string>
25#include <vector>
26
27#include <boost/program_options.hpp>
28
32
36
38#include "Metrics.h"
39#include "RoomScene.h"
40#include "Sampling.h"
41
42namespace po = boost::program_options;
43
44using namespace armarx::navigation;
45
46namespace
47{
49 toLogLevel(const std::string& name)
50 {
51 if (name == "debug")
53 if (name == "verbose")
55 if (name == "info")
57 if (name == "important")
59 if (name == "warn")
61 if (name == "error")
63
64 throw std::invalid_argument{"Unknown log level `" + name + "`."};
65 }
66
67 std::filesystem::path
68 resolvePriorKnowledge(const std::string& override)
69 {
70 if (not override.empty())
71 {
72 return override;
73 }
74
75 // The package layout puts the data one level below the package root.
76 return armarx::PackagePath("PriorKnowledgeData", "").toSystemPath();
77 }
78
79 using SmoothingParams = decltype(algorithms::spfa::smoothing::io::loadSmoothingParams());
80
81 /// Apply the `--corridor`, `--serpentine` and `--clutter` scene modifiers, in that order.
82 void
83 shapeScene(const po::variables_map& vm,
84 const SmoothingParams& smoothingParams,
85 analysis::Config& config,
86 evaluation::RoomScene& roomScene,
87 std::mt19937& rng)
88 {
89 const float robotRadius = config.costmapParams.robotRadius;
90
91 if (const float corridorWidth = vm["corridor"].as<float>(); corridorWidth > 0.F)
92 {
93 const evaluation::CorridorParameters parameters{
94 .width = corridorWidth, .length = vm["corridor-length"].as<float>()};
95
96 roomScene.corridor = evaluation::makeCorridor(config.scene, parameters);
97
98 const float clearance = 0.5F * corridorWidth - robotRadius;
99
100 ARMARX_IMPORTANT << "Corridor: " << corridorWidth << " mm wide, " << parameters.length
101 << " mm long, leaving a " << robotRadius << " mm robot " << clearance
102 << " mm either side.";
103
104 if (clearance <= static_cast<float>(smoothingParams.min_clearance))
105 {
106 ARMARX_WARNING << "That is at or below the " << smoothingParams.min_clearance
107 << " mm the trajectory checker enforces, so no path along the "
108 "corridor can be accepted however well it is smoothed. Use a "
109 "width above "
110 << 2.F * (robotRadius +
111 static_cast<float>(smoothingParams.min_clearance))
112 << " mm.";
113 }
114 }
115
116 if (const float passageWidth = vm["serpentine"].as<float>(); passageWidth > 0.F)
117 {
118 const evaluation::SerpentineParameters parameters{
119 .passageWidth = passageWidth, .walls = vm["serpentine-walls"].as<int>()};
120
121 const float clearance =
122 evaluation::makeSerpentine(config.scene, parameters, robotRadius);
123
124 ARMARX_IMPORTANT << "Serpentine: " << parameters.walls << " walls, " << passageWidth
125 << " mm openings, tightest clearance " << clearance << " mm.";
126
127 if (clearance <= static_cast<float>(smoothingParams.clearance))
128 {
129 ARMARX_WARNING << "That is at or below the " << smoothingParams.clearance
130 << " mm the trajectory checker demands, so every path through "
131 "an opening is rejected however well it is smoothed. This "
132 "sweep would measure the scene, not the planner. Use openings "
133 "wider "
134 << 2.F *
135 (robotRadius + static_cast<float>(smoothingParams.clearance))
136 << " mm.";
137 }
138 }
139
140 if (const int clutter = vm["clutter"].as<int>(); clutter > 0)
141 {
142 const evaluation::ClutterParameters parameters{.count = clutter,
143 .minGap = vm["clutter-gap"].as<float>()};
144
145 // A gap of g leaves a robot of radius r only g/2 - r of clearance on the centre
146 // line. Below the checker's clearance every path through a clutter passage is
147 // rejected no matter how good the smoothing is, and the sweep measures the scene
148 // rather than the planner.
149 const float required =
150 2.F * (robotRadius + static_cast<float>(smoothingParams.clearance));
151
152 if (parameters.minGap <= required)
153 {
155 << "clutter-gap is " << parameters.minGap << " mm, which leaves a "
156 << robotRadius << " mm robot only " << (0.5F * parameters.minGap - robotRadius)
157 << " mm of clearance -- below the " << smoothingParams.clearance
158 << " mm the trajectory checker demands. Every path through a clutter passage "
159 "will be rejected regardless of smoothing quality, so the rejection rate "
160 "from this sweep says nothing about the smoother. Use more than "
161 << required << " mm.";
162 }
163
164 const int placed = evaluation::addClutter(config.scene, parameters, rng);
165
166 ARMARX_IMPORTANT << "Clutter: placed " << placed << " of " << clutter
167 << " boxes at a minimum gap of " << parameters.minGap << " mm."
168 << (placed < clutter ? " The room saturated before the rest fit."
169 : "");
170 }
171 }
172
173 /// The near-obstacle clearance band, from the command line or derived from the smoother.
175 samplingParameters(const po::variables_map& vm, const SmoothingParams& smoothingParams)
176 {
177 // By default the band starts just above the smoother's target clearance, which keeps
178 // every endpoint above the (lower) limit the checker enforces, so the category
179 // stresses the smoother rather than generating requests it cannot satisfy.
181 const float bandMin = vm["endpoint-clearance-min"].as<float>();
182 sampling.nearObstacleMinClearance =
183 bandMin >= 0.F ? bandMin : static_cast<float>(smoothingParams.clearance) + 5.F;
184
185 const float bandMax = vm["endpoint-clearance-max"].as<float>();
186 sampling.nearObstacleMaxClearance =
187 bandMax >= 0.F ? bandMax : sampling.nearObstacleMinClearance + 300.F;
188
189 return sampling;
190 }
191
192 /// Scene geometry and the first few planned paths, for plotting.
193 class PathDump
194 {
195 public:
196 PathDump(const analysis::Scene& scene, const int maxPaths) : maxPaths_{maxPaths}
197 {
198 nlohmann::json obstacles = nlohmann::json::array();
199 for (const analysis::Box& box : scene.obstacles)
200 {
201 obstacles.push_back({{"min", {box.min.x(), box.min.y()}},
202 {"max", {box.max.x(), box.max.y()}},
203 {"yaw", box.yaw}});
204 }
205
206 dump_["obstacles"] = obstacles;
207 dump_["bounds"] = {{"min", {scene.bounds.min.x(), scene.bounds.min.y()}},
208 {"max", {scene.bounds.max.x(), scene.bounds.max.y()}}};
209 dump_["paths"] = nlohmann::json::array();
210 }
211
212 /// Keep the path until `maxPaths` have been collected.
213 void
214 add(const evaluation::Request& request, const global_planning::GlobalPlannerResult& result)
215 {
216 if (static_cast<int>(dump_["paths"].size()) >= maxPaths_)
217 {
218 return;
219 }
220
221 nlohmann::json points = nlohmann::json::array();
222 for (const auto& point : result.trajectory.points())
223 {
224 points.push_back(
225 {point.waypoint.pose.translation().x(), point.waypoint.pose.translation().y()});
226 }
227
228 dump_["paths"].push_back({{"kind", toString(request.kind)},
229 {"index", request.index},
230 {"smoothed", result.positionSmoothingApplied},
231 {"points", points}});
232 }
233
234 void
235 write(const std::string& path) const
236 {
237 std::ofstream file{path};
238 file << dump_.dump() << std::endl;
239 ARMARX_IMPORTANT << "Wrote scene and " << dump_["paths"].size() << " paths to `" << path
240 << "`.";
241 }
242
243 private:
244 int maxPaths_;
245
246 nlohmann::json dump_;
247 };
248} // namespace
249
250int
251main(int argc, char** argv)
252try
253{
254 po::options_description desc("Offline sweep of planning requests against a real room.");
255
256 // clang-format off
257 desc.add_options()
258 ("help,h", "produce help message")
259 ("prior-knowledge", po::value<std::string>()->default_value(""),
260 "Path to PriorKnowledgeData's data directory. Resolved from the package if omitted.")
261 ("room", po::value<std::string>()->default_value("50_19/R007"),
262 "Room within the scene collections.")
263 ("scenes", po::value<std::vector<std::string>>()->multitoken()
264 ->default_value({"static_scene", "mobile_kitchen"}, "static_scene mobile_kitchen"),
265 "Scene descriptions to merge.")
266 ("uniform", po::value<int>()->default_value(100), "Uniform free-space requests.")
267 ("near-obstacle", po::value<int>()->default_value(200), "Near-obstacle requests.")
268 ("locations", po::value<int>()->default_value(100), "Named-location requests.")
269 ("seed", po::value<unsigned>()->default_value(1), "RNG seed; a sweep is reproducible.")
270 ("corridor", po::value<float>()->default_value(0.F),
271 "Replace the room's contents with a dead-end corridor of this clear width [mm] "
272 "opening into the rest of the room. Requests then start inside it and finish outside, "
273 "which is the case a start pinned in tight space produces and no other scene here "
274 "does. 0 leaves the room as furnished.")
275 ("corridor-length", po::value<float>()->default_value(5000.F),
276 "How far the corridor runs into the room [mm].")
277 ("corridor-exits", po::value<int>()->default_value(0),
278 "Corridor-exit requests to draw.")
279 ("serpentine", po::value<float>()->default_value(0.F),
280 "Replace the room's contents with transverse walls whose openings alternate sides, "
281 "each opening this wide [mm]. Forces a path to thread tight passages and reverse "
282 "direction repeatedly. 0 leaves the room as furnished.")
283 ("serpentine-walls", po::value<int>()->default_value(6),
284 "Number of transverse walls in the serpentine scene.")
285 ("endpoint-clearance-min", po::value<float>()->default_value(-1.F),
286 "Lower bound [mm] of the clearance band near-obstacle endpoints are drawn from. "
287 "Negative ties it to the smoother's target, which excludes endpoints the planner "
288 "cannot satisfy; set it lower to include them and see how the rejection rate depends "
289 "on how close to furniture the robot is asked to start and finish.")
290 ("endpoint-clearance-max", po::value<float>()->default_value(-1.F),
291 "Upper bound [mm] of that band. Negative uses the lower bound plus 300.")
292 ("no-smoothing", po::bool_switch(),
293 "Bypass the smoothing stage entirely, so the sweep measures the raw resampled path.")
294 ("dump-scene", po::value<std::string>()->default_value(""),
295 "Write the scene geometry and the first few planned paths here, for plotting.")
296 ("dump-count", po::value<int>()->default_value(6), "How many paths to dump.")
297 ("resample", po::value<float>()->default_value(-1.F),
298 "Waypoint spacing [mm] before smoothing. Negative uses the planner's default.")
299 ("clutter", po::value<int>()->default_value(0),
300 "Extra boxes scattered through the room to create tight passages. 0 leaves the room "
301 "as furnished.")
302 ("clutter-gap", po::value<float>()->default_value(1000.F),
303 "Gap [mm] clutter must leave to every other obstacle. Must exceed "
304 "2 * (robot radius + smoothing clearance) or the passages it creates cannot be "
305 "traversed at all; a warning says so.")
306 ("cell-size", po::value<float>()->default_value(100.F), "Costmap cell size [mm].")
307 ("robot-radius", po::value<float>()->default_value(350.F),
308 "Robot radius [mm]. ARMAR-7 is circular; the static costmap inflates the 3D model "
309 "geometry by this.")
310 ("log-level", po::value<std::string>()->default_value("important"),
311 "debug|verbose|info|important|warn|error. A sweep provokes planning failures by "
312 "design, and each one logs a backtrace at error level.")
313 ("out", po::value<std::string>()->default_value("evaluation.jsonl"),
314 "Per-request records (JSON lines).")
315 ;
316 // clang-format on
317
318 po::variables_map vm;
319 po::store(po::parse_command_line(argc, argv, desc), vm);
320
321 if (vm.count("help") != 0u)
322 {
323 std::cout << desc << "\n";
324 return EXIT_SUCCESS;
325 }
326
327 po::notify(vm);
328
329 armarx::LogSender::SetGlobalMinimumLoggingLevel(toLogLevel(vm["log-level"].as<std::string>()));
330
331 const std::filesystem::path priorKnowledge =
332 resolvePriorKnowledge(vm["prior-knowledge"].as<std::string>());
333 const std::string room = vm["room"].as<std::string>();
334
335 ARMARX_INFO << "Reading `" << room << "` from `" << priorKnowledge.string() << "`.";
336
338 priorKnowledge, room, vm["scenes"].as<std::vector<std::string>>());
339
340 ARMARX_IMPORTANT << "Scene: " << roomScene.scene.obstacles.size() << " obstacles (walls "
341 << "included), " << roomScene.locations.size() << " named locations.";
342
343 for (const std::string& warning : roomScene.importWarnings)
344 {
345 ARMARX_WARNING << "Scene import -- " << warning;
346 }
347
348 analysis::Config config;
349 config.scene = roomScene.scene;
350 config.costmapParams.cellSize = vm["cell-size"].as<float>();
351 config.costmapParams.robotRadius = vm["robot-radius"].as<float>();
352
353 // Seeded before the scene is finished, so the clutter and the requests come from one
354 // reproducible stream.
355 std::mt19937 rng{vm["seed"].as<unsigned>()};
356
357 const SmoothingParams smoothingParams = algorithms::spfa::smoothing::io::loadSmoothingParams();
358
359 shapeScene(vm, smoothingParams, config, roomScene, rng);
360
361 const algorithms::Costmap costmap = analysis::buildCostmap(config);
362
363 // The pipeline under test: the same SPFAImpl the navigation stack runs.
364 config.plannerParams.resampleDistance = vm["resample"].as<float>();
365 config.plannerParams.enablePositionSmoothing = not vm["no-smoothing"].as<bool>();
366
368 {
369 ARMARX_IMPORTANT << "Smoothing disabled: measuring the raw resampled path.";
370 }
371
372 global_planning::SPFAImpl planner(config.plannerParams, config.generalConfig, costmap);
373
374 const evaluation::SamplingParameters sampling = samplingParameters(vm, smoothingParams);
375
376 ARMARX_IMPORTANT << "Near-obstacle endpoints drawn from clearance ["
377 << sampling.nearObstacleMinClearance << ", "
378 << sampling.nearObstacleMaxClearance << "] mm.";
379
380 std::vector<evaluation::Request> requests;
381 for (const auto& [kind, count] :
382 {std::pair{evaluation::RequestKind::Uniform, vm["uniform"].as<int>()},
383 std::pair{evaluation::RequestKind::NearObstacle, vm["near-obstacle"].as<int>()},
384 std::pair{evaluation::RequestKind::Location, vm["locations"].as<int>()},
385 std::pair{evaluation::RequestKind::CorridorExit, vm["corridor-exits"].as<int>()}})
386 {
387 if (count <= 0)
388 {
389 continue;
390 }
391
392 const std::vector<evaluation::Request> sampled =
393 evaluation::sampleRequests(kind, count, costmap, roomScene, sampling, rng);
394 requests.insert(requests.end(), sampled.begin(), sampled.end());
395 }
396
397 ARMARX_IMPORTANT << "Planning " << requests.size() << " requests ...";
398
399 const std::filesystem::path outFilename = vm["out"].as<std::string>();
400 std::ofstream out{outFilename};
401 if (not out.good())
402 {
403 throw std::runtime_error{"Cannot write to `" + outFilename.string() + "`."};
404 }
405
406 const std::string dumpPath = vm["dump-scene"].as<std::string>();
407 std::optional<PathDump> dump;
408 if (not dumpPath.empty())
409 {
410 dump.emplace(config.scene, vm["dump-count"].as<int>());
411 }
412
413 std::vector<evaluation::Record> records;
414 records.reserve(requests.size());
415
416 for (const evaluation::Request& request : requests)
417 {
418 const auto before = std::chrono::steady_clock::now();
419
420 std::optional<global_planning::GlobalPlannerResult> result;
421 try
422 {
423 result = planner.plan(request.start, request.goal);
424 }
425 catch (const std::exception& e)
426 {
427 // One unplannable request must not end the sweep -- the interesting sweeps are
428 // exactly the ones that provoke failures.
429 ARMARX_WARNING << "Request " << request.index << " (" << toString(request.kind)
430 << ") threw: " << e.what();
431 }
432
433 const float seconds =
434 std::chrono::duration<float>(std::chrono::steady_clock::now() - before).count();
435
436 if (dump.has_value() and result.has_value())
437 {
438 dump->add(request, result.value());
439 }
440
441 evaluation::Record record = evaluation::measure(request, result, costmap, seconds);
442 evaluation::writeRecord(out, record);
443 records.push_back(std::move(record));
444 }
445
446 out.flush();
447
448 if (dump.has_value())
449 {
450 dump->write(dumpPath);
451 }
452
453 ARMARX_IMPORTANT << "Wrote " << records.size() << " records to `" << outFilename.string()
454 << "`.";
455
456 // Against the limit the gate enforces (`min_clearance`), not the target the smoother's
457 // obstacle barrier aims for (`clearance`).
458 std::cout << evaluation::summarize(records, static_cast<float>(smoothingParams.min_clearance))
459 << std::endl;
460
461 return EXIT_SUCCESS;
462}
463catch (const std::exception& e)
464{
465 ARMARX_ERROR << e.what();
466 return EXIT_FAILURE;
467}
static void SetGlobalMinimumLoggingLevel(MessageTypeT level)
With setGlobalMinimumLoggingLevel the minimum verbosity-level of log-messages can be set for the whol...
static std::filesystem::path toSystemPath(const data::PackagePath &pp)
const std::vector< GlobalTrajectoryPoint > & points() const
std::optional< GlobalPlannerResult > plan(const core::Pose &start, const core::Pose &goal)
Definition SPFA.cpp:255
void dump(vec_point_2d &vec)
Definition gdiam.cpp:1666
#define ARMARX_INFO
The normal logging level.
Definition Logging.h:179
#define ARMARX_IMPORTANT
The logging level for always important information, but expected behaviour (in contrast to ARMARX_WAR...
Definition Logging.h:188
#define ARMARX_ERROR
The logging level for unexpected behaviour, that must be fixed.
Definition Logging.h:194
#define ARMARX_WARNING
The logging level for unexpected behaviour, but not a serious problem.
Definition Logging.h:191
void write(WriterT &aron_w, const Eigen::Matrix< EigenT, rows, cols, options > &input, typename WriterT::ReturnType &ret, const armarx::aron::Path &aron_p=armarx::aron::Path())
Definition eigen.h:138
SmoothingParams loadSmoothingParams(const std::string &filePath)
Definition io.cpp:29
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
std::vector< Request > sampleRequests(const RequestKind kind, const int count, const algorithms::Costmap &costmap, const RoomScene &room, const SamplingParameters &parameters, std::mt19937 &rng)
Draw count requests of the given kind.
Definition Sampling.cpp:88
Record measure(const Request &request, const std::optional< global_planning::GlobalPlannerResult > &result, const algorithms::Costmap &costmap, const float plannedSeconds)
Measure a planning result against the costmap.
Definition Metrics.cpp:137
algorithms::SceneBounds makeCorridor(analysis::Scene &scene, const CorridorParameters &parameters)
A dead-end corridor opening into the rest of the room.
float makeSerpentine(analysis::Scene &scene, const SerpentineParameters &parameters, const float robotRadius)
Replace the room's contents with transverse walls whose openings alternate sides.
void writeRecord(std::ostream &out, const Record &record)
One JSON object per line, so a sweep streams to disk and can be diffed across revisions.
Definition Metrics.cpp:245
@ NearObstacle
Start and goal drawn from cells whose clearance sits just above zero, so the poses hug obstacle edges...
Definition Sampling.h:40
@ CorridorExit
Start inside a narrow corridor, goal out in the open.
Definition Sampling.h:55
@ Location
Pairs of poses from the room's navigation graph – the requests the robot really receives here.
Definition Sampling.h:44
@ Uniform
Start and goal drawn uniformly from the cells in open space, i.e.
Definition Sampling.h:36
RoomScene buildRoomScene(const std::filesystem::path &priorKnowledge, const std::string &room, const std::vector< std::string > &sceneFiles, const float wallThickness)
Build the scene for a room from PriorKnowledgeData.
int addClutter(analysis::Scene &scene, const ClutterParameters &parameters, std::mt19937 &rng)
Scatter boxes through the room to create tight passages.
This file is part of ArmarX.
const char * toString(InteractionFeedbackType type)
Definition Interaction.h:28
MessageTypeT
Definition LogSender.h:46
bool empty(const std::string &s)
Definition cxxopts.hpp:234
float cellSize
How big each cell is in the uniform grid.
Definition Costmap.h:31
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
algorithms::Costmap::Parameters costmapParams
Definition Scene.h:78
algorithms::SceneBounds bounds
Definition Scene.h:61
std::vector< Box > obstacles
Definition Scene.h:66
What one planning request produced.
Definition Metrics.h:36
int index
Index within the requests of the same kind, so a record can be traced back to its inputs.
Definition Sampling.h:66
The room as the planner sees it, assembled from PriorKnowledgeData.
Definition RoomScene.h:49
std::vector< NamedPose > locations
Poses from navigation-graphs/<room>/locations.json.
Definition RoomScene.h:55
std::optional< algorithms::SceneBounds > corridor
Set when the scene is a corridor, so requests can be drawn as exits from it.
Definition RoomScene.h:62
analysis::Scene scene
The first roomWallCount obstacles are always the room walls, which is what lets the synthetic scenes ...
Definition RoomScene.h:52
std::vector< std::string > importWarnings
Import problems: objects and locations dropped, and objects kept with an approximated footprint.
Definition RoomScene.h:59
float nearObstacleMinClearance
Clearance band [mm] a cell must fall in to count as "near an obstacle".
Definition Sampling.h:88
float nearObstacleMaxClearance
Upper bound of that band, and lower bound of the Uniform category.
Definition Sampling.h:91
bool positionSmoothingApplied
Whether the position smoothing stage ran and its result was adopted.
float resampleDistance
Spacing [mm] the planned path is resampled to before smoothing.
Definition SPFA.h:62
bool enablePositionSmoothing
Diagnostic switches for the post-processing stages.
Definition SPFA.h:77