In this lesson, we will learn how to use the PPL framework to create motion
planning methods. Planning methods are implemented as MPStrategy objects,
which call on other abstractions such as LocalPlanners and
DistanceMetrics to perform various functions. Here we will start with a
blank strategy called
MyStrategy and fill in the content for a simple planner that creates four
configurations connected in series and outputs both a .map file and the
Euclidean length of the chain.
-
Set up MyStrategy in PPL
The first step is to put the blank strategy into PPL and set it up to compile
and run.
- Download MyStrategy.h and place it in your
src/MPLibrary/MPStrategies directory.
- We want to be able to test our method as we go, so it's best to set up the
compilation support right away. Go to src/Traits/ and open the file
CfgTraits.h. This file defines the list of things that exist in PPL.
We need to include our header file with the other MPStrategies.
Use your editor's find command (for vim, type /MPStrategies) to locate the
MPStrategies includes. Add a line for your file in alphabetical order:
- #include "MPLibrary/MPStrategies/MyStrategy.h"
- Now scroll down and look at the MPTraits struct. Find the list of
strategies and add a line for MyStrategy:
- MyStrategy<MPTraits>,
Now your file has been included in PPL and will be compiled with
everything else, so go ahead and save and exit.
- Now, recompile PPL to incorporate MyStrategy.
You should not have any compilation errors; if you did get some, look at the
error message to find the file and line number with the error. Check that line
to make sure that there are no typos or other simple mistakes. Be sure to follow
the same formatting as you see in the file. Try to recompile; if you still get
errors, ask for help.
- Next, we need to tell PPL to use MyStrategy. PPL uses an XML file
as input to set all sorts of variables like which Environment file to
use, which MPStrategy to map it with, which Sampler the
MPStrategy should use, etc. This information is recorded in XML nodes,
which look like this:
- <nodetag variable=stuff/> --or--
<nodetag> stuff </nodetag>
Go to src/Examples and open the file CfgExamples.xml. This file
includes an example node for most of the things in PPL. Look around to see how
this is organized. Now go to the bottom of the file (type GG in vim) and add a
node for MyStrategy above the closing tag of MPStrategies
like so:
- <MyStrategy label="MyStrategy"/>
This sets up MyStrategy as an MPStrategy in this XML file. Note
that it is not necessary to recompile PPL after editing the XML file.
- CfgExamples includes nodes for many MPStrategies, but only
one will be executed when we run the program. To set MyStrategy as the
method to use, find the XML node called Solver - it should be the last
node in the file. In the Solver node, change the label
mpStrategyLabel to mpStrategyLabel="MyStrategy".
- Save and quit, and get ready for testing!
-
Test MyStrategy
Thus far, we have added the file MyStrategy.h, included it in
MPTraits, and created an XML node in order to run our method.
We can now test our (blank) MyStrategy to see that PPL can execute it.
Go to ppl/ and execute PPL using the file Examples/CfgExamples.xml
with the following command:
- ./ppl_mp -f Examples/CfgExamples.xml
You should see the message MPLibrary is solving with MPStrategyMethod labeled
MyStrategy using seed 12345678. In this case, MyStrategy is a blank
method and does nothing, so PPL essentially loads the files and then terminates.
This is good news though, because we now have a blank, testable method that is
ready to build on.
-
A look at MPStrategy
Before we start adding things to MyStrategy, lets take a look at how it
works.
- Open the file again in a text editor. The first thing to note is that
MyStrategy is templated off of MPTraits. This is true for almost
everything in PPL because MPTraits defines what objects are included in
our world.
- Also notice that MyStrategy inherits MPStrategyMethod, which
is the base class for all MPStrategies. This common base class enables
PPL to interact with all MPStrategies through the same interface,
provided by MPStrategyMethod.
- Next you will see several typdefs, which are included for convenience
and code readability. Then we have two constructors, one each for XML and
non-XML intialization, followed by MyStrategy's versions of the
MPStrategyMethod functions ParseXML, Print,
Initialize, Run, and Finalize.
- These MPStrategyMethod functions define the interface for
MPStrategies. They need to be implemented for each MPStrategy so
that PPL knows what to do with it:
- Print is used by other methods that need to get the name and label
information of MyStrategy.
- ParseXML is intended to be used in conjunction with the XML
constructor in order to extract parameter information from an XML node.
- The functions Initialize, Run, and Finalize are called
when PPL executes MyStrategy, in that order. This is where the planning
will happen: we use Initialize to set up any supporting variables or
objects, Run to execute the planning methods, and Finalize to
export output files, such as maps and/or stats.
-
Set up your Infrastructure
Now that we know roughly how an MPStrategy works, lets make a simple planner
that creates four nodes (Cfgs) connected in series and outputs the Euclidean
distance of the resulting path. Such a path for a real system would represent
four configurations that the robot could reach in succession. To do this, we
will use the following objects:
- CfgType - to create the nodes
- ValidityChecker - to check that the nodes are not in collision
- DistanceMetric - to calculate the Euclidean distance
- LocalPlanner - to check the connections between nodes
- GraphType - to store the resulting Roadmap so we can check it
in Vizmo later
- If we start typing a huge block of code in the Run method, our code
will be very hard to read and debug, especially for other people. Since all code
is shared in our lab, we need to write our methods so that other people can
understand and maintain them. Therefore, we will organize our work into two
auxiliary methods called helper functions to make reading and debugging
easier:
- GenerateNode - returns a valid CfgType
- ConnectNode - tries to connect a valid CfgType to the end of
the chain, returns true or false to indicate success or failure
- Lets begin by creating the declarations and definitions for these functions.
Helper functions are only for internal use, so we want to declare them as
protected. Find the declaration of Finalize in the class
definition. Skip a line and add protected just like you see done at the
top with public. Under protected, declare the helper functions:
CfgType GenerateNode();
bool ConnectNode(CfgType& _c);
- Now go to the bottom of the file and add empty definitions for each:
template<class MPTraits>
typename MPTraits::CfgType
MyStrategy<MPTraits>::
GenerateNode() {
}
The typename business with CfgType is needed because the typedef in the class
definition is not in this scope. Write the ConnectNode definition similarly for
its parameters (no typename... required).
- Next, we need to create member variables to store the labels of the
ValidityChecker, DistanceMetric, and LocalPlanner we want MyStrategy to use. Go
back to the class definition and add three private member variables to store
these:
string m_vcLabel;
string m_dmLabel;
string m_lpLabel;
- To populate those variables, we will need to read in data from the XML file. The
purpose in doing things this way is that changing which ValidityChecker or which
DistanceMetric you use is reduced to a one-word substitution in the XML file. To
see how to set it up, look at ParseXML in
src/MPLibrary/MPStrategies/VisibilityBasedPRM.h:
- The entries there for m_vcLabel and m_lpLabel are the same as what
we need for MyStrategy.
- The four parameters for the string parser are: XML label,
required, default, type. Compare what you see here to the
VisibilityBasedPRM node in CfgExamples.xml to see what this is
reading.
- Duplicate the parsing for m_vcLabel and m_lpLabel in
MyStrategy, and create the same for m_dmLabel based on those
examples.
- Change the default parameter for m_vcLabel from "" to
"pqp_solid", and make m_dmLabel's default "euclidean". Now
set the required parameter to false for all three of the m_ labels so that we can use the
default values.
- Finally, we need to tell the XML constructor to use this information. At the
end of the XML constructor, add a call to our parsing method with
- ParseXML(_node);
- The variables we created will now be read from the XML file if the
corresponding XML label is present in the MyStrategy node. If
it is not, the default value will be used. If you wanted to change the
DistanceMetric to manhattan later on, all you would need to do is add
dmLabel="manhattan" to the MyStrategy XML node.
-
Write GenerateNode
Now that we have inputs configured and access to some abstractions, we can write
the content for GenerateNode. This method will make a single node using
the Cfg class (which is typedefed here as CfgType) and
collision check it with the ValidityChecker stored in MPProblem
(remember that MPProblem is the container that holds everything for this
problem). We can get access to these objects through the MPBaseObject (from which
MPStrategyMethod inherits) by using the this pointer.
- In the body of GenerateNode, get a pointer to the ValidityChecker and
Environment, and create a new CfgType:
auto vc = this->GetValidityChecker(m_vcLabel);
auto env = this->GetEnvironment();
CfgType newCfg(this->GetTask()->GetRobot());
- Now we want to generate a random CfgType until we find one that is
inside the bounding box and not in collision. Use a basic loop to randomly
generate a new Cfg until a good one is found:
do {
newCfg.GetRandomCfg(this->GetEnvironment()->GetBoundary());
} while(!newCfg.InBounds(env) ||
!vc->IsValid(newCfg, this->GetNameAndLabel()));
return newCfg;
Take a close look at the while condition. At a high level, it basically says
that if the node is either out of bounds or invalid, then resample the node.
- GenerateNode is now a simple, one-node sampler that we can use inside Run to
keep things orderly and readable. Before moving on, add some comments to explain
what you are doing so that others can work with your code.
-
Test your work
With GenerateNode finished, now is a good time to check that your code compiles.
Before we do, we need to give ConnectNode a return value. Just set it to return
false so that we can test what we have done so far. Also, you might want to add
a cout statement to Initialize so that you can see that your
method is working:
cout << "Some cheesy but safe for work message :)" << endl;
Then recompile your code. Handling errors in small batches will
help you improve debugging your time and maintain your sanity. Fix any compile
errors before moving on. You can also test that it still executes if you want to
see your message.
-
Write ConnectNode
Now we can write the other core method, ConnectNode. We want this method to test
if the input node can be connected to the last node in the chain. If it can, we
want to add the node and corresponding edges to the Roadmap and return
true. If not, it needs to ignore the node and return false. In order
to do this, we will need to keep track of the last node that was added to the
Roadmap. We will also need a way to store the total length of the chain.
- Make private member variables in the class definition:
CfgType m_lastNode to hold a copy of the last node in the map, and
double m_length to hold the total chain length.
We also want to initialize those variables when the method is constructed.
Include a default initialization for m_lastNode and m_length in both
constructors:
MyStrategy() : m_lastNode(), m_length(0.) {
and
MyStrategy(XMLNode& _node)
: MPStrategyMethod(_node), m_lastNode(),
m_length(0.) {
- In Initialize(), initialize m_lastNode to an empty CfgType object:
- m_lastNode = CfgType(this->GetTask()->GetRobot());
- Now go to ConnectNode. First we need to setup pointers to access the
Environment, the Graph, the DistanceMetric, and the
LocalPlanner. Go through the get-chain to acquire these:
auto lp = this-> GetLocalPlanner(m_lpLabel);
auto dm = this-> GetDistanceMetric(m_dmLabel);
auto r = this->GetRoadmap();
auto env = this->GetEnvironment();
Again, remember that MPLibrary stores all of this stuff: we are going
through our lowest-level base class (MPBaseObject) to access it.
- Now we want to test if this is the first node: if it is, it should be added
to the Roadmap right away (it was already determined valid in
GenerateNode, and there's nothing for it to connect to). Afterward, we
need to update the information for the last node in the chain:
CfgType blank(this->GetTask()->GetRobot());
if(m_lastNode == blank) {
r->AddVertex(_c);
m_lastNode = _c;
return true;
}
The if line checks if m_lastNode still holds the default value, which is a
configuration with a 0 value for all degrees of freedom. If it has something
other than the default value, then some other node has already been added to the
chain.
Note: This test could fail if the previous CfgType was sampled smack on
the default values, but this is extremely unlikely in our case as we are working in a
6-DOF environment where each coordinate is represented as a double. Since the
probability of sampling all six coordinates at exactly 0 is extremely small, we
will ignore the issue in this tutorial.
- For subsequent nodes, we need to discern whether this node can be connected to
m_lastNode. To do this, we need a few temporary variables:
LPOutput<MPTraits> lpOutput;
auto robot = this->GetTask()->GetRobot();
CfgType collisionCfg(robot);
These are needed by the LocalPlanner to store the output edge and the
Cfg where collision occured in the event of failure.
- Now we can test the connection with the LocalPlanner, and add the
new node and edge tp the Roadmap if it succeeds. We can also use
this opportunity to add the distance between the nodes to the running total:
if(lp->IsConnected(m_lastNode, _c, collisionCfg, &lpOutput,
env->GetPositionRes(), env->GetOrientationRes())) {
m_length += dm->Distance(m_lastNode, _c);
VID newNode = r->AddVertex(_c);
r->AddEdge(r->GetVID(m_lastNode), newNode, lpOutput.m_edge);
m_lastNode = _c;
return true;
}
- The LocalPlanner call takes quite a few variables. Here is a short run-down:
- The first two Cfgs are the Cfgs that lp is trying to
connect.
- The next, collisionCfg, is used to store the first configuration
along the local path that is found to be in collision; we don't need this for
our method, but IsConnected requires it, so we just use a dummy Cfg as
a place holder.
- The next parameter is an LPOutput object, which stores the calculated local
path. We use this to add successful edges to the graph.
- Finally, we have the position and orientation resolutions, which let the
local planner know what step size to use when checking the connection.
- Assuming all goes well, _c is reachable from m_lastNode, and
IsConnected returns true. We then enter into the if block,
add the distance to the total, and add the new node and edge to our Roadmap.
- If ConnectNode hasn't returned after the if block, then
_c is not the first node, and that it can't be connected to
m_lastNode. In this case, we do nothing with _c and return
false:
- return false;
- Great, now ConnectNode is complete! Take a moment to comment your code. Good
comments here would describe each of the three cases and possible returns in 1-2
lines per case.
-
Test your work
Once your code is commented, test that it compiles and fix any errors. We are
now finished with the algorithmic methods, so all that remains is to call those
methods, do some book keeping, and make output files!
-
Use GenerateNode and ConnectNode to write Run
Lets put GenerateNode and ConnectNode together now to complete our
algorithm.
- Go to the Run function and set up access to the RoadmapType
pointer, just as you did in ConnectNode. Name the pointer r again
for consistency.
- Next, let's create a temporary int variable to set the number of nodes
we want:
size_t numNodes = 4;
- Now we want to use our helper functions to continuously generate a new node and
try to connect it to the chain until we have four nodes in our map. A while
loop provides us with a simple and elegant means to express this:
while(r->get_num_vertices() < numNodes) {
CfgType newCfg = GenerateNode();
ConnectNode(newCfg);
}
- That's it: our helper functions have encapsulated all of the messy details into
single-statement ideas, which makes our Run function clean and easy to
understand. If someone else read our code, it would only take a few seconds for
them to figure out the basic structure of our method. Add some brief comments if
you think it would help readability.
-
Add Output Generation
We're in the home stretch now: all that remains is to produce an output file for
our Roadmap and to print the chain length.
- Since we want to produce output after Run completes, lets head to the
Finalize method. Start by outputting the path length with a simple
cout statement:
cout << "MyStrategy Path Length: " << m_length << endl;
- Now lets handle the .map file. We always start with the base
filename, which is owned by the base class MPStrategyMethod, and we
export .map files as "filename.map":
string fileName = this->GetBaseFilename() + ".map";
- Now we will call on the Roadmap class to write its data
to complete the Finalize method:
this->GetRoadmap()->Write(fileName, this->GetEnvironment());
- Hurrah! MyStrategy will now print the chain length and produce a
.map file that we can look at in Vizmo to see our results. Before we
recompile and test our work, we have one last task to perform: all classes in
PPL are required to define their Print function to return their name and
label. Go to the Print method and fill in the bare-bones minimum
Print function:
_os << this->GetNameAndLabel() << endl;
- The end is near. Recompile and debug your code, then you are ready to try it
out!
-
Run MyStrategy
- It's finally time to see MyStrategy in action. Go to
ppl/ and run PPL using Examples/CfgExamples.xml as before:
./ppl_mp -f Examples/CfgExamples.xml
- You should see some output messages, including your cout line in
Initialize (if you made one) and the "MyStrategy Path Length" as created
in Finalize. Type ls to verify that you have a .map file in this
directory (it should be example.12345678.query.map).
Note: The "example" part comes from the XML label baseFilename in the
Solver node in CfgExamples.xml, while the 12345678 part is generated
from the seed label in the same node.
- Now we can use Vizmo to see the fruits of our labor. From
src/Examples, type vizmo++ to open Vizmo. Click open file and select
your .map file.
- Once the files are loaded, use Show/Hide Roadmap on the Roadmap
tool tab to see the Roadmap we made. You can also select Robot
mode to view the actual robot positions instead of box-points. You should now
see four valid configurations connected in series. If you see something else or
an error occurs, double check for typos and simple mistakes before asking for
help.
Once you get the expected output, you are finished! Congratulations on
completing your first MPStrategy!
-
Self-check
Before you move on, make sure that you understand the main points of this
lesson:
- MPStrategies define a way to execute PPL. During runtime, PPL will call
Initialize, Run, and Finalize once each, in that order.
- MPStrategies use objects from MPLibrary like LocalPlanners and
ValidityCheckers, and access those objects through the GetObject functions.
- Wrap up computational ideas like GenerateNode and ConnectNode in helper
functions to make code easier to read and debug.
- To create a new MPStrategy, we need to make a class file, include the header
and class in MPTraits, and create an XML node for the strategy.
- To run an MPStrategy, we set the Solver node in the XML file to refer to our
desired strategy.
- Write small segments of code and then test them right away. This style of
iterative development improves debugging speed and readability. It also makes
it easier for others to help you when you get stuck.
You can learn more about the individual components such as LocalPlanners
through the PPL documentation, which is available under docs, and through
looking at examples of their use in other code. For example,
Connectors use LocalPlanners to connect nodes, so you might find
an example of how to use a LocalPlanner in a Connector class. Keep
both in mind as you will likely need both resources to find the functions and
examples you require.