5) Look ahead (aka motion planning)

The objective of this work is to introduce some key algorithm of motion planning: collision checking, probabilistic roadmaps, visibility PRM, \(A^*\) (a-star), random shortcut.

A robot model with simple (static actuated) dynamics, like a UR5 manipulator robot. See Lab 1 for loading the UR5 robot.

A collision checking library, provided by Pinocchio.

Pinocchio provides collision checking for a large class of 3D objects (sphere, capsule, box, triangle soup) using library FCL. Any 3D object of FCL can be loaded using the C++ interface or the URDF model. Although, only capsules can yet be added through the Python API, and only URDF-loaded meshes and Python-loaded capsules can be easily connected with Gepetto viewer (help is welcome to correct this, it would requires 4h work).

An example of loading a capsule in Pinocchio and Gepetto viewer is below:

obs = se3.GeometryObject.CreateCapsule(rad, length) # Pinocchio obstacle object

obs.name = "obs" # Set object name

obs.parentJoint = 0 # Set object parent = 0 = universe

obs.placement = se3.SE3(rotate('x', .1) * rotate('y', .1) * rotate('z', .1), np.array([.1, .1, .1])) # Set object placement wrt parent

robot.collision_model.addGeometryObject(obs, robot.model, False) # Add object to collision model

robot.visual_model.addGeometryObject(obs, robot.model, False) # Add object to visual model

# Also create a geometric object in gepetto viewer, with according name.

robot.viewer.gui.addCapsule("world/pinocchio/" + obs.name, rad, length, [1.0, 0.2, 0.2, 1.0])

URDF specifications does not allow to define which collision pairs should be tested. By default, Pinocchio does not load any collision pair. A simple strategy is to add all pairs, but often, some meshes of the models induce wrong collision. Then manually remove them by testing valid configurations. To be clean, you can store the valid collision pair in a SRDF file. For UR5:

robot.collision_model.addAllCollisionPairs()

for idx in [56, 35, 23]:

robot.collision_model.removeCollisionPair(robot.collision_model.collisionPairs[idx])

Collision checking are done through the following algorithms:

se3.updateGeometryPlacements(robot.model, robot.data, robot.collision_model, robot.collision_data, q)

se3.computeCollision(robot.collision_model, robot.collision_data, pairId)

se3.computeCollisions(robot.collision_model, robot.collision_data, False)

# last arg to stop early.

Both collision algorithms requires a preliminary update of placement and return `True`

if configuration is in collision (`False`

otherwise).

We need to define a simple function to check whether a configuration is respecting the robot constraints (joint limits and collision, plus any other inequality-defined constraints you might want).

Implement the function `check`

taking a configuration `q`

in argument and return `True`

if and only if `q`

is acceptable – The solution only uses the 2 collision algorithms of Pinocchio listed above and standard python otherwise.

We need to define a local controller, aka a steering method, to define the behavior of the robot when it tries to connect to configuration together. Here we will simply use linear interpolation. More complex controllers (like optimal trajectories) might be preferred if more knowledge about the system is available.

In the meantime, we will also need a method to check whether a local path is collision free. We will do that be simply regularly sampling the path with a given step length. Continuous collision detection might be implemented for better and safer performances.

Here we propose to implement the steering method and the path validation in a single connected method. More versatile implementation is obtained by defining two different functions.

Implement a `connect`

function, that takes as argument an initial `q1`

and a final `q2`

configuration, and return `True`

is it is possible to connect both using linear interpolation while avoiding collision. Optionally, the function should also returns the sampling of the path as a list of intermediate configurations – The solution does not need any new Pinocchio calls.

Finally, we need a k-nearest-neighbors algorithms.

Implement a function `nearest_neighbors`

that takes as argument a new configuration `q`

, a list of candidates `qs`

, and the number of requested neighbors `k`

, and returns the list of the `k`

nearest neighbors of `q`

in `qs`

. Optionally, the distance function that scores how close a configuration `q1`

is close to a configuration `q2`

might be also provided. If `qs`

contains less that `k`

elements, simply returns them all – no new Pinocchio method is needed for the solution.

Basically, probabilistic roadmaps are build by maintaining a graph of configurations. At each iteration, a new configuration is randomly sampled and connected to the nearest configurations already in the graph. The algorithm stops when both start configuration qstart and goal configuration qgoal can be directly connected to some elements of the graph.

We propose here to implement the visibility PRM algorithm. This algorithm also maintains the connected components of the graph. When a new configuration qnew is sampled, we try to connect it to its nearest neighbor in each of the already-sampled connected component. Configuration qnew is added to the graph only if one of the two following conditions is respected:

- if qnew cannot be connected to any existing connected component
- or if it can be connected to at least two connected component.

In the second case, the connected components that can be connected are also merged.

A graph structure with connected components is provided here.

Implement a `visibilityPRM`

that takes in argument two start and goal configurations `qstart`

and `qgoal`

, and the number of random sampling that must be achieved before failing. The returns `True`

if `qgoal`

can be connected to `qstart`

. The graph must also be returned – no fancy Pinocchio algorithm is needed here.

The PRM can be visualized in Gepetto-viewer using the function `display_prm`

provided here.

\(A^*\) is an algorithm to find the shortest path in a graph (discrete problem). \(A^*\) iterativelly explore the nodes of the graph starting for the given start. One a node gets explored, its exact cost from start (cost to here) is exactly known. Then, all its children are added to the "frontier" of the set of already-explored nodes. Cost from nodes of the frontier to the goal is not (yet) exactly known but is anticipated through a heuristic function (also provided). At next iteration, the algorithm examines a node of the frontier, looking first at the node that is the most likely to be in the shortest path using the heuristic distance.

See the fairly complete description of \(A^*\) provided here.

Implement the \(A^*\) algorithm. The \(A^*\) returns a sequence of node ID from start to goal. We only work here with the existing graph of configuration, meaning that no new nodes a sampled, and no new collision test are computed. Pinocchio is not necessary here. \(A^*\) returns a list containing the indexes of the nodes of the PRM graph that one should cross to reach qgoal from qstart.

Being given a list of configurations, a random shortcut simply randomly tries to shortcut some subpart of the list: it randomly selects the start and end of the subpart, and tries to directly connect them (using the above-defined steering method). In case of success, it skips the unnecessary subpart. The algorithm iterates a given number of time.

The shortcut can be run on either the list of configuration output by the \(A^*\), or on a sampling of the trajectory connecting the nodes of the \(A^*\). We propose here the second version.

Defines a function `sample_path`

to uniformly sample that trajectory connecting the nodes selected by \(A^*\): for each edge of the \(A^*\) optimal sequence, call `connect`

and add the resulting sampling to the previously-computed sample. It takes as argument the PRM graph and the list of indexes computed by \(A^*\) and returns a list of robot configuration starting by qstart and ending by qgoal – no Pinocchio method is needed here.

The sampled path can be displayed in Gepetto-viewer using the function `displayPath`

provided here.

Implement the `shortcut`

algorithm that tries to randomly connect two configuration of the sampled path. It takes the list of configuration output by `sample_path`

and the number of random shortcut it should tries. It returns an optimized list of configurations – no Pinocchio method is needed here.