safety net to stop on huge jumps

This commit is contained in:
Maxime Ellerbach
2026-07-21 08:37:26 +00:00
parent bc2d56f8bf
commit 102e6091cd
2 changed files with 63 additions and 0 deletions
+14
View File
@@ -223,6 +223,15 @@ class RolloutConfig:
fps: float = 30.0
duration: float = 0.0 # 0 = infinite (24/7 mode)
interpolation_multiplier: int = 1
# Safety net (opt-in): if any commanded joint's target differs from the robot's
# currently-measured position by more than this many units in a single control
# step, the action is treated as an unsafe jump — the robot is NOT commanded and
# the rollout stops. Units are the robot's raw `.pos` values (degrees for the
# SO-arms). Gripper joints are excluded (different unit/range). `None` disables it.
# Guards against chunk-splice / bad-chunk slams; a normal per-tick move at fps=30
# is only a few degrees, so a threshold like 20-30 catches slams without tripping
# on legitimate fast motion.
max_action_jump_deg: float | None = None
device: str | None = None
task: str = ""
display_data: bool = False
@@ -274,6 +283,11 @@ class RolloutConfig:
self.inference.type,
)
if self.max_action_jump_deg is not None and self.max_action_jump_deg <= 0:
raise ValueError(
f"max_action_jump_deg must be positive when set, got {self.max_action_jump_deg}"
)
# --- Strategy-specific validation ---
if isinstance(self.strategy, DAggerStrategyConfig) and self.teleop is None:
raise ValueError("DAgger strategy requires --teleop.type to be set")
+49
View File
@@ -273,6 +273,36 @@ def estimate_max_episode_seconds(
# ---------------------------------------------------------------------------
def detect_unsafe_action_jumps(
action_dict: dict,
obs_raw: dict,
max_jump: float,
exclude_substrings: tuple[str, ...] = ("gripper",),
) -> dict[str, float]:
"""Return ``{joint: jump}`` for joints whose commanded target exceeds the current
measured position by more than ``max_jump`` in a single control step; empty if safe.
Only keys present in both ``action_dict`` and ``obs_raw`` and not matching an
excluded substring are checked (grippers use a different unit/range). A large
single-step jump is the signature of a chunk-splice slam: a well-behaved trajectory
advances a joint by only a few units per tick.
"""
violations: dict[str, float] = {}
for key, target in action_dict.items():
if any(token in key for token in exclude_substrings):
continue
current = obs_raw.get(key)
if current is None:
continue
try:
jump = abs(float(target) - float(current))
except (TypeError, ValueError):
continue
if jump > max_jump:
violations[key] = jump
return violations
def send_next_action(
obs_processed: dict,
obs_raw: dict,
@@ -306,6 +336,25 @@ def send_next_action(
if len(interp) != len(ordered_keys):
raise ValueError(f"Interpolated tensor length ({len(interp)}) != action keys ({len(ordered_keys)})")
action_dict = {k: interp[i].item() for i, k in enumerate(ordered_keys)}
# Safety net: refuse to command a single-step joint jump larger than the configured
# limit and stop the rollout. Catches chunk-splice / bad-chunk slams before they reach
# the motors. Compares the commanded target against the freshly-measured pose in
# `obs_raw` (same `.pos` units); no-op when the limit is unset.
max_jump = ctx.runtime.cfg.max_action_jump_deg
if max_jump is not None:
unsafe = detect_unsafe_action_jumps(action_dict, obs_raw, max_jump)
if unsafe:
detail = ", ".join(f"{k}={v:.1f}" for k, v in sorted(unsafe.items(), key=lambda kv: -kv[1]))
logger.error(
"SAFETY STOP: commanded joint jump exceeds %.1f in one step (%s); "
"refusing to send action and signalling shutdown.",
max_jump,
detail,
)
ctx.runtime.shutdown_event.set()
return None
processed = ctx.processors.robot_action_processor((action_dict, obs_raw))
ctx.hardware.robot_wrapper.send_action(processed)
return action_dict