# Copyright 2025 The HuggingFace Inc. team. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Action interpolation for smoother robot control. Provides configurable Nx control rate by interpolating between consecutive actions. Useful with RTC and action-chunking policies to reduce jerkiness. """ from torch import Tensor class ActionInterpolator: """Interpolates between consecutive actions for smoother control. When enabled with multiplier N, produces N actions per policy action by linearly interpolating between the previous and current action. Example with multiplier=3: prev_action -> [1/3 interpolated, 2/3 interpolated, current_action] This effectively multiplies the control rate for smoother motion. Usage: interpolator = ActionInterpolator(multiplier=2) # 2x control rate # In control loop: if interpolator.needs_new_action(): new_action = queue.get() if new_action: interpolator.add(new_action.cpu()) action = interpolator.get() if action: robot.send_action(action) """ def __init__(self, multiplier: int = 1): """Initialize the interpolator. Args: multiplier: Control rate multiplier (1 = no interpolation, 2 = 2x, 3 = 3x, etc.) """ if multiplier < 1: raise ValueError(f"multiplier must be >= 1, got {multiplier}") self.multiplier = multiplier self._prev: Tensor | None = None self._buffer: list[Tensor] = [] self._idx = 0 @property def enabled(self) -> bool: """Whether interpolation is active (multiplier > 1).""" return self.multiplier > 1 def reset(self): """Reset interpolation state (call between episodes).""" self._prev = None self._buffer = [] self._idx = 0 def needs_new_action(self) -> bool: """Check if a new action is needed from the queue.""" return self._idx >= len(self._buffer) def add(self, action: Tensor) -> None: """Add a new action and compute interpolated sequence. Args: action: New action tensor from policy/queue (already on CPU). """ if self.multiplier > 1 and self._prev is not None: self._buffer = [] for i in range(1, self.multiplier + 1): t = i / self.multiplier interp = self._prev + t * (action - self._prev) self._buffer.append(interp) else: # First step: no previous action yet, so run at base FPS without interpolation. self._buffer = [action.clone()] self._prev = action.clone() self._idx = 0 def get(self) -> Tensor | None: """Get the next interpolated action. Returns: Next action tensor, or None if buffer is exhausted. """ if self._idx >= len(self._buffer): return None action = self._buffer[self._idx] self._idx += 1 return action def get_control_interval(self, fps: float) -> float: """Get the control interval based on interpolation multiplier. Args: fps: Base frames per second. Returns: Control interval in seconds (divided by multiplier). """ return 1.0 / (fps * self.multiplier)