| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| |
| |
| |
| import time |
| from contextlib import nullcontext |
| from copy import copy |
| from typing import TYPE_CHECKING, Any |
|
|
| import numpy as np |
| import torch |
|
|
| from lerobot.policies import PreTrainedPolicy, prepare_observation_for_inference |
| from lerobot.utils.import_utils import _deepdiff_available, require_package |
|
|
| if TYPE_CHECKING or _deepdiff_available: |
| from deepdiff import DeepDiff |
| else: |
| DeepDiff = None |
|
|
| if TYPE_CHECKING: |
| from lerobot.datasets import LeRobotDataset |
| from lerobot.processor import PolicyProcessorPipeline |
| from lerobot.robots import Robot |
| from lerobot.types import PolicyAction |
|
|
|
|
| def predict_action( |
| observation: dict[str, np.ndarray], |
| policy: PreTrainedPolicy, |
| device: torch.device, |
| preprocessor: PolicyProcessorPipeline[dict[str, Any], dict[str, Any]], |
| postprocessor: PolicyProcessorPipeline[PolicyAction, PolicyAction], |
| use_amp: bool, |
| task: str | None = None, |
| robot_type: str | None = None, |
| ): |
| """ |
| Performs a single-step inference to predict a robot action from an observation. |
| |
| This function encapsulates the full inference pipeline: |
| 1. Prepares the observation by converting it to PyTorch tensors and adding a batch dimension. |
| 2. Runs the preprocessor pipeline on the observation. |
| 3. Feeds the processed observation to the policy to get a raw action. |
| 4. Runs the postprocessor pipeline on the raw action. |
| 5. Formats the final action by removing the batch dimension and moving it to the CPU. |
| |
| Args: |
| observation: A dictionary of NumPy arrays representing the robot's current observation. |
| policy: The `PreTrainedPolicy` model to use for action prediction. |
| device: The `torch.device` (e.g., 'cuda' or 'cpu') to run inference on. |
| preprocessor: The `PolicyProcessorPipeline` for preprocessing observations. |
| postprocessor: The `PolicyProcessorPipeline` for postprocessing actions. |
| use_amp: A boolean to enable/disable Automatic Mixed Precision for CUDA inference. |
| task: An optional string identifier for the task. |
| robot_type: An optional string identifier for the robot type. |
| |
| Returns: |
| A `torch.Tensor` containing the predicted action, ready for the robot. |
| """ |
| observation = copy(observation) |
| with ( |
| torch.inference_mode(), |
| torch.autocast(device_type=device.type) if device.type == "cuda" and use_amp else nullcontext(), |
| ): |
| |
| observation = prepare_observation_for_inference(observation, device, task, robot_type) |
| observation = preprocessor(observation) |
|
|
| |
| |
| action = policy.select_action(observation) |
|
|
| action = postprocessor(action) |
|
|
| return action |
|
|
|
|
| def sanity_check_dataset_name(repo_id, policy_cfg): |
| """ |
| Validates the dataset repository name against the presence of a policy configuration. |
| |
| This function enforces a naming convention: a dataset repository ID should start with "eval_" |
| if and only if a policy configuration is provided for evaluation purposes. |
| |
| Args: |
| repo_id: The Hugging Face Hub repository ID of the dataset. |
| policy_cfg: The configuration object for the policy, or `None`. |
| |
| Raises: |
| ValueError: If the naming convention is violated. |
| """ |
| _, dataset_name = repo_id.split("/") |
| |
| |
|
|
| |
| if dataset_name.startswith("eval_") and policy_cfg is None: |
| raise ValueError( |
| f"Your dataset name begins with 'eval_' ({dataset_name}), but no policy is provided." |
| ) |
|
|
| |
| if not dataset_name.startswith("eval_") and policy_cfg is not None: |
| raise ValueError( |
| f"Your dataset name does not begin with 'eval_' ({dataset_name}), but a policy is provided ({policy_cfg.type})." |
| ) |
|
|
|
|
| def sanity_check_dataset_robot_compatibility( |
| dataset: LeRobotDataset, robot: Robot, fps: int, features: dict |
| ) -> None: |
| """ |
| Checks if a dataset's metadata is compatible with the current robot and recording setup. |
| |
| This function compares key metadata fields (`robot_type`, `fps`, and `features`) from the |
| dataset against the current configuration to ensure that appended data will be consistent. |
| |
| Args: |
| dataset: The `LeRobotDataset` instance to check. |
| robot: The `Robot` instance representing the current hardware setup. |
| fps: The current recording frequency (frames per second). |
| features: The dictionary of features for the current recording session. |
| |
| Raises: |
| ValueError: If any of the checked metadata fields do not match. |
| """ |
| require_package("deepdiff", extra="deepdiff-dep") |
|
|
| from lerobot.utils.constants import DEFAULT_FEATURES |
|
|
| fields = [ |
| ("robot_type", dataset.meta.robot_type, robot.robot_type), |
| ("fps", dataset.fps, fps), |
| ("features", dataset.features, {**features, **DEFAULT_FEATURES}), |
| ] |
|
|
| mismatches = [] |
| for field, dataset_value, present_value in fields: |
| diff = DeepDiff(dataset_value, present_value, exclude_regex_paths=[r".*\['info'\]$"]) |
| if diff: |
| mismatches.append(f"{field}: expected {present_value}, got {dataset_value}") |
|
|
| if mismatches: |
| raise ValueError( |
| "Dataset metadata compatibility check failed with mismatches:\n" + "\n".join(mismatches) |
| ) |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| def teleop_supports_feedback(teleop) -> bool: |
| """Return True when the teleop can receive position feedback (is actuated). |
| |
| Actuated teleops (e.g. SO-101, OpenArmMini) have non-empty ``feedback_features`` |
| and expose ``enable_torque`` / ``disable_torque`` motor-control methods. |
| |
| TODO(Maxime): See if it is possible to unify this interface across teleops instead of duck-typing. |
| """ |
| return ( |
| bool(teleop.feedback_features) |
| and hasattr(teleop, "disable_torque") |
| and hasattr(teleop, "enable_torque") |
| ) |
|
|
|
|
| def teleop_smooth_move_to(teleop, target_pos: dict, duration_s: float = 2.0, fps: int = 30) -> None: |
| """Smoothly move an actuated teleop to ``target_pos`` via linear interpolation. |
| |
| Requires the teleoperator to support feedback (i.e. have non-empty |
| ``feedback_features`` and implement ``disable_torque`` / ``enable_torque``). |
| |
| ``target_pos`` is expected to be in the teleop's action/feedback key space. |
| For homogeneous setups (e.g. SO-101 leader + SO-101 follower) this matches |
| the robot action key space directly. |
| |
| TODO(Maxime): This blocks up to ``duration_s`` seconds; during this time the |
| follower robot does not receive new actions, which could be an issue on LeKiwi. |
| """ |
| teleop.enable_torque() |
| current = teleop.get_action() |
| steps = max(int(duration_s * fps), 1) |
|
|
| for step in range(steps + 1): |
| t = step / steps |
| interp = { |
| k: current[k] * (1 - t) + target_pos[k] * t if k in target_pos else current[k] for k in current |
| } |
| teleop.send_feedback(interp) |
| time.sleep(1 / fps) |
|
|
|
|
| def follower_smooth_move_to( |
| robot, current: dict, target: dict, duration_s: float = 1.0, fps: int = 30 |
| ) -> None: |
| """Smoothly move the follower robot from ``current`` to ``target`` action. |
| |
| Used when the teleop is non-actuated: instead of driving the leader arm to |
| the follower, the follower is brought to the teleop's current pose so the |
| robot meets the operator's hand rather than jumping to it on the first frame. |
| |
| Both ``current`` and ``target`` must be in the robot action key space |
| (i.e. the output of ``robot_action_processor``). |
| """ |
| steps = max(int(duration_s * fps), 1) |
|
|
| for step in range(steps + 1): |
| t = step / steps |
| interp = {k: current[k] * (1 - t) + target[k] * t if k in target else current[k] for k in current} |
| robot.send_action(interp) |
| time.sleep(1 / fps) |
|
|