| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from __future__ import annotations |
|
|
| from typing import TYPE_CHECKING |
|
|
| import numpy as np |
|
|
| from lerobot.utils.import_utils import require_package |
|
|
| _placo_runtime_error: ImportError | None = None |
|
|
| if TYPE_CHECKING: |
| import placo |
| else: |
| try: |
| import placo |
| except ImportError as _placo_import_err: |
| placo = None |
| _placo_runtime_error = _placo_import_err |
|
|
|
|
| def _raise_if_placo_unusable() -> None: |
| if placo is None and _placo_runtime_error is not None: |
| raise ImportError( |
| f"placo is installed but failed to import: {_placo_runtime_error!s}" |
| ) from _placo_runtime_error |
|
|
|
|
| class RobotKinematics: |
| """Robot kinematics using placo library for forward and inverse kinematics.""" |
|
|
| def __init__( |
| self, |
| urdf_path: str, |
| target_frame_name: str = "gripper_frame_link", |
| joint_names: list[str] | None = None, |
| ): |
| """ |
| Initialize placo-based kinematics solver. |
| |
| Args: |
| urdf_path (str): Path to the robot URDF file |
| target_frame_name (str): Name of the end-effector frame in the URDF |
| joint_names (list[str] | None): List of joint names to use for the kinematics solver |
| """ |
| require_package("placo", extra="placo-dep") |
| _raise_if_placo_unusable() |
|
|
| self.robot = placo.RobotWrapper(urdf_path) |
| self.solver = placo.KinematicsSolver(self.robot) |
| self.solver.mask_fbase(True) |
|
|
| self.target_frame_name = target_frame_name |
|
|
| |
| self.joint_names = list(self.robot.joint_names()) if joint_names is None else joint_names |
|
|
| |
| self.tip_frame = self.solver.add_frame_task(self.target_frame_name, np.eye(4)) |
|
|
| def forward_kinematics(self, joint_pos_deg: np.ndarray) -> np.ndarray: |
| """ |
| Compute forward kinematics for given joint configuration given the target frame name in the constructor. |
| |
| Args: |
| joint_pos_deg: Joint positions in degrees (numpy array) |
| |
| Returns: |
| 4x4 transformation matrix of the end-effector pose |
| """ |
|
|
| |
| joint_pos_rad = np.deg2rad(joint_pos_deg[: len(self.joint_names)]) |
|
|
| |
| for i, joint_name in enumerate(self.joint_names): |
| self.robot.set_joint(joint_name, joint_pos_rad[i]) |
|
|
| |
| self.robot.update_kinematics() |
|
|
| |
| return self.robot.get_T_world_frame(self.target_frame_name) |
|
|
| def inverse_kinematics( |
| self, |
| current_joint_pos: np.ndarray, |
| desired_ee_pose: np.ndarray, |
| position_weight: float = 1.0, |
| orientation_weight: float = 0.01, |
| ) -> np.ndarray: |
| """ |
| Compute inverse kinematics using placo solver. |
| |
| Args: |
| current_joint_pos: Current joint positions in degrees (used as initial guess) |
| desired_ee_pose: Target end-effector pose as a 4x4 transformation matrix |
| position_weight: Weight for position constraint in IK |
| orientation_weight: Weight for orientation constraint in IK, set to 0.0 to only constrain position |
| |
| Returns: |
| Joint positions in degrees that achieve the desired end-effector pose |
| """ |
|
|
| |
| current_joint_rad = np.deg2rad(current_joint_pos[: len(self.joint_names)]) |
|
|
| |
| for i, joint_name in enumerate(self.joint_names): |
| self.robot.set_joint(joint_name, current_joint_rad[i]) |
|
|
| |
| self.tip_frame.T_world_frame = desired_ee_pose |
|
|
| |
| self.tip_frame.configure(self.target_frame_name, "soft", position_weight, orientation_weight) |
|
|
| |
| self.solver.solve(True) |
| self.robot.update_kinematics() |
|
|
| |
| joint_pos_rad = [] |
| for joint_name in self.joint_names: |
| joint = self.robot.get_joint(joint_name) |
| joint_pos_rad.append(joint) |
|
|
| |
| joint_pos_deg = np.rad2deg(joint_pos_rad) |
|
|
| |
| if len(current_joint_pos) > len(self.joint_names): |
| result = np.zeros_like(current_joint_pos) |
| result[: len(self.joint_names)] = joint_pos_deg |
| result[len(self.joint_names) :] = current_joint_pos[len(self.joint_names) :] |
| return result |
| else: |
| return joint_pos_deg |
|
|