From 573e7d0243a8b6ccfb93f8b8c8b463b23bfca362 Mon Sep 17 00:00:00 2001 From: Maxime Ellerbach Date: Fri, 31 Jul 2026 14:48:17 +0200 Subject: [PATCH] feat(rollout): add smooth_handover flag to episodic strategy config (#4159) * feat(rollout): add smooth_handover flag to episodic strategy config Follow-up to #3985, which added the same flag to the DAgger strategy. The episodic strategy's reset-phase handover had two gaps: - Non-actuated teleops could not skip the blocking follower slide at all. - Setting smooth_leader_to_follower_handover=false on an actuated teleop swapped which arm moves instead of skipping the handover. Add --strategy.smooth_handover (default true, existing behavior unchanged) as a master switch that skips the interpolation entirely, for clutch-style teleops that re-reference at the current robot pose on engage. Co-Authored-By: Claude Fable 5 * docs: fix table formatting via prettier --------- Co-authored-by: griffinaddison Co-authored-by: Claude Fable 5 --- docs/source/inference.mdx | 17 ++++++----- src/lerobot/rollout/configs.py | 9 ++++++ src/lerobot/rollout/strategies/episodic.py | 34 ++++++++++++---------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/docs/source/inference.mdx b/docs/source/inference.mdx index 31405b5de..aa1716015 100644 --- a/docs/source/inference.mdx +++ b/docs/source/inference.mdx @@ -186,14 +186,15 @@ Teleop is optional — if omitted the robot holds its position during the reset | `←` (left) | Discard episode and re-record it | | `ESC` | Stop the recording session | -| Flag | Description | -| ----------------------------------------------- | -------------------------------------------------------------------------- | -| `--dataset.num_episodes` | Number of episodes to record | -| `--dataset.episode_time_s` | Duration of each recording episode in seconds | -| `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds | -| `--teleop.type` | Optional. Teleoperator to drive the robot during resets | -| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes | -| `--strategy.smooth_leader_to_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. | +| Flag | Description | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--dataset.num_episodes` | Number of episodes to record | +| `--dataset.episode_time_s` | Duration of each recording episode in seconds | +| `--dataset.reset_time_s` | Duration of the reset phase between episodes in seconds | +| `--teleop.type` | Optional. Teleoperator to drive the robot during resets | +| `--strategy.reset_to_initial_position` | Whether to reset the robot to its initial position between episodes | +| `--strategy.smooth_leader_to_follower_handover` | Whether to turn on or off the leader -> follower smooth handover behavior. | +| `--strategy.smooth_handover` | Smoothly hand control to the teleop at reset start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage | --- diff --git a/src/lerobot/rollout/configs.py b/src/lerobot/rollout/configs.py index c0b5b345f..2890668eb 100644 --- a/src/lerobot/rollout/configs.py +++ b/src/lerobot/rollout/configs.py @@ -149,6 +149,15 @@ class EpisodicStrategyConfig(RolloutStrategyConfig): # Note that leader -> follower handover is only supported when the leader has `send_feedback` capability. smooth_leader_to_follower_handover: bool = True + # Whether to turn on or off the smooth handover behavior at the start of the + # reset phase: the leader is driven to the follower position (actuated + # teleops, see `smooth_leader_to_follower_handover`), or the follower is + # slid to the teleop pose (non-actuated teleops). Disable for clutch-style + # teleoperators (e.g. VR controllers) that re-reference at the current robot + # pose on engage: the handover is already continuous there, and the blocking + # interpolation only delays the start of the reset phase. + smooth_handover: bool = True + @RolloutStrategyConfig.register_subclass("dagger") @dataclass diff --git a/src/lerobot/rollout/strategies/episodic.py b/src/lerobot/rollout/strategies/episodic.py index 15b9bb971..e4eb9a885 100644 --- a/src/lerobot/rollout/strategies/episodic.py +++ b/src/lerobot/rollout/strategies/episodic.py @@ -143,21 +143,25 @@ class EpisodicStrategy(RolloutStrategy): # position so the operator takes over without fighting the arm. # For non-actuated teleops: slide the follower to the teleop's current # pose instead, since the leader cannot be driven. - obs = robot.get_observation() - current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")} - if ( - teleop_supports_feedback(teleop) - and self.config.smooth_leader_to_follower_handover - ): - logger.info("Smooth handover: moving leader arm to follower position") - teleop_smooth_move_to(teleop, current_pos, duration_s=2) - teleop.disable_torque() - else: - logger.info("Smooth handover: sliding follower to teleop position") - teleop_action = teleop.get_action() - processed = ctx.processors.teleop_action_processor((teleop_action, obs)) - target = ctx.processors.robot_action_processor((processed, obs)) - follower_smooth_move_to(robot, current_pos, target, duration_s=1) + # Disabled entirely with --strategy.smooth_handover=false (useful for + # clutch-style teleops that re-reference at the current robot pose on + # engage). + if self.config.smooth_handover: + obs = robot.get_observation() + current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")} + if ( + teleop_supports_feedback(teleop) + and self.config.smooth_leader_to_follower_handover + ): + logger.info("Smooth handover: moving leader arm to follower position") + teleop_smooth_move_to(teleop, current_pos, duration_s=2) + teleop.disable_torque() + else: + logger.info("Smooth handover: sliding follower to teleop position") + teleop_action = teleop.get_action() + processed = ctx.processors.teleop_action_processor((teleop_action, obs)) + target = ctx.processors.robot_action_processor((processed, obs)) + follower_smooth_move_to(robot, current_pos, target, duration_s=1) elif self.config.reset_to_initial_position: # No teleop: return the robot to its startup position.