Compare commits

..

2 Commits

Author SHA1 Message Date
Maxime Ellerbach 410b8f946d docs: fix table formatting via prettier 2026-07-27 09:40:16 +00:00
griffinaddison 360e619e18 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 <noreply@anthropic.com>
2026-07-27 09:38:16 +00:00
4 changed files with 47 additions and 50 deletions
+3 -3
View File
@@ -150,12 +150,11 @@ lerobot-rollout \
Foot pedal input is also supported via `--strategy.input_device=pedal`. Configure pedal codes with `--strategy.pedal.*` flags. Foot pedal input is also supported via `--strategy.input_device=pedal`. Configure pedal codes with `--strategy.pedal.*` flags.
| Flag | Description | | Flag | Description |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------ | ------------------------------------------------------- |
| `--strategy.num_episodes` | Number of correction episodes to record (default: 10) | | `--strategy.num_episodes` | Number of correction episodes to record (default: 10) |
| `--strategy.record_autonomous` | Record autonomous frames too (default: false) | | `--strategy.record_autonomous` | Record autonomous frames too (default: false) |
| `--strategy.upload_every_n_episodes` | Push to Hub every N episodes (default: 5) | | `--strategy.upload_every_n_episodes` | Push to Hub every N episodes (default: 5) |
| `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) | | `--strategy.input_device` | Input device: `keyboard` or `pedal` (default: keyboard) |
| `--strategy.smooth_handover` | Smoothly hand control over at pause / correction start (default: true). Disable for clutch-style teleops that re-reference at the current robot pose on engage |
| `--teleop.type` | **Required.** Teleoperator type | | `--teleop.type` | **Required.** Teleoperator type |
### Episodic (`--strategy.type=episodic`) ### Episodic (`--strategy.type=episodic`)
@@ -188,13 +187,14 @@ Teleop is optional — if omitted the robot holds its position during the reset
| `ESC` | Stop the recording session | | `ESC` | Stop the recording session |
| Flag | Description | | Flag | Description |
| ----------------------------------------------- | -------------------------------------------------------------------------- | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dataset.num_episodes` | Number of episodes to record | | `--dataset.num_episodes` | Number of episodes to record |
| `--dataset.episode_time_s` | Duration of each recording episode in seconds | | `--dataset.episode_time_s` | Duration of each recording episode in seconds |
| `--dataset.reset_time_s` | Duration of the reset phase between episodes 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 | | `--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.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_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 |
--- ---
+9 -8
View File
@@ -149,6 +149,15 @@ class EpisodicStrategyConfig(RolloutStrategyConfig):
# Note that leader -> follower handover is only supported when the leader has `send_feedback` capability. # Note that leader -> follower handover is only supported when the leader has `send_feedback` capability.
smooth_leader_to_follower_handover: bool = True 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") @RolloutStrategyConfig.register_subclass("dagger")
@dataclass @dataclass
@@ -180,14 +189,6 @@ class DAggerStrategyConfig(RolloutStrategyConfig):
# Target video file size in MB for episode rotation (record_autonomous # Target video file size in MB for episode rotation (record_autonomous
# mode only). Defaults to DEFAULT_VIDEO_FILE_SIZE_IN_MB when None. # mode only). Defaults to DEFAULT_VIDEO_FILE_SIZE_IN_MB when None.
target_video_file_size_mb: int | None = None target_video_file_size_mb: int | None = None
# Whether to turn on or off the smooth handover behavior at phase transitions:
# the leader is driven to the follower position on pause (teleops with
# `send_feedback` capability), and the follower is slid to the teleop pose when
# a correction starts (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 correction.
smooth_handover: bool = True
input_device: str = "keyboard" input_device: str = "keyboard"
keyboard: DAggerKeyboardConfig = field(default_factory=DAggerKeyboardConfig) keyboard: DAggerKeyboardConfig = field(default_factory=DAggerKeyboardConfig)
pedal: DAggerPedalConfig = field(default_factory=DAggerPedalConfig) pedal: DAggerPedalConfig = field(default_factory=DAggerPedalConfig)
+3 -11
View File
@@ -623,8 +623,8 @@ class DAggerStrategy(RolloutStrategy):
# State-machine transition side-effects # State-machine transition side-effects
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@staticmethod
def _apply_transition( def _apply_transition(
self,
old_phase: DAggerPhase, old_phase: DAggerPhase,
new_phase: DAggerPhase, new_phase: DAggerPhase,
engine, engine,
@@ -634,10 +634,6 @@ class DAggerStrategy(RolloutStrategy):
) -> None: ) -> None:
"""Execute side-effects for a validated phase transition, including smooth handovers. """Execute side-effects for a validated phase transition, including smooth handovers.
The smooth handovers below can be disabled with
``--strategy.smooth_handover=false`` (useful for clutch-style teleops
that re-reference at the current robot pose on engage).
AUTONOMOUS -> PAUSED (actuated teleop): AUTONOMOUS -> PAUSED (actuated teleop):
Pause the engine, then drive the leader arm to the follower's last Pause the engine, then drive the leader arm to the follower's last
commanded position so the operator takes over without a jerk. commanded position so the operator takes over without a jerk.
@@ -661,7 +657,7 @@ class DAggerStrategy(RolloutStrategy):
logger.info("Pausing engine - robot holds position") logger.info("Pausing engine - robot holds position")
engine.pause() engine.pause()
if self.config.smooth_handover and teleop_supports_feedback(teleop) and prev_action is not None: if teleop_supports_feedback(teleop) and prev_action is not None:
# TODO(Maxime): prev_action is in robot action key space (output of robot_action_processor). # TODO(Maxime): prev_action is in robot action key space (output of robot_action_processor).
# send_feedback expects teleop feedback key space. For homogeneous setups (e.g. SO-101 # send_feedback expects teleop feedback key space. For homogeneous setups (e.g. SO-101
# leader + SO-101 follower) the keys are identical so this works. If the processor pipeline # leader + SO-101 follower) the keys are identical so this works. If the processor pipeline
@@ -672,11 +668,7 @@ class DAggerStrategy(RolloutStrategy):
elif old_phase == DAggerPhase.PAUSED and new_phase == DAggerPhase.CORRECTING: elif old_phase == DAggerPhase.PAUSED and new_phase == DAggerPhase.CORRECTING:
logger.info("Entering correction mode - human teleop control") logger.info("Entering correction mode - human teleop control")
if ( if not teleop_supports_feedback(teleop) and prev_action is not None:
self.config.smooth_handover
and not teleop_supports_feedback(teleop)
and prev_action is not None
):
logger.info("Smooth handover: sliding follower to teleop position") logger.info("Smooth handover: sliding follower to teleop position")
obs = robot.get_observation() obs = robot.get_observation()
teleop_action = teleop.get_action() teleop_action = teleop.get_action()
@@ -143,6 +143,10 @@ class EpisodicStrategy(RolloutStrategy):
# position so the operator takes over without fighting the arm. # position so the operator takes over without fighting the arm.
# For non-actuated teleops: slide the follower to the teleop's current # For non-actuated teleops: slide the follower to the teleop's current
# pose instead, since the leader cannot be driven. # pose instead, since the leader cannot be driven.
# 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() obs = robot.get_observation()
current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")} current_pos = {k: v for k, v in obs.items() if k.endswith(".pos")}
if ( if (