Source code for mink_warp.tasks.joint_limit_task
"""Soft joint configuration limits as a least-squares task."""
from __future__ import annotations
import mujoco
import numpy as np
import numpy.typing as npt
import warp as wp
from ..configuration import Configuration
from ..exceptions import TaskDefinitionError
from ..kernels.limits import joint_limit_error_jac
from .task import Task
[docs]
class JointLimitTask(Task):
r"""Soft hinge/slide joint limits as a least-squares penalty.
When :math:`q_i` violates bounds :math:`[q_i^{\min}, q_i^{\max}]`:
.. math::
e_i = \begin{cases}
q_i - q_i^{\max} & q_i > q_i^{\max} \\
q_i - q_i^{\min} & q_i < q_i^{\min} \\
0 & \text{otherwise}
\end{cases}
with :math:`J_{ii} = 1` on limited dofs. Free and ball joints are ignored.
For **hard** limits use :class:`~mink_warp.ConfigurationLimit`.
"""
def __init__(
self,
model: mujoco.MjModel,
cost: npt.ArrayLike = 1.0,
gain: float = 1.0,
lm_damping: float = 0.0,
min_distance_from_limits: float = 0.0,
):
super().__init__(cost=np.zeros(model.nv), gain=gain, lm_damping=lm_damping)
self.k = model.nv
self.model = model
qposadr: list[int] = []
dofadr: list[int] = []
lower: list[float] = []
upper: list[float] = []
for jnt in range(model.njnt):
jnt_type = model.jnt_type[jnt]
if jnt_type in (
mujoco.mjtJoint.mjJNT_FREE,
mujoco.mjtJoint.mjJNT_BALL,
):
continue
if not model.jnt_limited[jnt]:
continue
qposadr.append(int(model.jnt_qposadr[jnt]))
dofadr.append(int(model.jnt_dofadr[jnt]))
lo, hi = model.jnt_range[jnt]
lower.append(float(lo + min_distance_from_limits))
upper.append(float(hi - min_distance_from_limits))
self._n_limited = len(qposadr)
self._qposadr_np = np.asarray(qposadr, dtype=np.int32)
self._dofadr_np = np.asarray(dofadr, dtype=np.int32)
self._lower_np = np.asarray(lower, dtype=np.float32)
self._upper_np = np.asarray(upper, dtype=np.float32)
self._qposadr: wp.array | None = None
self._dofadr: wp.array | None = None
self._lower: wp.array | None = None
self._upper: wp.array | None = None
self.set_cost(cost)
[docs]
def set_cost(self, cost: npt.ArrayLike) -> None:
cost = np.atleast_1d(np.asarray(cost, dtype=np.float64))
if cost.ndim != 1 or cost.shape[0] not in (1, self.k):
raise TaskDefinitionError(
f"cost must be shape (1,) or ({self.k},), got {cost.shape}"
)
if not np.all(cost >= 0.0):
raise TaskDefinitionError("cost must be >= 0")
self.cost[: self.k] = cost
self._cost_dev = None
def _alloc_extra_buffers(self, configuration: Configuration) -> None:
if self._n_limited > 0:
self._qposadr = wp.array(self._qposadr_np, dtype=int)
self._dofadr = wp.array(self._dofadr_np, dtype=int)
self._lower = wp.array(self._lower_np, dtype=float)
self._upper = wp.array(self._upper_np, dtype=float)
else:
self._qposadr = wp.zeros(1, dtype=int)
self._dofadr = wp.zeros(1, dtype=int)
self._lower = wp.zeros(1, dtype=float)
self._upper = wp.zeros(1, dtype=float)
def _eval(self, configuration: Configuration) -> None:
assert self._error is not None
assert self._jacobian is not None
if self._n_limited == 0:
self._error.zero_()
self._jacobian.zero_()
return
with wp.ScopedDevice(configuration.device):
wp.launch(
joint_limit_error_jac,
dim=configuration.nworld,
inputs=[
configuration.q,
self._lower,
self._upper,
self._qposadr,
self._dofadr,
self._n_limited,
configuration.nv,
],
outputs=[self._error, self._jacobian],
)
# Backward-compatible alias.
ConfigurationLimitTask = JointLimitTask