feat(rollout): add smooth_handover flag to DAgger strategy config

The DAgger phase transitions run blocking smooth handovers: on pause the
leader is driven to the follower (~2 s), and on correction start the
follower is slid to the teleop pose (~1 s), both inside the record loop.

For clutch-style teleoperators (e.g. VR controllers) that re-reference
their command frame at the current robot pose on engage, the handover is
already continuous — the interpolation only delays the start of the
correction and eats its first frames.

Add --strategy.smooth_handover (default true, existing behavior
unchanged) to let such setups skip it, mirroring the episodic strategy's
smooth_leader_to_follower_handover flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
griffinaddison
2026-07-10 14:11:45 -07:00
committed by Maxime Ellerbach
parent 0d383d09f2
commit 124d03608c
3 changed files with 20 additions and 3 deletions
+1
View File
@@ -155,6 +155,7 @@ Foot pedal input is also supported via `--strategy.input_device=pedal`. Configur
| `--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`)
+8
View File
@@ -180,6 +180,14 @@ 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)
+11 -3
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,6 +634,10 @@ 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.
@@ -657,7 +661,7 @@ class DAggerStrategy(RolloutStrategy):
logger.info("Pausing engine - robot holds position") logger.info("Pausing engine - robot holds position")
engine.pause() engine.pause()
if teleop_supports_feedback(teleop) and prev_action is not None: if self.config.smooth_handover and 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
@@ -668,7 +672,11 @@ 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 not teleop_supports_feedback(teleop) and prev_action is not None: if (
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()