Thunder.cpp
1 /*********************************************************************
2  * Software License Agreement (BSD License)
3  *
4  * Copyright (c) 2014, JSK, The University of Tokyo.
5  * All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  *
11  * * Redistributions of source code must retain the above copyright
12  * notice, this list of conditions and the following disclaimer.
13  * * Redistributions in binary form must reproduce the above
14  * copyright notice, this list of conditions and the following
15  * disclaimer in the documentation and/or other materials provided
16  * with the distribution.
17  * * Neither the name of the JSK, The University of Tokyo nor the names of its
18  * contributors may be used to endorse or promote products derived
19  * from this software without specific prior written permission.
20  *
21  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24  * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25  * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26  * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27  * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31  * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32  * POSSIBILITY OF SUCH DAMAGE.
33  *********************************************************************/
34 
35 /* Author: Dave Coleman */
36 
37 #include <ompl/tools/thunder/Thunder.h>
38 #include <ompl/geometric/planners/rrt/RRTConnect.h>
39 #include <ompl/base/PlannerStatus.h>
40 #include <ompl/util/Console.h>
41 
42 namespace og = ompl::geometric;
43 namespace ob = ompl::base;
44 namespace ot = ompl::tools;
45 
47  : ompl::tools::ExperienceSetup(si)
48 {
49  initialize();
50 }
51 
53  : ompl::tools::ExperienceSetup(space)
54 {
55  initialize();
56 }
57 
58 void ompl::tools::Thunder::initialize()
59 {
60  OMPL_INFORM("Initializing Thunder Framework");
61 
62  recallEnabled_ = true;
63  scratchEnabled_ = true;
64  filePath_ = "unloaded";
65 
66  // How many threads to plan with scratch (1 or 2)
67  dualThreadScratchEnabled_ = true;
68 
69  // Load the experience database
70  experienceDB_.reset(new ompl::tools::ThunderDB(si_->getStateSpace()));
71 
72  // Load the Retrieve repair database. We do it here so that setRepairPlanner() works
73  rrPlanner_ = ob::PlannerPtr(new og::ThunderRetrieveRepair(si_, experienceDB_));
74 
75  OMPL_INFORM("Thunder Framework initialized.");
76 }
77 
79 {
80  if (!configured_ || !si_->isSetup() || !planner_->isSetup() || !rrPlanner_->isSetup() )
81  {
82  // Setup Space Information if we haven't already done so
83  if (!si_->isSetup())
84  si_->setup();
85 
86  // Setup planning from scratch planner
87  if (!planner_)
88  {
89  if (pa_)
90  planner_ = pa_(si_);
91  if (!planner_)
92  {
93  OMPL_INFORM("Getting default planner: ");
95  // This was disabled because i like to use Thunder / SPARSdb without setting a goal definition
96  //planner_ = ompl::geometric::getDefaultPlanner(pdef_->getGoal()); // we could use the repairProblemDef_ here but that isn't setup yet
97 
98  OMPL_INFORM("No planner specified. Using default: %s", planner_->getName().c_str() );
99  }
100  }
101  planner_->setProblemDefinition(pdef_);
102  if (!planner_->isSetup())
103  planner_->setup();
104 
105  // Decide if we should setup the second planning from scratch planner for benchmarking w/o recall
106  if (dualThreadScratchEnabled_ && !recallEnabled_)
107  {
108  // Setup planning from scratch planner 2
109  if (!planner2_)
110  {
111  if (pa_)
112  planner2_ = pa_(si_);
113  if (!planner2_)
114  {
115  OMPL_INFORM("Getting default planner: ");
117  // This was disabled because i like to use Thunder / SPARSdb without setting a goal definition
118  //planner2_ = ompl::geometric::getDefaultPlanner(pdef_->getGoal()); // we could use the repairProblemDef_ here but that isn't setup yet
119 
120  OMPL_INFORM("No planner 2 specified. Using default: %s", planner2_->getName().c_str() );
121  }
122  }
123  planner2_->setProblemDefinition(pdef_);
124  if (!planner2_->isSetup())
125  planner2_->setup();
126 
127  }
128 
129  // Setup planning from experience planner
130  rrPlanner_->setProblemDefinition(pdef_);
131 
132  if (!rrPlanner_->isSetup())
133  rrPlanner_->setup();
134 
135  // Create the parallel component for splitting into two threads
136  pp_ = ot::ParallelPlanPtr(new ot::ParallelPlan(pdef_) );
137  if (!scratchEnabled_ && !recallEnabled_)
138  {
139  throw Exception("Both planning from scratch and experience have been disabled, unable to plan");
140  }
141  if (recallEnabled_)
142  pp_->addPlanner(rrPlanner_); // Add the planning from experience planner if desired
143  if (scratchEnabled_)
144  pp_->addPlanner(planner_); // Add the planning from scratch planner if desired
145  if (dualThreadScratchEnabled_ && !recallEnabled_)
146  {
147  OMPL_INFORM("Adding second planning from scratch planner");
148  pp_->addPlanner(planner2_); // Add a SECOND planning from scratch planner if desired
149  }
150 
151  // Setup SPARS
152  if (!experienceDB_->getSPARSdb())
153  {
154  OMPL_INFORM("Calling setup() for SPARSdb");
155 
156  // Load SPARSdb
157  experienceDB_->getSPARSdb().reset(new ompl::geometric::SPARSdb(si_));
158  experienceDB_->getSPARSdb()->setProblemDefinition(pdef_);
159  experienceDB_->getSPARSdb()->setup();
160 
161  experienceDB_->getSPARSdb()->setStretchFactor(1.2);
162  experienceDB_->getSPARSdb()->setSparseDeltaFraction(0.05); // vertex visibility range = maximum_extent * this_fraction
163  //experienceDB_->getSPARSdb()->setDenseDeltaFraction(0.001);
164 
165  experienceDB_->getSPARSdb()->printDebug();
166 
167  experienceDB_->load(filePath_); // load from file
168  }
169 
170  // Set the configured flag
171  configured_ = true;
172  }
173 }
174 
176 {
177  if (planner_)
178  planner_->clear();
179  if (rrPlanner_)
180  rrPlanner_->clear();
181  if (planner2_)
182  planner2_->clear();
183  if (pdef_)
184  pdef_->clearSolutionPaths();
185  if (pp_)
186  {
187  pp_->clearHybridizationPaths();
188  }
189 }
190 
192 {
193  pa_ = pa;
194  planner_.reset();
195  // note: the rrPlanner_ never uses the allocator so does not need to be reset
196  configured_ = false;
197 }
198 
200 {
201  // we provide a duplicate implementation here to allow the planner to choose how the time is turned into a planner termination condition
202 
203  OMPL_INFORM("Thunder Framework: Starting solve()");
204 
205  // Setup again in case it has not been done yet
206  setup();
207 
208  lastStatus_ = base::PlannerStatus::UNKNOWN;
209  time::point start = time::now();
210 
211  // Warn if there are queued paths that have not been added to the experience database
212  if (!queuedSolutionPaths_.empty())
213  {
214  OMPL_WARN("Previous solved paths are currently uninserted into the experience database and are in the post-proccessing queue");
215  }
216 
217  // There are two modes for running parallel plan - one in which both threads are run until they both return a result and/or fail
218  // The second mode stops with the first solution found - we want this one
219  bool stopWhenFirstSolutionFound = true;
220 
221  if (stopWhenFirstSolutionFound)
222  {
223  // If \e hybridize is false, when the first solution is found, the rest of the planners are stopped as well.
224  //OMPL_DEBUG("Thunder: stopping when first solution is found from either thread");
225  // Start both threads
226  bool hybridize = false;
227  lastStatus_ = pp_->solve(ptc, hybridize);
228  }
229  else
230  {
231  OMPL_WARN("Thunder: not stopping until a solution or a failure is found from both threads. THIS MODE IS JUST FOR TESTING");
232  // This mode is more for benchmarking, since I don't care about optimality
233  // If \e hybridize is false, when \e minSolCount new solutions are found (added to the set of solutions maintained by ompl::base::Goal), the rest of the planners are stopped as well.
234 
235  // Start both threads
236  std::size_t minSolCount = 2;
237  std::size_t maxSolCount = 2;
238  bool hybridize = false;
239  lastStatus_ = pp_->solve(ptc, minSolCount, maxSolCount, hybridize);
240  }
241 
242  // Planning time
243  planTime_ = time::seconds(time::now() - start);
244 
245  // Create log
247  log.planning_time = planTime_;
248 
249  // Record stats
250  stats_.totalPlanningTime_ += planTime_; // used for averaging
251  stats_.numProblems_++; // used for averaging
252 
253  if (lastStatus_ == ompl::base::PlannerStatus::TIMEOUT)
254  {
255  // Skip further processing if absolutely no path is available
256  OMPL_ERROR("Thunder Solve: No solution found after %f seconds", planTime_);
257 
258  stats_.numSolutionsTimedout_++;
259 
260  // Logging
261  log.planner = "neither_planner";
262  log.result = "timedout";
263  log.is_saved = "not_saved";
264  }
265  else if (!lastStatus_)
266  {
267  // Skip further processing if absolutely no path is available
268  OMPL_ERROR("Thunder Solve: Unknown failure");
269  stats_.numSolutionsFailed_++;
270 
271  // Logging
272  log.planner = "neither_planner";
273  log.result = "failed";
274  log.is_saved = "not_saved";
275  }
276  else
277  {
278  OMPL_INFORM("Thunder Solve: Possible solution found in %f seconds", planTime_);
279 
280  // Smooth the result
281  simplifySolution(ptc);
282 
283  og::PathGeometric solutionPath = getSolutionPath(); // copied so that it is non-const
284  OMPL_INFORM("Solution path has %d states and was generated from planner %s", solutionPath.getStateCount(), getSolutionPlannerName().c_str());
285 
286  // Logging
287  log.planner = getSolutionPlannerName();
288 
289  // Do not save if approximate
290  if (!haveExactSolutionPath())
291  {
292  OMPL_INFORM("THUNDER RESULTS: Approximate");
293 
294  // Logging
295  log.result = "not_exact_solution";
296  log.is_saved = "not_saved";
297  log.approximate = true;
298 
299  // Stats
300  stats_.numSolutionsApproximate_++;
301 
302  // TODO not sure what to do here, use case not tested
303  OMPL_WARN("NOT saving to database because the solution is APPROXIMATE");
304  }
305  else if (getSolutionPlannerName() == rrPlanner_->getName())
306  {
307  OMPL_INFORM("THUNDER RESULTS: From Recall");
308 
309  // Stats
310  stats_.numSolutionsFromRecall_++;
311 
312  // Logging
313  log.result = "from_recall";
314 
315  // Make sure solution has at least 2 states
316  if (solutionPath.getStateCount() < 2)
317  {
318  OMPL_INFORM("NOT saving to database because solution is less than 2 states long");
319  stats_.numSolutionsTooShort_++;
320 
321  // Logging
322  log.is_saved = "less_2_states";
323  log.too_short = true;
324 
325  }
326  else if (false) // always add when from recall
327  {
328  OMPL_INFORM("Adding path to database because SPARS will decide for us if we should keep the nodes");
329 
330  // Stats
331  stats_.numSolutionsFromRecallSaved_++;
332 
333  // Queue the solution path for future insertion into experience database (post-processing)
334  queuedSolutionPaths_.push_back(solutionPath);
335 
336  // Logging
337  log.insertion_failed = 0; // TODO this is wrong logging data
338  log.is_saved = "always_attempt";
339  }
340  else // never add when from recall
341  {
342  OMPL_INFORM("NOT adding path to database because SPARS already has it");
343 
344  // Logging
345  log.is_saved = "skipped";
346  }
347  }
348  else
349  {
350  OMPL_INFORM("THUNDER RESULTS: From Scratch");
351 
352  // Logging
353  log.result = "from_scratch";
354 
355  // Stats
356  stats_.numSolutionsFromScratch_++;
357 
358  // Make sure solution has at least 2 states
359  if (solutionPath.getStateCount() < 2)
360  {
361  OMPL_INFORM("NOT saving to database because solution is less than 2 states long");
362 
363  // Logging
364  log.is_saved = "less_2_states";
365  log.too_short = true;
366 
367  // Stats
368  stats_.numSolutionsTooShort_++;
369  }
370  else
371  {
372  OMPL_INFORM("Adding path to database because best solution was not from database");
373 
374  // Logging
375  log.result = "from_scratch";
376  log.is_saved = "saving";
377 
378  // Queue the solution path for future insertion into experience database (post-processing)
379  queuedSolutionPaths_.push_back(solutionPath);
380 
381  log.insertion_failed = 0; // TODO fix this wrong logging info
382  }
383  }
384  }
385 
386  // Final log data
387  //log.insertion_time = insertionTime; TODO fix this
388  log.num_vertices = experienceDB_->getSPARSdb()->getNumVertices();
389  log.num_edges = experienceDB_->getSPARSdb()->getNumEdges();
390  log.num_connected_components = experienceDB_->getSPARSdb()->getNumConnectedComponents();
391 
392  // Flush the log to buffer
393  convertLogToString(log);
394 
395  return lastStatus_;
396 }
397 
399 {
400  ob::PlannerTerminationCondition ptc = ob::timedPlannerTerminationCondition( time );
401  return solve(ptc);
402 }
403 
405 {
406  setup(); // ensure the PRM db has been loaded to the Experience DB
407  return experienceDB_->save(filePath_);
408 }
409 
411 {
412  setup(); // ensure the PRM db has been loaded to the Experience DB
413  return experienceDB_->saveIfChanged(filePath_);
414 }
415 
416 void ompl::tools::Thunder::printResultsInfo(std::ostream &out) const
417 {
418  for (std::size_t i = 0; i < pdef_->getSolutionCount(); ++i)
419  {
420  out << "Solution " << i
421  << "\t | Length: " << pdef_->getSolutions()[i].length_
422  << "\t | Approximate: " << (pdef_->getSolutions()[i].approximate_ ? "true" : "false")
423  << "\t | Planner: " << pdef_->getSolutions()[i].plannerName_ << std::endl;
424  }
425 }
426 
427 void ompl::tools::Thunder::print(std::ostream &out) const
428 {
429  if (si_)
430  {
431  si_->printProperties(out);
432  si_->printSettings(out);
433  }
434  if (planner_)
435  {
436  planner_->printProperties(out);
437  planner_->printSettings(out);
438  }
439  if (rrPlanner_)
440  {
441  rrPlanner_->printProperties(out);
442  rrPlanner_->printSettings(out);
443  }
444  if (planner2_)
445  {
446  planner2_->printProperties(out);
447  planner2_->printSettings(out);
448  }
449  if (pdef_)
450  pdef_->print(out);
451 }
452 
453 void ompl::tools::Thunder::printLogs(std::ostream &out) const
454 {
455  if (!recallEnabled_)
456  out << "Scratch Planning Logging Results (inside Thunder Framework)" << std::endl;
457  else
458  out << "Thunder Framework Logging Results" << std::endl;
459  out << " Solutions Attempted: " << stats_.numProblems_ << std::endl;
460  out << " Solved from scratch: " << stats_.numSolutionsFromScratch_ << " (" << stats_.numSolutionsFromScratch_/stats_.numProblems_*100 << "%)" << std::endl;
461  out << " Solved from recall: " << stats_.numSolutionsFromRecall_ << " (" << stats_.numSolutionsFromRecall_/stats_.numProblems_*100 << "%)" << std::endl;
462  out << " That were saved: " << stats_.numSolutionsFromRecallSaved_ << std::endl;
463  out << " That were discarded: " << stats_.numSolutionsFromRecall_ - stats_.numSolutionsFromRecallSaved_ << std::endl;
464  out << " Less than 2 states: " << stats_.numSolutionsTooShort_ << std::endl;
465  out << " Failed: " << stats_.numSolutionsFailed_ << std::endl;
466  out << " Timedout: " << stats_.numSolutionsTimedout_ << std::endl;
467  out << " Approximate: " << stats_.numSolutionsApproximate_ << std::endl;
468  out << " SPARSdb " << std::endl;
469  out << " Vertices: " << experienceDB_->getSPARSdb()->getNumVertices() << std::endl;
470  out << " Edges: " << experienceDB_->getSPARSdb()->getNumEdges() << std::endl;
471  out << " Connected Components: " << experienceDB_->getSPARSdb()->getNumConnectedComponents() << std::endl;
472  out << " Unsaved paths inserted: " << experienceDB_->getNumPathsInserted() << std::endl;
473  out << " Consecutive state failures: " << experienceDB_->getSPARSdb()->getNumConsecutiveFailures() << std::endl;
474  out << " Connected path failures: " << experienceDB_->getSPARSdb()->getNumPathInsertionFailed() << std::endl;
475  out << " Sparse Delta Fraction: " << experienceDB_->getSPARSdb()->getSparseDeltaFraction() << std::endl;
476  out << " Average planning time: " << stats_.getAveragePlanningTime() << std::endl;
477  out << " Average insertion time: " << stats_.getAverageInsertionTime() << std::endl;
478 }
479 
481 {
482  return experienceDB_->getSPARSdb()->getNumVertices();
483 }
484 
485 void ompl::tools::Thunder::getAllPlannerDatas(std::vector<ob::PlannerDataPtr> &plannerDatas) const
486 {
487  experienceDB_->getAllPlannerDatas(plannerDatas);
488 }
489 
491 {
492  // Convert the planner data verticies into a vector of states
493  for (std::size_t i = 0; i < plannerData->numVertices(); ++i)
494  path.append(plannerData->getVertex(i).getState());
495 }
496 
498 {
499  // Reverse path2 if it matches better
500  const ob::State* s1 = path1.getState(0);
501  const ob::State* s2 = path2.getState(0);
502  const ob::State* g1 = path1.getState(path1.getStateCount()-1);
503  const ob::State* g2 = path2.getState(path2.getStateCount()-1);
504 
505  double regularDistance = si_->distance(s1,s2) + si_->distance(g1,g2);
506  double reversedDistance = si_->distance(s1,g2) + si_->distance(s2,g1);
507 
508  // Check if path is reversed from normal [start->goal] direction
509  if ( regularDistance > reversedDistance )
510  {
511  // needs to be reversed
512  path2.reverse();
513  return true;
514  }
515 
516  return false;
517 }
518 
519 ompl::tools::ThunderDBPtr ompl::tools::Thunder::getExperienceDB()
520 {
521  return experienceDB_;
522 }
523 
525 {
526  OMPL_INFORM("Performing post-processing");
527 
528  for (std::size_t i = 0; i < queuedSolutionPaths_.size(); ++i)
529  {
530  // Time to add a path to experience database
531  double insertionTime;
532 
533  experienceDB_->addPath(queuedSolutionPaths_[i], insertionTime);
534  OMPL_INFORM("Finished inserting experience path in %f seconds", insertionTime);
535  stats_.totalInsertionTime_ += insertionTime; // used for averaging
536  }
537 
538  // Remove all inserted paths from the queue
539  queuedSolutionPaths_.clear();
540 
541  return true;
542 }
This is a utility that allows executing multiple planners in parallel, until one or more find a solut...
Definition: ParallelPlan.h:66
std::size_t getExperiencesCount() const
Get the total number of paths stored in the database.
Definition: Thunder.cpp:480
ompl::tools::ThunderDBPtr getExperienceDB()
Hook for getting access to debug data.
Definition: Thunder.cpp:519
Single entry for the csv data logging file.
void log(const char *file, int line, LogLevel level, const char *m,...)
Root level logging function. This should not be invoked directly, but rather used via a logging macro...
Definition: Console.cpp:120
The planner failed to find a solution.
Definition: PlannerStatus.h:62
A shared pointer wrapper for ompl::base::StateSpace.
bool saveIfChanged()
Save the experience database to file if there has been a change.
Definition: Thunder.cpp:410
Encapsulate a termination condition for a motion planner. Planners will call operator() to decide whe...
virtual base::PlannerStatus solve(double time=1.0)
Run the planner for up to a specified amount of time (default is 1 second)
Definition: Thunder.cpp:398
void append(const base::State *state)
Append state to the end of this path. The memory for state is copied.
SPArse Roadmap Spanner Version 2.0
Definition: SPARSdb.h:88
bool reversePathIfNecessary(ompl::geometric::PathGeometric &path1, ompl::geometric::PathGeometric &path2)
If path1 and path2 have a better start/goal match when reverse, then reverse path2.
Definition: Thunder.cpp:497
virtual void print(std::ostream &out=std::cout) const
Print information about the current setup.
Definition: Thunder.cpp:427
base::State * getState(unsigned int index)
Get the state located at index along the path.
bool doPostProcessing()
Allow accumlated experiences to be processed.
Definition: Thunder.cpp:524
The Thunder Framework's Retrieve-Repair component.
std::size_t getStateCount() const
Get the number of states (way-points) that make up this path.
Thunder(const base::SpaceInformationPtr &si)
Constructor needs the state space used for planning.
Definition: Thunder.cpp:46
duration seconds(double sec)
Return the time duration representing a given number of seconds.
Definition: Time.h:78
std::function< PlannerPtr(const SpaceInformationPtr &)> PlannerAllocator
Definition of a function that can allocate a planner.
Definition: Planner.h:423
A shared pointer wrapper for ompl::base::Planner.
void printLogs(std::ostream &out=std::cout) const
Display debug data about overall results from Thunder since being loaded.
Definition: Thunder.cpp:453
#define OMPL_ERROR(fmt,...)
Log a formatted error string.
Definition: Console.h:64
void reverse()
Reverse the path.
RRT-Connect (RRTConnect)
Definition: RRTConnect.h:61
A class to store the exit status of Planner::solve()
Definition: PlannerStatus.h:48
A shared pointer wrapper for ompl::base::SpaceInformation.
Definition of an abstract state.
Definition: State.h:50
void setPlannerAllocator(const base::PlannerAllocator &pa)
Set the planner allocator to use. This is only used if no planner has been set. This is optional – a ...
Definition: Thunder.cpp:191
Save and load entire paths from file.
Definition: ThunderDB.h:71
#define OMPL_WARN(fmt,...)
Log a formatted warning string.
Definition: Console.h:66
The exception type for ompl.
Definition: Exception.h:47
point now()
Get the current time point.
Definition: Time.h:72
bool save()
Save the experience database to file.
Definition: Thunder.cpp:404
virtual void clear(void)
Clear all planning data. This only includes data generated by motion plan computation. Planner settings, start & goal states are not affected.
Definition: Thunder.cpp:175
void convertPlannerData(const ompl::base::PlannerDataPtr plannerData, ompl::geometric::PathGeometric &path)
Convert PlannerData to PathGeometric. Assume ordering of verticies is order of path.
Definition: Thunder.cpp:490
Definition of a geometric path.
Definition: PathGeometric.h:60
void printResultsInfo(std::ostream &out=std::cout) const
Display debug data about potential available solutions.
Definition: Thunder.cpp:416
std::chrono::system_clock::time_point point
Representation of a point in time.
Definition: Time.h:66
virtual void setup(void)
This method will create the necessary classes for planning. The solve() method will call this functio...
Definition: Thunder.cpp:78
A shared pointer wrapper for ompl::base::PlannerData.
Create the set of classes typically needed to solve a geometric problem.
void getAllPlannerDatas(std::vector< ompl::base::PlannerDataPtr > &plannerDatas) const
Get a vector of all the planning data in the database.
Definition: Thunder.cpp:485
#define OMPL_INFORM(fmt,...)
Log a formatted information string.
Definition: Console.h:68