robogym/robot/ur16e/mujoco/free_dof_tcp_arm.py (155 lines of code) (raw):
from typing import Dict, List, Optional
import numpy as np
from robogym.robot.control.tcp.mocap_solver import MocapSolver
from robogym.robot.control.tcp.solver import PrincipalAxis
from robogym.robot.robot_interface import RobotControlParameters, TcpSolverMode
from robogym.robot.ur16e.arm_interface import Arm
from robogym.robot.ur16e.mujoco.joint_controlled_arm import MujocoObservation
from robogym.robot.ur16e.mujoco.simulation.base import ArmSimulationInterface
# speed factors that are scaled by the max_position_change.
DOF_DIM_SPEED_SCALE: Dict[PrincipalAxis, float] = {
PrincipalAxis.ROLL: np.deg2rad(200),
PrincipalAxis.PITCH: np.deg2rad(600),
PrincipalAxis.YAW: np.deg2rad(300),
}
class FreeDOFTcpArm(Arm):
"""
Mujoco implementation of a tool center point (TCP) actuated UR 16e arm
with a user-defined set of quaternion DOFs.
"""
JOINT_DRIFT_THRESHOLD = np.deg2rad(
1
) # an additional buffer to prevent us from hitting joint range
DOF_DIMS: List[PrincipalAxis] = []
ALIGN_AXIS: Optional[PrincipalAxis] = None
def __init__(
self,
simulation: ArmSimulationInterface,
robot_control_params: RobotControlParameters,
initial_qpos: Optional[List],
robot_prefix="robot0:",
autostep=False,
):
"""
:param simulation: simulation interface for the mujoco robot.
:param robot_control_params: Robot control parameters
:param initial_qpos: The initial valueto be applied to the simulation.
:param robot_prefix: prefix to add to the joint names while constructing the mujoco simulation.
:param autostep: when true, calls step() on the simulation whenever a control is set. this
should only be used only when the Robot is being controlled without a
simulation runner in the loop.
"""
super().__init__()
self.simulation = simulation
self.robot_prefix = robot_prefix
self.autostep = autostep
self.joint_group = robot_prefix + "arm_joint_angles"
self.simulation.register_joint_group(
self.joint_group, prefix=robot_prefix + "J"
)
if initial_qpos is not None:
self.simulation.set_qpos(self.joint_group, initial_qpos)
self.simulation.forward()
assert (
robot_control_params.max_position_change
and robot_control_params.max_position_change > 0.0
), "Position multiplier must be a positive number"
self._max_position_change = robot_control_params.max_position_change
self.speed_per_dof_dim = [
DOF_DIM_SPEED_SCALE[axis] * self._max_position_change
for axis in self.DOF_DIMS
]
self.is_in_joint_control_mode = False
tcp_solver_mode = robot_control_params.tcp_solver_mode
assert (
tcp_solver_mode is TcpSolverMode.MOCAP
), f"Invalid solver mode: {tcp_solver_mode}"
self.solver = MocapSolver(
simulation=self.simulation,
body_name="robot0:gripper_tcp",
robot_prefix=self.robot_prefix,
quat_dof_dims=self.DOF_DIMS,
alignment_axis=self.ALIGN_AXIS,
)
def get_name(self) -> str:
return "unnamed-mujoco-ur16e-tcp-arm"
def switch_to_joint_control(self) -> None:
"""Set flag so that commands are now interpreted as joint space commands. See reach_helper for details on
why this is necessary."""
self.is_in_joint_control_mode = True
def switch_to_tcp_control(self) -> None:
"""Set flag so that commands are now interpreted as TCP space commands. See reach_helper for details on
why this is necessary."""
self.is_in_joint_control_mode = False
@property
def mj_sim(self):
""" MuJoCo MjSim simulation object """
return self.simulation.mj_sim
def get_control_time_delta(self) -> float:
"""Returns the delta that the robot wants to be controlled under, by matching it with its mujoco simulation
step delta.
:return: Returns the delta that the robot wants to be controlled under, by matching it with its mujoco simulation
step delta.
"""
dt = self.mj_sim.nsubsteps * self.mj_sim.model.opt.timestep
return dt
def zero_control(self):
return np.zeros(3 + len(self.DOF_DIMS))
def get_robot_transform(self) -> Optional[np.ndarray]:
"""Return the robot transformation wrt its mujoco world. Coordinates are (xyz + rot_quat).
:return: Return the robot transformation wrt its mujoco world.
"""
robot_xyz = self.mj_sim.data.get_body_xpos(
f"{self.robot_prefix}base_link"
).copy()
robot_quat = self.mj_sim.data.get_body_xquat(
f"{self.robot_prefix}base_link"
).copy()
return np.concatenate((robot_xyz, robot_quat))
def constrain_quat_ctrl(self, ctrl: np.ndarray):
"""
Constrain quat control if the solver defines a mapping between control dimensions and
joints
:param ctrl:
:return:
"""
joint_ids = self.solver.get_joint_mapping()
if not any(joint_ids):
return ctrl
joint_ids_mask = [i for i, v in enumerate(joint_ids) if v is not None]
joint_mask = np.array(joint_ids[joint_ids_mask], dtype=np.int8)
joint_pos = self.observe().joint_positions()[joint_mask]
ctrl[joint_ids_mask] = np.clip(
ctrl[joint_ids_mask],
self.actuator_ctrl_range_lower_bound()[joint_mask]
+ self.JOINT_DRIFT_THRESHOLD
- joint_pos,
self.actuator_ctrl_range_upper_bound()[joint_mask]
- self.JOINT_DRIFT_THRESHOLD
- joint_pos,
)
return ctrl
@property
def max_position_change(self):
return self._max_position_change
def denormalize_position_control(
self, position_control: np.ndarray, relative_action: bool = False,
) -> np.ndarray:
# if in joint control, delegate on parent
if self.is_in_joint_control_mode:
raise NotImplementedError(
"Denormalization is not implemented with joint controls."
)
if relative_action and self.max_position_change is not None:
return np.concatenate(
(
position_control[:3] * self.max_position_change,
np.multiply(position_control[3:], self.speed_per_dof_dim),
)
)
else:
return position_control
def set_position_control(self, control: np.ndarray) -> None:
# if in joint control, directly apply to the position
if self.is_in_joint_control_mode:
self.mj_sim.data.qpos[:6] = control
if self.autostep:
self.solver.reset()
self.mj_sim.step()
return
# Arm action space is TCP position [x,y,z] + arm wrist angle.
assert control.shape == (
len(self.zero_control()),
), f"{control} vs {self.zero_control()}"
pos, angle = np.split(control, (3,))
angle = self.constrain_quat_ctrl(angle)
quat_ctrl = self.solver.get_tcp_quat(angle)
agg_control = np.concatenate([pos, quat_ctrl])
agg_control = np.array(agg_control, dtype=np.double)
self.solver.set_action(agg_control)
if self.autostep:
self.mj_sim.step()
def observe(self) -> MujocoObservation:
return MujocoObservation(self.simulation, self.robot_prefix, self.joint_group)
def get_joint_state(self) -> np.ndarray:
return self.observe().joint_positions()
def sync_to(
self, joint_positions: np.ndarray, joint_controls: Optional[np.ndarray]
):
"""Update this arm to the given position and control. Update from values rather than observations so
that we can sync from any observation providers.
:param joint_positions: Arm position.
:param joint_controls: Arm control target. Currently unused since this arm will be controlled during
the tick.
"""
self.simulation.set_qpos(self.joint_group, joint_positions)
self.mj_sim.forward()
def reset(self) -> None:
self.solver.reset()
def actuator_ctrl_range_upper_bound(self) -> np.ndarray:
# We use the joint range in xml since there are no actuators for this arm.
return self.mj_sim.model.jnt_range[:6, 1]
def actuator_ctrl_range_lower_bound(self) -> np.ndarray:
# We use the joint range in xml since there are no actuators for this arm.
return self.mj_sim.model.jnt_range[:6, 0]
class FreeWristTcpArm(FreeDOFTcpArm):
DOF_DIMS = [PrincipalAxis.PITCH]
# WARNING: We share here the notation RPY with PrincipalAxis, but current code may use this wrt world axes, which
# don't necessarily map those of the arm/tcp/gripper.
# TODO Fix this alignment disparity
ALIGN_AXIS = PrincipalAxis.PITCH
class FreeRollYawTcpArm(FreeDOFTcpArm):
"""
TCP with DOF for roll and yaw. This mode currently
is only supported using the MocapJoint solver.
"""
DOF_DIMS = [PrincipalAxis.ROLL, PrincipalAxis.PITCH]