Skip to content

Commit 486ca4d

Browse files
committed
Merge pull request #95 from personalrobotics/feature/SmoothingRefactor2
Trajectory timing/smoothing refactor 2.0.
2 parents ecc77f9 + 4942a96 commit 486ca4d

File tree

1 file changed

+187
-29
lines changed

1 file changed

+187
-29
lines changed

src/prpy/base/robot.py

Lines changed: 187 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -181,51 +181,204 @@ def GetTrajectoryManipulators(self, traj):
181181

182182
return active_manipulators
183183

184-
def ExecutePath(self, path, simplify=True, smooth=True, defer=False,
185-
timeout=1., **kwargs):
184+
def PostProcessPath(self, path, defer=False, executor=None,
185+
constrained=None, smooth=None, default_timelimit=0.5,
186+
shortcut_options=None, smoothing_options=None,
187+
retiming_options=None):
188+
""" Post-process a geometric path to prepare it for execution.
189+
190+
This method post-processes a geometric path by (optionally) optimizing
191+
it and timing it. Three different post-processing pipelines are used:
192+
193+
1. For constrained trajectories, we do not modify the geometric path
194+
and retime the path to be time-optimal. This trajectory must stop
195+
at every waypoint. The only exception is for...
196+
2. For smooth trajectories, we attempt to fit a time-optimal smooth
197+
curve through the waypoints (not implemented). If this curve is
198+
not collision free, then we fall back on...
199+
3. By default, we run a smoother that jointly times and smooths the
200+
path. This algorithm can change the geometric path to optimize
201+
runtime.
202+
203+
The behavior in (1) and (2) can be forced by passing constrained=True
204+
or smooth=True. By default, the case is inferred by the tag(s) attached
205+
to the trajectory: (1) is triggered by the CONSTRAINED tag and (2) is
206+
tiggered by the SMOOTH tag.
207+
208+
Options an be passed to each post-processing routine using the
209+
shortcut-options, smoothing_options, and retiming_options **kwargs
210+
dictionaries. If no "timelimit" is specified in any of these
211+
dictionaries, it defaults to default_timelimit seconds.
212+
213+
@param path un-timed OpenRAVE trajectory
214+
@param defer return immediately with a future trajectory
215+
@param executor executor to use when defer = True
216+
@param constrained the path is constrained; do not change it
217+
@param smooth the path is smooth; attempt to execute it directly
218+
@param default_timelimit timelimit for all operations, if not set
219+
@param shortcut_options kwargs to ShortcutPath for shortcutting
220+
@param smoothing_options kwargs to RetimeTrajectory for smoothing
221+
@param retiming_options kwargs to RetimeTrajectory for timing
222+
@return trajectory ready for execution
223+
"""
224+
from ..planning.base import Tags
225+
from ..util import GetTrajectoryTags, CopyTrajectory
226+
227+
# Default parameters.
228+
if shortcut_options is None:
229+
shortcut_options = dict()
230+
if smoothing_options is None:
231+
smoothing_options = dict()
232+
if retiming_options is None:
233+
retiming_options = dict()
234+
235+
shortcut_options.setdefault('timelimit', default_timelimit)
236+
smoothing_options.setdefault('timelimit', default_timelimit)
237+
retiming_options.setdefault('timelimit', default_timelimit)
238+
239+
# Read default parameters from the trajectory's tags.
240+
tags = GetTrajectoryTags(path)
241+
242+
if constrained is None:
243+
constrained = tags.get(Tags.CONSTRAINED, False)
244+
logger.debug('Detected "%s" tag on trajectory: Setting'
245+
' constrained = True.', Tags.CONSTRAINED)
246+
247+
if smooth is None:
248+
smooth = tags.get(Tags.SMOOTH, False)
249+
logger.debug('Detected "%s" tag on trajectory: Setting smooth'
250+
' = True', Tags.SMOOTH)
251+
252+
def do_postprocess():
253+
with Clone(self.GetEnv()) as cloned_env:
254+
cloned_robot = cloned_env.Cloned(self)
186255

187-
logger.debug('Begin ExecutePath')
256+
# Planners only operate on the active DOFs. We'll set any DOFs
257+
# in the trajectory as active.
258+
env = path.GetEnv()
259+
cspec = path.GetConfigurationSpecification()
260+
used_bodies = cspec.ExtractUsedBodies(env)
261+
if self not in used_bodies:
262+
raise ValueError(
263+
'Robot "{:s}" is not in the trajectory.'.format(
264+
self.GetName()))
265+
266+
dof_indices, _ = cspec.ExtractUsedIndices(self)
267+
cloned_robot.SetActiveDOFs(dof_indices)
268+
logger.debug(
269+
'Setting robot "%s" DOFs %s as active for post-processing.',
270+
cloned_robot.GetName(), list(dof_indices))
271+
272+
# TODO: Handle a affine DOF trajectories for the base.
273+
274+
# Directly compute a timing of smooth trajectories.
275+
if smooth:
276+
logger.warning(
277+
'Post-processing smooth paths is not supported.'
278+
' Using the default post-processing logic; this may'
279+
' significantly change the geometric path.'
280+
)
188281

189-
def do_execute(path, simplify, smooth, timeout, **kwargs):
282+
# The trajectory is constrained. Retime it without changing the
283+
# geometric path.
284+
if constrained:
285+
logger.debug('Retiming a constrained path. The output'
286+
' trajectory will stop at every waypoint.')
287+
traj = self.retimer.RetimeTrajectory(
288+
cloned_robot, path, defer=False, **retiming_options)
289+
else:
290+
# The trajectory is not constrained, so we can shortcut it
291+
# before execution.
292+
logger.debug('Shortcutting an unconstrained path.')
293+
shortcut_path = self.simplifier.ShortcutPath(
294+
cloned_robot, path, defer=False, **shortcut_options)
295+
296+
logger.debug('Smoothing an unconstrained path.')
297+
traj = self.smoother.RetimeTrajectory(
298+
cloned_robot, shortcut_path, defer=False,
299+
**smoothing_options)
300+
301+
return CopyTrajectory(traj, env=self.GetEnv())
190302

191-
with Clone(self.GetEnv()) as cloned_env:
192-
traj_dofs = util.GetTrajectoryIndices(path)
193-
cloned_env.Cloned(self).SetActiveDOFs(traj_dofs)
194-
cloned_robot = cloned_env.Cloned(self)
303+
if defer:
304+
from trollius.executor import get_default_executor
305+
from trollius.futures import wrap_future
195306

196-
if simplify:
197-
path = self.simplifier.ShortcutPath(cloned_robot, path, defer=False,
198-
timeout=timeout, **kwargs)
307+
if executor is None:
308+
executor = get_default_executor()
199309

200-
retimer = self.smoother if smooth else self.retimer
201-
cloned_timed_traj = retimer.RetimeTrajectory(cloned_robot, path, defer=False, **kwargs)
310+
return wrap_future(executor.submit(do_postprocess))
311+
else:
312+
return do_postprocess()
313+
314+
def ExecutePath(self, path, defer=False, executor=None, **kwargs):
315+
""" Post-process and execute an un-timed path.
316+
317+
This method calls PostProcessPath, then passes the result to
318+
ExecuteTrajectory. Any extra **kwargs are forwarded to both of these
319+
methods. This function returns the timed trajectory that was executed
320+
on the robot.
321+
322+
@param path OpenRAVE trajectory representing an un-timed path
323+
@param defer execute asynchronously and return a future
324+
@param executor if defer = True, which executor to use
325+
@param **kwargs forwarded to PostProcessPath and ExecuteTrajectory
326+
@return timed trajectory executed on the robot
327+
"""
202328

203-
# Copy the trajectory back to the original environment.
204-
from ..util import CopyTrajectory
205-
timed_traj = CopyTrajectory(cloned_timed_traj, env=self.GetEnv())
329+
def do_execute():
330+
logger.debug('Post-processing path to compute a timed trajectory.')
331+
traj = self.PostProcessPath(path, defer=False, **kwargs)
206332

207-
return self.ExecuteTrajectory(timed_traj, defer=False, **kwargs)
333+
logger.debug('Executing timed trajectory.')
334+
return self.ExecuteTrajectory(traj, defer=False, **kwargs)
208335

209336
if defer:
210337
from trollius.executor import get_default_executor
211338
from trollius.futures import wrap_future
212339

213-
executor = kwargs.get('executor') or get_default_executor()
214-
return wrap_future(
215-
executor.submit(do_execute,
216-
path, simplify=simplify, smooth=smooth, timeout=timeout,
217-
**kwargs
218-
)
219-
)
340+
if executor is None:
341+
executor = get_default_executor()
342+
343+
return wrap_future(executor.submit(do_execute))
220344
else:
221-
return do_execute(path, simplify=simplify, smooth=smooth,
222-
timeout=timeout, **kwargs)
345+
return do_execute()
346+
347+
def ExecuteTrajectory(self, traj, defer=False, timeout=None, period=0.01):
348+
""" Executes a time trajectory on the robot.
349+
350+
This function directly executes a timed OpenRAVE trajectory on the
351+
robot. If you have a geometric path, such as those returned by a
352+
geometric motion planner, you should first time the path using
353+
PostProcessPath. Alternatively, you could use the ExecutePath helper
354+
function to time and execute the path in one function call.
355+
356+
If timeout = None (the default), this function does not return until
357+
execution has finished. Termination occurs if the trajectory is
358+
successfully executed or if a fault occurs (in this case, an exception
359+
will be raised). If timeout is a float (including timeout = 0), this
360+
function will return None once the timeout has ellapsed, even if the
361+
trajectory is still being executed.
362+
363+
NOTE: We suggest that you either use timeout=None or defer=True. If
364+
trajectory execution times out, there is no way to tell whether
365+
execution was successful or not. Other values of timeout are only
366+
supported for legacy reasons.
367+
368+
This function returns the trajectory that was actually executed on the
369+
robot, including controller error. If this is not available, the input
370+
trajectory will be returned instead.
371+
372+
@param traj timed OpenRAVE trajectory to be executed
373+
@param defer execute asynchronously and return a trajectory Future
374+
@param timeout maximum time to wait for execution to finish
375+
@param period poll rate, in seconds, for checking trajectory status
376+
@return trajectory executed on the robot
377+
"""
223378

224-
def ExecuteTrajectory(self, traj, defer=False, timeout=None, period=0.01, **kw_args):
225379
# TODO: Verify that the trajectory is timed.
226380
# TODO: Check if this trajectory contains the base.
227381

228-
logger.debug('Begin ExecuteTrajectory')
229382
needs_base = util.HasAffineDOFs(traj.GetConfigurationSpecification())
230383

231384
self.GetController().SetPath(traj)
@@ -238,8 +391,13 @@ def ExecuteTrajectory(self, traj, defer=False, timeout=None, period=0.01, **kw_a
238391
]
239392

240393
if needs_base:
241-
if hasattr(self, 'base') and hasattr(self.base, 'controller'):
394+
if (hasattr(self, 'base') and hasattr(self.base, 'controller')
395+
and self.base.controller is not None):
242396
active_controllers.append(self.base.controller)
397+
else:
398+
logger.warning(
399+
'Trajectory includes the base, but no base controller is'
400+
' available. Is self.base.controller set?')
243401

244402
if defer:
245403
import time

0 commit comments

Comments
 (0)