Skip to content

Commit eadd1b9

Browse files
committed
Merge pull request #280 from personalrobotics/feature/more_planningmethods
Added separate LockedPlanningMethod and ClonedPlanningMethod calls.
2 parents 8cbfa84 + 0c08ea9 commit eadd1b9

File tree

17 files changed

+171
-133
lines changed

17 files changed

+171
-133
lines changed

README.md

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,27 @@ two subclasses:
2525
2. `prpy.planning.base.MetaPlanner`: combines the output of multiple motion
2626
planners, each of which is a `BasePlanner` or another `MetaPlanner`
2727

28-
Each planner has one or more *planning methods*, annotated with the
29-
`@PlanningMethod` decorator, that look like ordinary functions. However, unlike
30-
an ordinary function, calling a planning method clones the robot's environment
31-
into a *planning environment* associated with the planner. Planning occurs in
32-
the cloned environment to allow PrPy to run multiple planners in parallel and
33-
to paralellize planning and execution.
28+
Each planner has one or more *planning methods*, annotated with either the
29+
`@LockedPlanningMethod` or `@ClonedPlanningMethod decorator, that look like
30+
ordinary functions. Using these decorators makes other PrPy components
31+
aware that these methods exist and follow a particular specification that
32+
allows them to be composed with other PrPy objects automatically. For
33+
example, `MetaPlanner`s will report that they can perform planning methods
34+
that their child motion planners have enumerated via `@PlanningMethod`
35+
decorators.
36+
37+
`@PlanningMethod` decorators also make sure that calls to planning code are
38+
executed in a thread-safe manner. In the case of `@LockedPlanningMethod`,
39+
this is enforced by locking the calling environment until the planning method
40+
has completed. In the case of `@ClonedPlanningMethod`, this is enforced by
41+
cloning the calling environment, and calling the wrapped method with references
42+
to the cloned environment. The result of the method is then copied back to the
43+
calling environment. `@ClonedPlanningMethod`s can be used to run multiple
44+
planners in parallel and to parallelize planning and execution.
45+
46+
In general, **locked** planning methods are used for calls that will terminate
47+
extremely quickly, while **cloned** planning methods are used for calls that
48+
might take a significant amount of time.
3449

3550
For example, the following code will use OMPL to plan `robot`'s active DOFs
3651
from their current values to to the `goal_config` configuration:
@@ -40,10 +55,10 @@ planner = OMPLPlanner('RRTConnect')
4055
output_path = planner.PlanToConfiguration(robot, goal_config)
4156
```
4257

43-
First, `robot.GetEnv()` is cloned into the the `planner.env` planning
44-
environment. Next, planning occurs in the cloned environment. Finally, the
45-
output path is cloned back into `robot.GetEnv()` and is returned by the
46-
planner.
58+
As this is a `@ClonedPlanningMethod`, `robot.GetEnv()` is cloned into the
59+
the `planner.env` planning environment. Planning occurs within this cloned
60+
environment. Finally, the output path is cloned back into `robot.GetEnv()`
61+
and is returned by the planner.
4762

4863
See the following sub-sections for more information about the built-in planners
4964
provided with PrPy, information about writing your own planner, and several
@@ -86,7 +101,7 @@ See the Python docstrings the above classes for more information.
86101

87102
### Common Planning Methods
88103

89-
There is no formal list of `@PlanningMethod`s or their arguments. However, we
104+
There is no formal list of `@*PlanningMethod`s or their arguments. However, we
90105
have found these methods to be useful:
91106

92107
- `PlanToConfiguration(robot, goal_config)`: plan the robot's active DOFs from
@@ -112,25 +127,36 @@ of these methods accept planner-specific keyword arguments.
112127
### Writing a Custom Planner
113128

114129
Implementing a custom planner requires extending the `BasePlanner` class and
115-
decorating one or more methods with the `@PlanningMethod` decorator. Extending
116-
the `BasePlanner` class constructs the planning environment `self.env` and
117-
allows PrPy to identify your planner as a base planner class, as opposed to a
118-
meta-planner. The `@PlanningMethod` decorator handles environment cloning and
119-
allows meta-planners to query the list of planning methods that the planner
120-
supports (e.g. to generate docstrings).
130+
decorating one or more methods with the `@LockedPlanningMethod` or
131+
`@ClonedPlanningMethod` decorator.
132+
133+
Extending the `BasePlanner` class allows PrPy to identify your planner as a
134+
base planner class, as opposed to a meta-planner. The `@PlanningMethod`
135+
decorators handle environment cloning or locking and allows meta-planners to
136+
query the list of planning methods that the planner supports (e.g. to generate
137+
docstrings).
138+
139+
Each instance of a `BasePlanner`-derived class constructs a planning
140+
environment `self.env`. This environment is uniquely associated with each
141+
instance of the planner and is what will be used in `@ClonedPlanningMethod`
142+
calls. Since this environment is persistent and unique, it can also be used
143+
as a place to cache data or pre-load plugins for planners that have heavyweight
144+
initialization steps. However, because of this, each planning instance can
145+
only execute one `@ClonedPlanningMethod` at a time. It can still execute
146+
arbitrary `@LockedPlanningMethod` calls, as long as they are referring to
147+
robots in different environments.
121148

122149
Please obey the following guidelines:
123150

124-
- Assume that the cloned environment is locked during the entire call.
125-
- Subclass constructor **must** call `BasePlanner.__init__`.
126-
- A `@PlanningMethod` **must not** call another `@PlanningMethod`.
151+
- Assume that the planning environment is locked during the entire call.
152+
- Subclass constructors **must** call `BasePlanner.__init__`.
127153
- Each `@PlanningMethod` **must** accept the first argument `robot`, which is a
128-
robot in the cloned environment.
154+
robot in the environment it should be using to perform planning.
129155
- Each `@PlanningMethod` **must** accept `**kwargs` to ignore arguments that
130156
are not supported by the planner.
131157
- Each `@PlanningMethod` **must** return a `Trajectory` which was created in
132-
the cloned environment.
133-
- When possible, use one of the defacto-standard `@PlanningMethod` names listed
158+
the same environment as the robot it was passed (e.g. `robot.GetEnv()`).
159+
- When possible, use one of the defacto-standard `@*PlanningMethod` names listed
134160
below.
135161
- Raise a `PlanningError` to indicate an expected, but fatal, error (e.g.
136162
timeout with no collision-free path).

src/prpy/planning/base.py

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,41 @@ def __init__(self, message, errors):
9191
# TODO: Print the inner exceptions.
9292

9393

94-
class PlanningMethod(object):
94+
class LockedPlanningMethod(object):
95+
"""
96+
Decorate a planning method that locks the calling environment.
97+
"""
9598
def __init__(self, func):
9699
self.func = func
97100

101+
def __call__(self, instance, robot, *args, **kw_args):
102+
with robot.GetEnv():
103+
# Perform the actual planning operation.
104+
traj = self.func(instance, robot, *args, **kw_args)
105+
106+
# Tag the trajectory with the planner and planning method
107+
# used to generate it. We don't overwrite these tags if
108+
# they already exist.
109+
tags = GetTrajectoryTags(traj)
110+
tags.setdefault(Tags.PLANNER, instance.__class__.__name__)
111+
tags.setdefault(Tags.METHOD, self.func.__name__)
112+
SetTrajectoryTags(traj, tags, append=False)
113+
114+
return traj
115+
116+
def __get__(self, instance, instancetype):
117+
# Bind the self reference and use update_wrapper to propagate the
118+
# function's metadata (e.g. name and docstring).
119+
wrapper = functools.partial(self.__call__, instance)
120+
functools.update_wrapper(wrapper, self.func)
121+
wrapper.is_planning_method = True
122+
return wrapper
123+
124+
125+
class ClonedPlanningMethod(LockedPlanningMethod):
126+
"""
127+
Decorate a planning method that clones the calling environment.
128+
"""
98129
def __call__(self, instance, robot, *args, **kw_args):
99130
env = robot.GetEnv()
100131

@@ -110,10 +141,11 @@ def __call__(self, instance, robot, *args, **kw_args):
110141
joint_values[1] = cloned_robot.GetActiveDOFValues()
111142

112143
# Check for mismatches in the cloning and hackily reset them.
113-
# (This is due to a possible bug in OpenRAVE environment cloning where in
114-
# certain situations, the Active DOF ordering and values do not match the
115-
# parent environment. It seems to be exacerbated by multirotation joints,
116-
# but the exact cause and repeatability is unclear at this point.)
144+
# (This is due to a possible bug in OpenRAVE environment cloning
145+
# where in certain situations, the Active DOF ordering and values
146+
# do not match the parent environment. It seems to be exacerbated
147+
# by multirotation joints, but the exact cause and repeatability
148+
# is unclear at this point.)
117149
if not numpy.array_equal(joint_indices[0], joint_indices[1]):
118150
logger.warning("Cloned Active DOF index mismatch: %s != %s",
119151
str(joint_indices[0]),
@@ -126,27 +158,16 @@ def __call__(self, instance, robot, *args, **kw_args):
126158
str(joint_values[1]))
127159
cloned_robot.SetActiveDOFValues(joint_values[0])
128160

129-
# Perform the actual planning operation.
130-
planner_traj = self.func(instance, cloned_robot,
131-
*args, **kw_args)
161+
traj = super(ClonedPlanningMethod, self).__call__(
162+
instance, cloned_robot, *args, **kw_args)
163+
return CopyTrajectory(traj, env=env)
132164

133-
# Tag the trajectory with the planner and planning method
134-
# used to generate it. We don't overwrite these tags if
135-
# they already exist.
136-
tags = GetTrajectoryTags(planner_traj)
137-
tags.setdefault(Tags.PLANNER, instance.__class__.__name__)
138-
tags.setdefault(Tags.METHOD, self.func.__name__)
139-
SetTrajectoryTags(planner_traj, tags, append=False)
140165

141-
return CopyTrajectory(planner_traj, env=env)
142-
143-
def __get__(self, instance, instancetype):
144-
# Bind the self reference and use update_wrapper to propagate the
145-
# function's metadata (e.g. name and docstring).
146-
wrapper = functools.partial(self.__call__, instance)
147-
functools.update_wrapper(wrapper, self.func)
148-
wrapper.is_planning_method = True
149-
return wrapper
166+
class PlanningMethod(ClonedPlanningMethod):
167+
def __init__(self, func):
168+
logger.warn("Please explicitly declare a ClonedPlanningMethod "
169+
"instead of using PlanningMethod.")
170+
super(ClonedPlanningMethod, self).__init__(func)
150171

151172

152173
class Planner(object):

src/prpy/planning/cbirrt.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#
77
# Redistribution and use in source and binary forms, with or without
88
# modification, are permitted provided that the following conditions are met:
9-
#
9+
#
1010
# - Redistributions of source code must retain the above copyright notice, this
1111
# list of conditions and the following disclaimer.
1212
# - Redistributions in binary form must reproduce the above copyright notice,
@@ -32,7 +32,7 @@
3232
import openravepy
3333
from ..util import SetTrajectoryTags
3434
from base import (BasePlanner, PlanningError, UnsupportedPlanningError,
35-
PlanningMethod, Tags)
35+
ClonedPlanningMethod, Tags)
3636
import prpy.kin
3737
import prpy.tsr
3838

@@ -48,7 +48,7 @@ def __init__(self):
4848
def __str__(self):
4949
return 'CBiRRT'
5050

51-
@PlanningMethod
51+
@ClonedPlanningMethod
5252
def PlanToConfigurations(self, robot, goals, **kw_args):
5353
"""
5454
Plan to multiple goal configurations with CBiRRT. This adds each goal
@@ -61,7 +61,7 @@ def PlanToConfigurations(self, robot, goals, **kw_args):
6161
kw_args.setdefault('smoothingitrs', 0)
6262
return self.Plan(robot, jointgoals=goals, **kw_args)
6363

64-
@PlanningMethod
64+
@ClonedPlanningMethod
6565
def PlanToConfiguration(self, robot, goal, **kw_args):
6666
"""
6767
Plan to a single goal configuration with CBiRRT.
@@ -72,7 +72,7 @@ def PlanToConfiguration(self, robot, goal, **kw_args):
7272
kw_args.setdefault('smoothingitrs', 0)
7373
return self.Plan(robot, jointgoals=[goal], **kw_args)
7474

75-
@PlanningMethod
75+
@ClonedPlanningMethod
7676
def PlanToEndEffectorPose(self, robot, goal_pose, **kw_args):
7777
"""
7878
Plan to a desired end-effector pose.
@@ -90,7 +90,7 @@ def PlanToEndEffectorPose(self, robot, goal_pose, **kw_args):
9090

9191
return self.Plan(robot, tsr_chains=[tsr_chain], **kw_args)
9292

93-
@PlanningMethod
93+
@ClonedPlanningMethod
9494
def PlanToEndEffectorOffset(self, robot, direction, distance,
9595
smoothingitrs=100, **kw_args):
9696
"""
@@ -153,7 +153,7 @@ def PlanToEndEffectorOffset(self, robot, direction, distance,
153153
**kw_args
154154
)
155155

156-
@PlanningMethod
156+
@ClonedPlanningMethod
157157
def PlanToTSR(self, robot, tsr_chains, smoothingitrs=100, **kw_args):
158158
"""
159159
Plan to a goal specified as a list of TSR chains. CBiRRT supports an

src/prpy/planning/chomp.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,9 @@
3232
import logging
3333
import numpy
3434
import openravepy
35-
from .. import tsr
3635
from ..util import SetTrajectoryTags
3736
from base import (BasePlanner, PlanningError, UnsupportedPlanningError,
38-
PlanningMethod, Tags)
37+
ClonedPlanningMethod, Tags)
3938
import prpy.tsr
4039

4140
logger = logging.getLogger(__name__)
@@ -192,7 +191,7 @@ def ComputeDistanceField(self, robot):
192191
logger.warning('ComputeDistanceField is deprecated. Distance fields are'
193192
' now implicity created by DistanceFieldManager.')
194193

195-
@PlanningMethod
194+
@ClonedPlanningMethod
196195
def OptimizeTrajectory(self, robot, traj, lambda_=100.0, n_iter=50,
197196
**kw_args):
198197
self.distance_fields.sync(robot)
@@ -215,7 +214,7 @@ def OptimizeTrajectory(self, robot, traj, lambda_=100.0, n_iter=50,
215214
SetTrajectoryTags(traj, {Tags.SMOOTH: True}, append=True)
216215
return traj
217216

218-
@PlanningMethod
217+
@ClonedPlanningMethod
219218
def PlanToConfiguration(self, robot, goal, lambda_=100.0, n_iter=15,
220219
**kw_args):
221220
"""
@@ -237,7 +236,7 @@ def PlanToConfiguration(self, robot, goal, lambda_=100.0, n_iter=15,
237236
SetTrajectoryTags(traj, {Tags.SMOOTH: True}, append=True)
238237
return traj
239238

240-
@PlanningMethod
239+
@ClonedPlanningMethod
241240
def PlanToEndEffectorPose(self, robot, goal_pose, lambda_=100.0,
242241
n_iter=100, goal_tolerance=0.01, **kw_args):
243242
"""
@@ -293,7 +292,7 @@ def PlanToEndEffectorPose(self, robot, goal_pose, lambda_=100.0,
293292

294293
# JK - Disabling. This is not working reliably.
295294
'''
296-
@PlanningMethod
295+
@ClonedPlanningMethod
297296
def PlanToTSR(self, robot, tsrchains, lambda_=100.0, n_iter=100,
298297
goal_tolerance=0.01, **kw_args):
299298
"""

src/prpy/planning/ik.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
from .. import ik_ranking
3434
from base import (BasePlanner,
3535
PlanningError,
36-
PlanningMethod)
36+
ClonedPlanningMethod)
3737

3838
logger = logging.getLogger(__name__)
3939

@@ -46,7 +46,7 @@ def __init__(self, delegate_planner=None):
4646
def __str__(self):
4747
return 'IKPlanner'
4848

49-
@PlanningMethod
49+
@ClonedPlanningMethod
5050
def PlanToIK(self, robot, goal_pose, ranker=ik_ranking.JointLimitAvoidance,
5151
num_attempts=1, **kw_args):
5252
from openravepy import (IkFilterOptions,

src/prpy/planning/mac_smoother.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030

3131
from ..util import (CopyTrajectory, SimplifyTrajectory, SetTrajectoryTags,
3232
IsTimedTrajectory)
33-
from base import (BasePlanner, PlanningError, PlanningMethod,
33+
from base import (BasePlanner, PlanningError, ClonedPlanningMethod,
3434
UnsupportedPlanningError, Tags)
3535
from openravepy import (Planner, PlannerStatus, RaveCreatePlanner,
3636
openrave_exception)
@@ -54,7 +54,7 @@ def __init__(self):
5454
' installed and in your OPENRAVE_PLUGIN path?'
5555
)
5656

57-
@PlanningMethod
57+
@ClonedPlanningMethod
5858
def RetimeTrajectory(self, robot, path):
5959
# Copy the input trajectory into the planning environment. This is
6060
# necessary for two reasons: (1) the input trajectory may be in another
@@ -72,8 +72,8 @@ def RetimeTrajectory(self, robot, path):
7272
params = Planner.PlannerParameters()
7373
self.blender.InitPlan(robot, params)
7474
status = self.blender.PlanPath(output_traj)
75-
if status not in [ PlannerStatus.HasSolution,
76-
PlannerStatus.InterruptedWithSolution ]:
75+
if status not in [PlannerStatus.HasSolution,
76+
PlannerStatus.InterruptedWithSolution]:
7777
raise PlanningError('Blending trajectory failed.')
7878
except openrave_exception as e:
7979
raise PlanningError('Blending trajectory failed: ' + str(e))
@@ -84,12 +84,11 @@ def RetimeTrajectory(self, robot, path):
8484
params = Planner.PlannerParameters()
8585
self.retimer.InitPlan(robot, params)
8686
status = self.retimer.PlanPath(output_traj)
87-
if status not in [ PlannerStatus.HasSolution,
88-
PlannerStatus.InterruptedWithSolution ]:
87+
if status not in [PlannerStatus.HasSolution,
88+
PlannerStatus.InterruptedWithSolution]:
8989
raise PlanningError('Timing trajectory failed.')
9090
except openrave_exception as e:
9191
raise PlanningError('Timing trajectory failed: ' + str(e))
9292

9393
SetTrajectoryTags(output_traj, {Tags.SMOOTH: True}, append=True)
9494
return output_traj
95-

src/prpy/planning/mk.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@
3030

3131
import logging, numpy, openravepy, time
3232
from ..util import SetTrajectoryTags
33-
from base import (BasePlanner, PlanningError, UnsupportedPlanningError,
34-
PlanningMethod, Tags)
33+
from base import (BasePlanner, PlanningError,
34+
ClonedPlanningMethod, Tags)
3535

3636
logger = logging.getLogger(__name__)
3737

@@ -100,7 +100,7 @@ def GetStraightVelocity(self, manip, velocity, initial_hand_pose, nullspace_fn,
100100

101101
return numpy.dot(jacobian_pinv, pose_error) + numpy.dot(nullspace_projector, nullspace_goal)
102102

103-
@PlanningMethod
103+
@ClonedPlanningMethod
104104
def PlanToEndEffectorOffset(self, robot, direction, distance, max_distance=None,
105105
nullspace=JointLimitAvoidance, timelimit=5.0, step_size=0.001,
106106
position_tolerance=0.01, angular_tolerance=0.15, **kw_args):

0 commit comments

Comments
 (0)