gym_hil/controllers/opspace.py (178 lines of code) (raw):

#!/usr/bin/env python # Copyright 2024 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. from typing import Optional, Tuple, Union import mujoco import numpy as np def mat_to_quat(mat: np.ndarray) -> np.ndarray: """Convert a 3x3 rotation matrix to a quaternion. Args: mat: 3x3 rotation matrix Returns: Quaternion as [w, x, y, z] """ trace = np.trace(mat) if trace > 0: s = 2.0 * np.sqrt(trace + 1.0) w = 0.25 * s x = (mat[2, 1] - mat[1, 2]) / s y = (mat[0, 2] - mat[2, 0]) / s z = (mat[1, 0] - mat[0, 1]) / s elif mat[0, 0] > mat[1, 1] and mat[0, 0] > mat[2, 2]: s = 2.0 * np.sqrt(1.0 + mat[0, 0] - mat[1, 1] - mat[2, 2]) w = (mat[2, 1] - mat[1, 2]) / s x = 0.25 * s y = (mat[0, 1] + mat[1, 0]) / s z = (mat[0, 2] + mat[2, 0]) / s elif mat[1, 1] > mat[2, 2]: s = 2.0 * np.sqrt(1.0 + mat[1, 1] - mat[0, 0] - mat[2, 2]) w = (mat[0, 2] - mat[2, 0]) / s x = (mat[0, 1] + mat[1, 0]) / s y = 0.25 * s z = (mat[1, 2] + mat[2, 1]) / s else: s = 2.0 * np.sqrt(1.0 + mat[2, 2] - mat[0, 0] - mat[1, 1]) w = (mat[1, 0] - mat[0, 1]) / s x = (mat[0, 2] + mat[2, 0]) / s y = (mat[1, 2] + mat[2, 1]) / s z = 0.25 * s return np.array([w, x, y, z]) def quat_diff_active(source_quat: np.ndarray, target_quat: np.ndarray) -> np.ndarray: """Compute the quaternion difference from source to target. Args: source_quat: Source quaternion [w, x, y, z] target_quat: Target quaternion [w, x, y, z] Returns: Quaternion representing rotation from source to target """ # q_diff = q_target * q_source^(-1) # For unit quaternions, inverse is conjugate source_conj = np.array([source_quat[0], -source_quat[1], -source_quat[2], -source_quat[3]]) # Quaternion multiplication: q1 * q2 w1, x1, y1, z1 = target_quat w2, x2, y2, z2 = source_conj w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2 z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2 return np.array([w, x, y, z]) def quat_to_axisangle(quat: np.ndarray) -> np.ndarray: """Convert a quaternion to axis-angle representation. Args: quat: Quaternion [w, x, y, z] Returns: Axis-angle vector (angle * axis) """ w, x, y, z = quat # Normalize quaternion norm = np.sqrt(w * w + x * x + y * y + z * z) if norm < 1e-10: return np.zeros(3) w /= norm x /= norm y /= norm z /= norm # Compute angle angle = 2.0 * np.arccos(np.clip(w, -1.0, 1.0)) # Compute axis sin_half_angle = np.sqrt(1.0 - w * w) if sin_half_angle < 1e-10: # Small angle, arbitrary axis return np.array([angle * x, angle * y, angle * z]) axis = np.array([x, y, z]) / sin_half_angle # Return axis-angle vector return angle * axis def pd_control( x: np.ndarray, x_des: np.ndarray, dx: np.ndarray, kp_kv: np.ndarray, ddx_max: float = 0.0, ) -> np.ndarray: # Compute error. x_err = x - x_des dx_err = dx # Apply gains. x_err *= -kp_kv[:, 0] dx_err *= -kp_kv[:, 1] # Limit maximum error. if ddx_max > 0.0: x_err_sq_norm = np.sum(x_err**2) ddx_max_sq = ddx_max**2 if x_err_sq_norm > ddx_max_sq: x_err *= ddx_max / np.sqrt(x_err_sq_norm) return x_err + dx_err def pd_control_orientation( quat: np.ndarray, quat_des: np.ndarray, w: np.ndarray, kp_kv: np.ndarray, dw_max: float = 0.0, ) -> np.ndarray: # Compute error. quat_err = quat_diff_active(source_quat=quat_des, target_quat=quat) ori_err = quat_to_axisangle(quat_err) w_err = w # Apply gains. ori_err *= -kp_kv[:, 0] w_err *= -kp_kv[:, 1] # Limit maximum error. if dw_max > 0.0: ori_err_sq_norm = np.sum(ori_err**2) dw_max_sq = dw_max**2 if ori_err_sq_norm > dw_max_sq: ori_err *= dw_max / np.sqrt(ori_err_sq_norm) return ori_err + w_err def opspace( model, data, site_id, dof_ids: np.ndarray, pos: Optional[np.ndarray] = None, ori: Optional[np.ndarray] = None, joint: Optional[np.ndarray] = None, pos_gains: Union[Tuple[float, float, float], np.ndarray] = (200.0, 200.0, 200.0), ori_gains: Union[Tuple[float, float, float], np.ndarray] = (200.0, 200.0, 200.0), damping_ratio: float = 1.0, nullspace_stiffness: float = 0.5, max_pos_acceleration: Optional[float] = None, max_ori_acceleration: Optional[float] = None, gravity_comp: bool = True, ) -> np.ndarray: x_des = data.site_xpos[site_id] if pos is None else np.asarray(pos) if ori is None: xmat = data.site_xmat[site_id].reshape((3, 3)) quat_des = mat_to_quat(xmat.reshape((3, 3))) else: ori = np.asarray(ori) quat_des = mat_to_quat(ori) if ori.shape == (3, 3) else ori q_des = data.qpos[dof_ids] if joint is None else np.asarray(joint) kp = np.asarray(pos_gains) kd = damping_ratio * 2 * np.sqrt(kp) kp_kv_pos = np.stack([kp, kd], axis=-1) kp = np.asarray(ori_gains) kd = damping_ratio * 2 * np.sqrt(kp) kp_kv_ori = np.stack([kp, kd], axis=-1) kp_joint = np.full((len(dof_ids),), nullspace_stiffness) kd_joint = damping_ratio * 2 * np.sqrt(kp_joint) kp_kv_joint = np.stack([kp_joint, kd_joint], axis=-1) ddx_max = max_pos_acceleration if max_pos_acceleration is not None else 0.0 dw_max = max_ori_acceleration if max_ori_acceleration is not None else 0.0 # Get current state. q = data.qpos[dof_ids] dq = data.qvel[dof_ids] # Compute Jacobian of the eef site in world frame. J_v = np.zeros((3, model.nv), dtype=np.float64) # noqa: N806 J_w = np.zeros((3, model.nv), dtype=np.float64) # noqa: N806 mujoco.mj_jacSite( model, data, J_v, J_w, site_id, ) J_v = J_v[:, dof_ids] # noqa: N806 J_w = J_w[:, dof_ids] # noqa: N806 J = np.concatenate([J_v, J_w], axis=0) # noqa: N806 # Compute position PD control. x = data.site_xpos[site_id] dx = J_v @ dq ddx = pd_control( x=x, x_des=x_des, dx=dx, kp_kv=kp_kv_pos, ddx_max=ddx_max, ) # Compute orientation PD control. quat = mat_to_quat(data.site_xmat[site_id].reshape((3, 3))) if quat @ quat_des < 0.0: quat *= -1.0 w = J_w @ dq dw = pd_control_orientation( quat=quat, quat_des=quat_des, w=w, kp_kv=kp_kv_ori, dw_max=dw_max, ) # Compute inertia matrix in joint space. M = np.zeros((model.nv, model.nv), dtype=np.float64) # noqa: N806 mujoco.mj_fullM(model, M, data.qM) M = M[dof_ids, :][:, dof_ids] # noqa: N806 # Compute inertia matrix in task space. M_inv = np.linalg.inv(M) # noqa: N806 Mx_inv = J @ M_inv @ J.T # noqa: N806 Mx = np.linalg.inv(Mx_inv) if abs(np.linalg.det(Mx_inv)) >= 1e-2 else np.linalg.pinv(Mx_inv, rcond=1e-2) # noqa: N806 # Compute generalized forces. ddx_dw = np.concatenate([ddx, dw], axis=0) tau = J.T @ Mx @ ddx_dw # Add joint task in nullspace. ddq = pd_control( x=q, x_des=q_des, dx=dq, kp_kv=kp_kv_joint, ddx_max=0.0, ) Jnull = M_inv @ J.T @ Mx # noqa: N806 tau += (np.eye(len(q)) - J.T @ Jnull.T) @ ddq if gravity_comp: tau += data.qfrc_bias[dof_ids] return tau