From 360e619e184b5412210909e16daa6f1fd228aac0 Mon Sep 17 00:00:00 2001 From: griffinaddison Date: Thu, 23 Jul 2026 09:36:11 -0700 Subject: [PATCH] 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/source/inference.mdx | 1 + src/lerobot/rollout/configs.py | 9 ++++++ src/lerobot/rollout/strategies/episodic.py | 34 ++++++++++++---------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/docs/source/inference.mdx b/docs/source/inference.mdx index 31405b5de..847260dd6 100644 --- a/docs/source/inference.mdx +++ b/docs/source/inference.mdx @@ -194,6 +194,7 @@ Teleop is optional — if omitted the robot holds its position during the reset | `--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 639e2ba29..aef329a05 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.