Elliptic friction in MJX.

PiperOrigin-RevId: 641384367
Change-Id: I510c565940324fbbf392ce537ce27e0cb9af3eb1
This commit is contained in:
Erik Frey
2024-06-07 16:04:54 -07:00
committed by Copybara-Service
parent 5b6800116a
commit 4c3d9461ae
12 changed files with 610 additions and 329 deletions
+7
View File
@@ -10,6 +10,13 @@ General
1. Added :ref:`maxhullvert<asset-mesh-maxhullvert>`, the maximum number of vertices in a mesh's convex hull. 1. Added :ref:`maxhullvert<asset-mesh-maxhullvert>`, the maximum number of vertices in a mesh's convex hull.
MJX
~~~
2. Added support for :ref:`elliptic friction cones<option-cone>`.
3. Fixed a bug that resulted in less-optimal linesearch solutions for some difficult constraint settings.
4. Fixed a bug in the Newton solver that sometimes resulted in less-optimal gradients.
Version 3.1.6 (Jun 3, 2024) Version 3.1.6 (Jun 3, 2024)
--------------------------- ---------------------------
+3 -5
View File
@@ -198,13 +198,13 @@ The following features are **fully supported** in MJX:
* - :ref:`Geom <mjtGeom>` * - :ref:`Geom <mjtGeom>`
- ``PLANE``, ``HFIELD``, ``SPHERE``, ``CAPSULE``, ``BOX``, ``MESH`` are fully implemented. ``ELLIPSOID`` and ``CYLINDER`` are implemented but only collide with other primitives. - ``PLANE``, ``HFIELD``, ``SPHERE``, ``CAPSULE``, ``BOX``, ``MESH`` are fully implemented. ``ELLIPSOID`` and ``CYLINDER`` are implemented but only collide with other primitives.
* - :ref:`Constraint <mjtConstraint>` * - :ref:`Constraint <mjtConstraint>`
- ``EQUALITY``, ``LIMIT_JOINT``, ``CONTACT_FRICTIONLESS``, ``CONTACT_PYRAMIDAL`` - ``EQUALITY``, ``LIMIT_JOINT``, ``CONTACT_FRICTIONLESS``, ``CONTACT_PYRAMIDAL``, ``CONTACT_ELLIPTIC``
* - :ref:`Equality <mjtEq>` * - :ref:`Equality <mjtEq>`
- ``CONNECT``, ``WELD``, ``JOINT`` - ``CONNECT``, ``WELD``, ``JOINT``
* - :ref:`Integrator <mjtIntegrator>` * - :ref:`Integrator <mjtIntegrator>`
- ``EULER``, ``RK4`` - ``EULER``, ``RK4``
* - :ref:`Cone <mjtCone>` * - :ref:`Cone <mjtCone>`
- ``PYRAMIDAL`` - ``PYRAMIDAL``, ``ELLIPTIC``
* - :ref:`Condim <coContact>` * - :ref:`Condim <coContact>`
- 1, 3, 4, 6 - 1, 3, 4, 6
* - :ref:`Solver <mjtSolver>` * - :ref:`Solver <mjtSolver>`
@@ -225,7 +225,7 @@ The following features are **in development** and coming soon:
* - :ref:`Geom <mjtGeom>` * - :ref:`Geom <mjtGeom>`
- ``SDF``. Collisions between (``SPHERE``, ``BOX``, ``MESH``, ``HFIELD``) and ``CYLINDER``. Collisions between (``BOX``, ``MESH``, ``HFIELD``) and ``ELLIPSOID``. - ``SDF``. Collisions between (``SPHERE``, ``BOX``, ``MESH``, ``HFIELD``) and ``CYLINDER``. Collisions between (``BOX``, ``MESH``, ``HFIELD``) and ``ELLIPSOID``.
* - :ref:`Constraint <mjtConstraint>` * - :ref:`Constraint <mjtConstraint>`
- :ref:`Frictionloss <coFriction>`, ``CONTACT_ELLIPTIC``, ``FRICTION_DOF`` - :ref:`Frictionloss <coFriction>`, ``FRICTION_DOF``
* - :ref:`Integrator <mjtIntegrator>` * - :ref:`Integrator <mjtIntegrator>`
- ``IMPLICIT``, ``IMPLICITFAST`` - ``IMPLICIT``, ``IMPLICITFAST``
* - Dynamics * - Dynamics
@@ -240,8 +240,6 @@ The following features are **in development** and coming soon:
- ``MUSCLE`` - ``MUSCLE``
* - :ref:`Tendon Wrapping <mjtWrap>` * - :ref:`Tendon Wrapping <mjtWrap>`
- ``NONE``, ``JOINT``, ``PULLEY``, ``SITE``, ``SPHERE``, ``CYLINDER`` - ``NONE``, ``JOINT``, ``PULLEY``, ``SITE``, ``SPHERE``, ``CYLINDER``
* - :ref:`Cone <mjtCone>`
- ``ELLIPTIC``
* - Fluid Model * - Fluid Model
- :ref:`flEllipsoid` - :ref:`flEllipsoid`
* - :ref:`Tendons <tendon>` * - :ref:`Tendons <tendon>`
+4 -1
View File
@@ -239,7 +239,8 @@ def plane_convex(plane: GeomInfo, convex: ConvexInfo) -> Collision:
plane_pos = convex.mat.T @ (plane.pos - convex.pos) plane_pos = convex.mat.T @ (plane.pos - convex.pos)
n = convex.mat.T @ plane.mat[:, 2] n = convex.mat.T @ plane.mat[:, 2]
support = (plane_pos - vert) @ n support = (plane_pos - vert) @ n
idx = _manifold_points(vert, support > jp.maximum(0, support.max() - 1e-4), n) # search for manifold points within a 1mm skin depth
idx = _manifold_points(vert, support > jp.maximum(0, support.max() - 1e-3), n)
pos = vert[idx] pos = vert[idx]
# convert to world frame # convert to world frame
@@ -970,6 +971,7 @@ def _box_box(b1: ConvexInfo, b2: ConvexInfo) -> Collision:
# Go back to world frame. # Go back to world frame.
pos = b2.pos + pos @ b2.mat.T pos = b2.pos + pos @ b2.mat.T
n = normal @ b2.mat.T n = normal @ b2.mat.T
dist = jp.where(jp.isinf(dist), jp.finfo(float).max, dist)
return dist, pos, n return dist, pos, n
@@ -1029,6 +1031,7 @@ def _convex_convex(c1: ConvexInfo, c2: ConvexInfo) -> Collision:
pos = c2.pos + pos @ c2.mat.T pos = c2.pos + pos @ c2.mat.T
n = normal @ c2.mat.T n = normal @ c2.mat.T
n = -n if swapped else n n = -n if swapped else n
dist = jp.where(jp.isinf(dist), jp.finfo(float).max, dist)
return dist, pos, n return dist, pos, n
+240 -187
View File
@@ -24,6 +24,7 @@ from mujoco.mjx._src import math
from mujoco.mjx._src import support from mujoco.mjx._src import support
# pylint: disable=g-importing-member # pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode from mujoco.mjx._src.dataclasses import PyTreeNode
from mujoco.mjx._src.types import ConeType
from mujoco.mjx._src.types import ConstraintType from mujoco.mjx._src.types import ConstraintType
from mujoco.mjx._src.types import Contact from mujoco.mjx._src.types import Contact
from mujoco.mjx._src.types import Data from mujoco.mjx._src.types import Data
@@ -35,18 +36,14 @@ from mujoco.mjx._src.types import Model
import numpy as np import numpy as np
_CONDIM_EFC_COUNT = {1: 1, 3: 4, 4: 6, 6: 10}
class _Efc(PyTreeNode): class _Efc(PyTreeNode):
"""Support data for creating constraint matrices.""" """Support data for creating constraint matrices."""
J: jax.Array J: jax.Array
pos: jax.Array pos_aref: jax.Array
pos_norm: jax.Array pos_imp: jax.Array
invweight: jax.Array invweight: jax.Array
solref: jax.Array solref: jax.Array
solimp: jax.Array solimp: jax.Array
frictionloss: jax.Array
def _kbi( def _kbi(
@@ -59,13 +56,13 @@ def _kbi(
timeconst, dampratio = solref timeconst, dampratio = solref
if not m.opt.disableflags & DisableBit.REFSAFE: if not m.opt.disableflags & DisableBit.REFSAFE:
timeconst = jp.maximum(timeconst, 2 * m.opt.timestep) * (timeconst > 0) timeconst = jp.maximum(timeconst, 2 * m.opt.timestep)
dmin, dmax, width, mid, power = solimp dmin, dmax, width, mid, power = solimp
dmin = jp.clip(dmin, mujoco.mjMINIMP, mujoco.mjMAXIMP) dmin = jp.clip(dmin, mujoco.mjMINIMP, mujoco.mjMAXIMP)
dmax = jp.clip(dmax, mujoco.mjMINIMP, mujoco.mjMAXIMP) dmax = jp.clip(dmax, mujoco.mjMINIMP, mujoco.mjMAXIMP)
width = jp.maximum(0, width) width = jp.maximum(mujoco.mjMINVAL, width)
mid = jp.clip(mid, mujoco.mjMINIMP, mujoco.mjMAXIMP) mid = jp.clip(mid, mujoco.mjMINIMP, mujoco.mjMAXIMP)
power = jp.maximum(1, power) power = jp.maximum(1, power)
@@ -73,8 +70,8 @@ def _kbi(
k = 1 / (dmax * dmax * timeconst * timeconst * dampratio * dampratio) k = 1 / (dmax * dmax * timeconst * timeconst * dampratio * dampratio)
b = 2 / (dmax * timeconst) b = 2 / (dmax * timeconst)
# TODO(robotics-simulation): check various solparam settings in model gen test # TODO(robotics-simulation): check various solparam settings in model gen test
k = jp.where(dampratio <= 0, -solref[0] / (dmax * dmax), k) k = jp.where(solref[0] <= 0, -solref[0] / (dmax * dmax), k)
b = jp.where(timeconst <= 0, -solref[1] / dmax, b) b = jp.where(solref[1] <= 0, -solref[1] / dmax, b)
imp_x = jp.abs(pos) / width imp_x = jp.abs(pos) / width
imp_a = (1.0 / jp.power(mid, power - 1)) * jp.power(imp_x, power) imp_a = (1.0 / jp.power(mid, power - 1)) * jp.power(imp_x, power)
@@ -87,254 +84,280 @@ def _kbi(
return k, b, imp # corresponds to K, B, I of efc_KBIP return k, b, imp # corresponds to K, B, I of efc_KBIP
def _instantiate_equality_connect(m: Model, d: Data) -> Optional[_Efc]: def _row(j: jax.Array, *args) -> _Efc:
"""Creates an efc row, ensuring args all have same row count."""
if len(j.shape) < 2:
return _Efc(j, *args) # if j isn't batched, ignore
args = list(args)
for i, arg in enumerate(args):
if not arg.shape or arg.shape[0] != j.shape[0]:
args[i] = jp.tile(arg, (j.shape[0],) + (1,) * (len(arg.shape)))
return _Efc(j, *args)
def _efc_equality_connect(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for connect equality constraints.""" """Calculates constraint rows for connect equality constraints."""
ids = np.nonzero(m.eq_type == EqType.CONNECT)[0] eq_id = np.nonzero(m.eq_type == EqType.CONNECT)[0]
if (m.opt.disableflags & DisableBit.EQUALITY) or eq_id.size == 0:
if (m.opt.disableflags & DisableBit.EQUALITY) or ids.size == 0:
return None return None
id1, id2, data = m.eq_obj1id[ids], m.eq_obj2id[ids], m.eq_data[ids]
@jax.vmap @jax.vmap
def fn(data, id1, id2): def rows(obj1id, obj2id, data, solref, solimp):
anchor1, anchor2 = data[0:3], data[3:6] anchor1, anchor2 = data[0:3], data[3:6]
# find global points
pos1 = d.xmat[id1] @ anchor1 + d.xpos[id1]
pos2 = d.xmat[id2] @ anchor2 + d.xpos[id2]
# compute position error # error is difference in global positions
cpos = pos1 - pos2 pos1 = d.xmat[obj1id] @ anchor1 + d.xpos[obj1id]
pos2 = d.xmat[obj2id] @ anchor2 + d.xpos[obj2id]
pos = pos1 - pos2
# compute Jacobian difference (opposite of contact: 0 - 1) # compute Jacobian difference (opposite of contact: 0 - 1)
jacp1, _ = support.jac(m, d, pos1, id1) jacp1, _ = support.jac(m, d, pos1, obj1id)
jacp2, _ = support.jac(m, d, pos2, id2) jacp2, _ = support.jac(m, d, pos2, obj2id)
j = (jacp1 - jacp2).T j = (jacp1 - jacp2).T
pos_imp = math.norm(pos)
invweight = m.body_invweight0[obj1id, 0] + m.body_invweight0[obj2id, 0]
return j, cpos, jp.repeat(math.norm(cpos), 3) return _row(j, pos, pos_imp, invweight, solref, solimp)
# concatenate to drop connect grouping dimension args = (m.eq_obj1id, m.eq_obj2id, m.eq_data, m.eq_solref, m.eq_solimp)
j, pos, pos_norm = jax.tree_util.tree_map(jp.concatenate, fn(data, id1, id2)) args = jax.tree_util.tree_map(lambda x: x[eq_id], args)
invweight = m.body_invweight0[id1, 0] + m.body_invweight0[id2, 0] # concatenate to drop row grouping
invweight = jp.repeat(invweight, 3) return jax.tree_util.tree_map(jp.concatenate, rows(*args))
solref = jp.tile(m.eq_solref[ids], (3, 1))
solimp = jp.tile(m.eq_solimp[ids], (3, 1))
frictionloss = jp.zeros_like(pos_norm)
return _Efc(j, pos, pos_norm, invweight, solref, solimp, frictionloss)
def _instantiate_equality_weld(m: Model, d: Data) -> Optional[_Efc]: def _efc_equality_weld(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for weld equality constraints.""" """Calculates constraint rows for weld equality constraints."""
ids = np.nonzero(m.eq_type == EqType.WELD)[0] eq_id = np.nonzero(m.eq_type == EqType.WELD)[0]
if (m.opt.disableflags & DisableBit.EQUALITY) or eq_id.size == 0:
if (m.opt.disableflags & DisableBit.EQUALITY) or ids.size == 0:
return None return None
id1, id2, data = m.eq_obj1id[ids], m.eq_obj2id[ids], m.eq_data[ids]
@jax.vmap @jax.vmap
def fn(data, id1, id2): def rows(obj1id, obj2id, data, solref, solimp):
anchor1, anchor2 = data[0:3], data[3:6] anchor1, anchor2 = data[0:3], data[3:6]
relpose, torquescale = data[6:10], data[10] relpose, torquescale = data[6:10], data[10]
# find global points # error is difference in global position and orientation
pos1 = d.xmat[id1] @ anchor2 + d.xpos[id1] pos1 = d.xmat[obj1id] @ anchor2 + d.xpos[obj1id]
pos2 = d.xmat[id2] @ anchor1 + d.xpos[id2] pos2 = d.xmat[obj2id] @ anchor1 + d.xpos[obj2id]
# compute position error
cpos = pos1 - pos2 cpos = pos1 - pos2
# compute Jacobian difference (opposite of contact: 0 - 1) # compute Jacobian difference (opposite of contact: 0 - 1)
jacp1, jacr1 = support.jac(m, d, pos1, id1) jacp1, jacr1 = support.jac(m, d, pos1, obj1id)
jacp2, jacr2 = support.jac(m, d, pos2, id2) jacp2, jacr2 = support.jac(m, d, pos2, obj2id)
jacdifp = jacp1 - jacp2 jacdifp = jacp1 - jacp2
jacdifr = (jacr1 - jacr2) * torquescale jacdifr = (jacr1 - jacr2) * torquescale
# compute orientation error: neg(q1) * q0 * relpose (axis components only) # compute orientation error: neg(q1) * q0 * relpose (axis components only)
quat = math.quat_mul(d.xquat[id1], relpose) quat = math.quat_mul(d.xquat[obj1id], relpose)
quat1 = math.quat_inv(d.xquat[id2]) quat1 = math.quat_inv(d.xquat[obj2id])
crot = math.quat_mul(quat1, quat)[1:] # copy axis components crot = math.quat_mul(quat1, quat)[1:] # copy axis components
pos = jp.concatenate((cpos, crot * torquescale))
# correct rotation Jacobian: 0.5 * neg(q1) * (jac0-jac1) * q0 * relpose # correct rotation Jacobian: 0.5 * neg(q1) * (jac0-jac1) * q0 * relpose
jac_fn = lambda j: math.quat_mul(math.quat_mul_axis(quat1, j), quat)[1:] jac_fn = lambda j: math.quat_mul(math.quat_mul_axis(quat1, j), quat)[1:]
jacdifr = 0.5 * jax.vmap(jac_fn)(jacdifr) jacdifr = 0.5 * jax.vmap(jac_fn)(jacdifr)
j = jp.concatenate((jacdifp.T, jacdifr.T)) j = jp.concatenate((jacdifp.T, jacdifr.T))
pos = jp.concatenate((cpos, crot * torquescale)) pos_imp = math.norm(pos)
invweight = m.body_invweight0[obj1id] + m.body_invweight0[obj2id]
invweight = jp.repeat(invweight, 3, axis=0)
return j, pos, jp.repeat(math.norm(pos), 6) return _row(j, pos, pos_imp, invweight, solref, solimp)
# concatenate to drop weld grouping dimension args = (m.eq_obj1id, m.eq_obj2id, m.eq_data, m.eq_solref, m.eq_solimp)
j, pos, pos_norm = jax.tree_util.tree_map(jp.concatenate, fn(data, id1, id2)) args = jax.tree_util.tree_map(lambda x: x[eq_id], args)
invweight = m.body_invweight0[id1] + m.body_invweight0[id2] # concatenate to drop row grouping
invweight = jp.repeat(invweight, 3) return jax.tree_util.tree_map(jp.concatenate, rows(*args))
solref = jp.tile(m.eq_solref[ids], (6, 1))
solimp = jp.tile(m.eq_solimp[ids], (6, 1))
frictionloss = jp.zeros_like(pos_norm)
return _Efc(j, pos, pos_norm, invweight, solref, solimp, frictionloss)
def _instantiate_equality_joint(m: Model, d: Data) -> Optional[_Efc]: def _efc_equality_joint(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for joint equality constraints.""" """Calculates constraint rows for joint equality constraints."""
ids = np.nonzero(m.eq_type == EqType.JOINT)[0] eq_id = np.nonzero(m.eq_type == EqType.JOINT)[0]
if (m.opt.disableflags & DisableBit.EQUALITY) or ids.size == 0: if (m.opt.disableflags & DisableBit.EQUALITY) or eq_id.size == 0:
return None return None
id1, id2, data = m.eq_obj1id[ids], m.eq_obj2id[ids], m.eq_data[ids]
dofadr1, dofadr2 = m.jnt_dofadr[id1], m.jnt_dofadr[id2]
qposadr1, qposadr2 = m.jnt_qposadr[id1], m.jnt_qposadr[id2]
@jax.vmap @jax.vmap
def fn(data, id2, dofadr1, dofadr2, qposadr1, qposadr2): def rows(obj2id, data, solref, solimp, dofadr1, dofadr2, qposadr1, qposadr2):
pos1, pos2 = d.qpos[qposadr1], d.qpos[qposadr2] pos1, pos2 = d.qpos[qposadr1], d.qpos[qposadr2]
ref1, ref2 = m.qpos0[qposadr1], m.qpos0[qposadr2] ref1, ref2 = m.qpos0[qposadr1], m.qpos0[qposadr2]
pos2, ref2 = pos2 * (id2 > -1), ref2 * (id2 > -1) dif = (pos2 - ref2) * (obj2id > -1)
dif = pos2 - ref2
dif_power = jp.power(dif, jp.arange(0, 5)) dif_power = jp.power(dif, jp.arange(0, 5))
deriv = jp.dot(data[1:5], dif_power[:4] * jp.arange(1, 5))
j = jp.zeros((m.nv)).at[dofadr1].set(1.0).at[dofadr2].set(-deriv)
pos = pos1 - ref1 - jp.dot(data[:5], dif_power) pos = pos1 - ref1 - jp.dot(data[:5], dif_power)
return j, pos deriv = jp.dot(data[1:5], dif_power[:4] * jp.arange(1, 5)) * (obj2id > -1)
j, pos = fn(data, id2, dofadr1, dofadr2, qposadr1, qposadr2) j = jp.zeros((m.nv)).at[dofadr2].set(-deriv).at[dofadr1].set(1.0)
invweight = m.dof_invweight0[dofadr1] + m.dof_invweight0[dofadr2] * (id2 > -1) invweight = m.dof_invweight0[dofadr1]
solref, solimp = m.eq_solref[ids], m.eq_solimp[ids] invweight += m.dof_invweight0[dofadr2] * (obj2id > -1)
frictionloss = jp.zeros_like(pos)
return _Efc(j, pos, pos, invweight, solref, solimp, frictionloss) return _row(j, pos, pos, invweight, solref, solimp)
args = (m.eq_obj1id, m.eq_obj2id, m.eq_data, m.eq_solref, m.eq_solimp)
args = jax.tree_util.tree_map(lambda x: x[eq_id], args)
dofadr1, dofadr2 = m.jnt_dofadr[args[0]], m.jnt_dofadr[args[1]]
qposadr1, qposadr2 = m.jnt_qposadr[args[0]], m.jnt_qposadr[args[1]]
args = args[1:] + (dofadr1, dofadr2, qposadr1, qposadr2)
return rows(*args)
def _instantiate_friction(m: Model, d: Data) -> Optional[_Efc]: def _efc_friction(m: Model, d: Data) -> Optional[_Efc]:
# TODO(robotics-team): implement _instantiate_friction # TODO(robotics-team): implement _instantiate_friction
del m, d del m, d
return None return None
def _instantiate_limit_ball(m: Model, d: Data) -> Optional[_Efc]: def _efc_limit_ball(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for ball joint limits.""" """Calculates constraint rows for ball joint limits."""
ids = np.nonzero((m.jnt_type == JointType.BALL) & m.jnt_limited)[0] jnt_id = np.nonzero((m.jnt_type == JointType.BALL) & m.jnt_limited)[0]
if (m.opt.disableflags & DisableBit.LIMIT) or ids.size == 0: if (m.opt.disableflags & DisableBit.LIMIT) or jnt_id.size == 0:
return None return None
jnt_range = m.jnt_range[ids]
jnt_margin = m.jnt_margin[ids]
qposadr = np.array([np.arange(q, q + 4) for q in m.jnt_qposadr[ids]])
dofadr = np.array([np.arange(d, d + 3) for d in m.jnt_dofadr[ids]])
@jax.vmap @jax.vmap
def fn(jnt_range, jnt_margin, qposadr, dofadr): def rows(qposadr, dofadr, jnt_range, jnt_margin, solref, solimp):
axis, angle = math.quat_to_axis_angle(d.qpos[qposadr]) axis, angle = math.quat_to_axis_angle(d.qpos[jp.arange(4) + qposadr])
j = jp.zeros(m.nv).at[dofadr].set(-axis)
pos = jp.amax(jnt_range) - angle - jnt_margin pos = jp.amax(jnt_range) - angle - jnt_margin
active = pos < 0 active = pos < 0
return j * active, pos * active j = jp.zeros(m.nv).at[jp.arange(3) + dofadr].set(-axis)
invweight = m.dof_invweight0[dofadr]
j, pos = fn(jnt_range, jnt_margin, qposadr, dofadr) return _row(j * active, pos * active, pos, invweight, solref, solimp)
invweight = m.dof_invweight0[m.jnt_dofadr[ids]]
solref, solimp = m.jnt_solref[ids], m.jnt_solimp[ids]
frictionloss = jp.zeros_like(pos)
return _Efc(j, pos, pos, invweight, solref, solimp, frictionloss) args = (m.jnt_qposadr, m.jnt_dofadr, m.jnt_range, m.jnt_margin, m.jnt_solref)
args += (m.jnt_solimp,)
args = jax.tree_util.tree_map(lambda x: x[jnt_id], args)
return rows(*args)
def _instantiate_limit_slide_hinge(m: Model, d: Data) -> Optional[_Efc]: def _efc_limit_slide_hinge(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for slide and hinge joint limits.""" """Calculates constraint rows for slide and hinge joint limits."""
slide_hinge = np.isin(m.jnt_type, (JointType.SLIDE, JointType.HINGE)) slide_hinge = np.isin(m.jnt_type, (JointType.SLIDE, JointType.HINGE))
ids = np.nonzero(slide_hinge & m.jnt_limited)[0] jnt_id = np.nonzero(slide_hinge & m.jnt_limited)[0]
if (m.opt.disableflags & DisableBit.LIMIT) or ids.size == 0: if (m.opt.disableflags & DisableBit.LIMIT) or jnt_id.size == 0:
return None return None
jnt_range = m.jnt_range[ids]
jnt_margin = m.jnt_margin[ids]
qposadr = m.jnt_qposadr[ids]
dofadr = m.jnt_dofadr[ids]
@jax.vmap @jax.vmap
def fn(jnt_range, jnt_margin, qposadr, dofadr): def rows(qposadr, dofadr, jnt_range, jnt_margin, solref, solimp):
dist_min = d.qpos[qposadr] - jnt_range[0] qpos = d.qpos[qposadr]
dist_max = jnt_range[1] - d.qpos[qposadr] dist_min, dist_max = qpos - jnt_range[0], jnt_range[1] - qpos
j = jp.zeros(m.nv).at[dofadr].set((dist_min < dist_max) * 2 - 1)
pos = jp.minimum(dist_min, dist_max) - jnt_margin pos = jp.minimum(dist_min, dist_max) - jnt_margin
active = pos < 0 active = pos < 0
return j * active, pos * active j = jp.zeros(m.nv).at[dofadr].set((dist_min < dist_max) * 2 - 1)
invweight = m.dof_invweight0[dofadr]
j, pos = fn(jnt_range, jnt_margin, qposadr, dofadr) return _row(j * active, pos * active, pos, invweight, solref, solimp)
invweight = m.dof_invweight0[dofadr]
solref, solimp = m.jnt_solref[ids], m.jnt_solimp[ids]
frictionloss = jp.zeros_like(pos)
return _Efc(j, pos, pos, invweight, solref, solimp, frictionloss) args = (m.jnt_qposadr, m.jnt_dofadr, m.jnt_range, m.jnt_margin, m.jnt_solref)
args += (m.jnt_solimp,)
args = jax.tree_util.tree_map(lambda x: x[jnt_id], args)
return rows(*args)
def _instantiate_contact(m: Model, d: Data) -> Optional[_Efc]: def _efc_contact_frictionless(m: Model, d: Data) -> Optional[_Efc]:
"""Calculates constraint rows for contacts.""" """Calculates constraint rows for frictionless contacts."""
if d.ncon == 0: con_id = np.nonzero(d.contact.dim == 1)[0]
if con_id.size == 0:
return None return None
def contact_efc(c: Contact, condim: int): @jax.vmap
def rows(c: Contact):
pos = c.dist - c.includemargin
active = pos < 0
body1, body2 = jp.array(m.geom_bodyid)[c.geom]
jac1p, _ = support.jac(m, d, c.pos, body1)
jac2p, _ = support.jac(m, d, c.pos, body2)
j = (c.frame @ (jac2p - jac1p).T)[0]
invweight = m.body_invweight0[body1, 0] + m.body_invweight0[body2, 0]
@jax.vmap return _row(j * active, pos * active, pos, invweight, c.solref, c.solimp)
def fn(c: Contact):
dist = c.dist - c.includemargin
active = dist < 0
body1, body2 = jp.array(m.geom_bodyid)[c.geom]
jac1p, jac1r = support.jac(m, d, c.pos, body1)
jac2p, jac2r = support.jac(m, d, c.pos, body2)
diff = c.frame @ (jac2p - jac1p).T
if condim > 3: # only calculate rotational diff if needed
diff = jp.concatenate((diff, c.frame @ (jac2r - jac1r).T), axis=0)
tran = m.body_invweight0[body1, 0] + m.body_invweight0[body2, 0]
if condim == 1: contact = jax.tree_util.tree_map(lambda x: x[con_id], d.contact)
return diff[0] * active, tran, dist * active, c.solref, c.solimp
# a pair of opposing pyramid edges per friction dimension return rows(contact)
# repeat friction directions with positive and negative sign
fri = jp.repeat(c.friction[: condim - 1], 2, axis=0).at[1::2].mul(-1)
# repeat condims of jacdiff to match +/- friction directions
j = diff[0] + jp.repeat(diff[1:condim], 2, axis=0) * fri[:, None]
# pyramidal has common invweight across all edges
diag_approx = tran + fri[0] * fri[0] * tran
inv_w = diag_approx * 2 * fri[0] * fri[0] / m.opt.impratio
repeat_fn = lambda x: jp.repeat(x[None], (condim - 1) * 2, axis=0)
inv_w, pos, solref, solimp = jax.tree_util.tree_map(
repeat_fn, (inv_w, dist, c.solref, c.solimp)
)
return j * active, inv_w, pos * active, solref, solimp
return fn(c)
# group efc calculations by condim def _efc_contact_pyramidal(m: Model, d: Data, condim: int) -> Optional[_Efc]:
dims, begs = np.unique(d.contact.dim, return_index=True) """Calculates constraint rows for frictional pyramidal contacts."""
efcs = []
for i in range(len(dims)):
dim, beg = dims[i], begs[i]
end = begs[i + 1] if i < len(dims) - 1 else None
c = jax.tree_util.tree_map(lambda x, b=beg, e=end: x[b:e], d.contact)
efc = contact_efc(c, dim)
if dim > 1:
# remove efc grouping dimension
efc = jax.tree_util.tree_map(jp.concatenate, efc)
efcs.append(efc)
efc = jax.tree_util.tree_map(lambda *x: jp.concatenate(x), *efcs) con_id = np.nonzero(d.contact.dim == condim)[0]
j, invweight, pos, solref, solimp = efc
frictionloss = jp.zeros_like(pos)
return _Efc(j, pos, pos, invweight, solref, solimp, frictionloss) if con_id.size == 0:
return None
@jax.vmap
def rows(c: Contact):
pos = c.dist - c.includemargin
active = pos < 0
body1, body2 = jp.array(m.geom_bodyid)[c.geom]
jac1p, jac1r = support.jac(m, d, c.pos, body1)
jac2p, jac2r = support.jac(m, d, c.pos, body2)
diff = c.frame @ (jac2p - jac1p).T
if condim > 3:
diff = jp.concatenate((diff, (c.frame @ (jac2r - jac1r).T)), axis=0)
# a pair of opposing pyramid edges per friction dimension
# repeat friction directions with positive and negative sign
fri = jp.repeat(c.friction[: condim - 1], 2, axis=0).at[1::2].mul(-1)
# repeat condims of jacdiff to match +/- friction directions
j = diff[0] + jp.repeat(diff[1:condim], 2, axis=0) * fri[:, None]
# pyramidal has common invweight across all edges
invweight = m.body_invweight0[body1, 0] + m.body_invweight0[body2, 0]
invweight = invweight + fri[0] * fri[0] * invweight
invweight = invweight * 2 * fri[0] * fri[0] / m.opt.impratio
return _row(j * active, pos * active, pos, invweight, c.solref, c.solimp)
contact = jax.tree_util.tree_map(lambda x: x[con_id], d.contact)
# concatenate to drop row grouping
return jax.tree_util.tree_map(jp.concatenate, rows(contact))
def _efc_contact_elliptic(m: Model, d: Data, condim: int) -> Optional[_Efc]:
"""Calculates constraint rows for frictional elliptic contacts."""
con_id = np.nonzero(d.contact.dim == condim)[0]
if con_id.size == 0:
return None
@jax.vmap
def rows(c: Contact):
pos = c.dist - c.includemargin
active = pos < 0
obj1id, obj2id = jp.array(m.geom_bodyid)[c.geom]
jac1p, jac1r = support.jac(m, d, c.pos, obj1id)
jac2p, jac2r = support.jac(m, d, c.pos, obj2id)
j = c.frame @ (jac2p - jac1p).T
if condim > 3:
j = jp.concatenate((j, (c.frame @ (jac2r - jac1r).T)[: condim - 3]))
invweight = m.body_invweight0[obj1id, 0] + m.body_invweight0[obj2id, 0]
# normal row comes from solref, remaining rows from solreffriction
solreffriction = c.solreffriction + c.solref * ~c.solreffriction.any()
solreffriction = jp.tile(solreffriction, (condim - 1, 1))
solref = jp.concatenate((c.solref[None], solreffriction))
fri = jp.square(c.friction[0]) / jp.square(c.friction[1 : condim - 1])
invweight = jp.array([invweight, invweight / m.opt.impratio])
invweight = jp.concatenate((invweight, invweight[1] * fri))
pos_aref = jp.zeros(condim).at[0].set(pos)
return _row(j * active, pos_aref * active, pos, invweight, solref, c.solimp)
contact = jax.tree_util.tree_map(lambda x: x[con_id], d.contact)
# concatenate to drop row grouping
return jax.tree_util.tree_map(jp.concatenate, rows(contact))
def counts(efc_type: np.ndarray) -> Tuple[int, int, int, int]: def counts(efc_type: np.ndarray) -> Tuple[int, int, int, int]:
@@ -344,7 +367,8 @@ def counts(efc_type: np.ndarray) -> Tuple[int, int, int, int]:
nl = (efc_type == ConstraintType.LIMIT_JOINT).sum() nl = (efc_type == ConstraintType.LIMIT_JOINT).sum()
nc_f = (efc_type == ConstraintType.CONTACT_FRICTIONLESS).sum() nc_f = (efc_type == ConstraintType.CONTACT_FRICTIONLESS).sum()
nc_p = (efc_type == ConstraintType.CONTACT_PYRAMIDAL).sum() nc_p = (efc_type == ConstraintType.CONTACT_PYRAMIDAL).sum()
nc = nc_f + nc_p nc_e = (efc_type == ConstraintType.CONTACT_ELLIPTIC).sum()
nc = nc_f + nc_p + nc_e
return ne, nf, nl, nc return ne, nf, nl, nc
@@ -363,25 +387,48 @@ def make_efc_type(
num_rows = (m.eq_type == EqType.CONNECT).sum() * 3 num_rows = (m.eq_type == EqType.CONNECT).sum() * 3
num_rows += (m.eq_type == EqType.WELD).sum() * 6 num_rows += (m.eq_type == EqType.WELD).sum() * 6
num_rows += (m.eq_type == EqType.JOINT).sum() num_rows += (m.eq_type == EqType.JOINT).sum()
efc_types.extend([ConstraintType.EQUALITY] * num_rows) efc_types += [ConstraintType.EQUALITY] * num_rows
if not m.opt.disableflags & DisableBit.LIMIT: if not m.opt.disableflags & DisableBit.LIMIT:
efc_types.extend([ConstraintType.LIMIT_JOINT] * m.jnt_limited.sum()) efc_types += [ConstraintType.LIMIT_JOINT] * m.jnt_limited.sum()
if not m.opt.disableflags & DisableBit.CONTACT: if not m.opt.disableflags & DisableBit.CONTACT:
num_rows = sum(_CONDIM_EFC_COUNT[d] for d in dim) for condim in (1, 3, 4, 6):
efc_types.extend([ConstraintType.CONTACT_PYRAMIDAL] * num_rows) n = (dim == condim).sum()
if condim == 1:
efc_types += [ConstraintType.CONTACT_FRICTIONLESS] * n
elif m.opt.cone == ConeType.PYRAMIDAL:
efc_types += [ConstraintType.CONTACT_PYRAMIDAL] * (condim - 1) * 2 * n
elif m.opt.cone == ConeType.ELLIPTIC:
efc_types += [ConstraintType.CONTACT_ELLIPTIC] * condim * n
else:
raise ValueError(f'Unknown cone: {m.opt.cone}')
return np.array(efc_types) return np.array(efc_types)
def make_efc_address(efc_type: np.ndarray, dim: np.ndarray) -> np.ndarray: def make_efc_address(
m: Union[Model, mujoco.MjModel], dim: np.ndarray, efc_type: np.ndarray
) -> np.ndarray:
"""Returns efc_address that maps contacts to constraint row address.""" """Returns efc_address that maps contacts to constraint row address."""
nc = (efc_type == ConstraintType.CONTACT_PYRAMIDAL).sum() offsets = np.array([0], dtype=int)
nc_start = efc_type.size - nc for condim in (1, 3, 4, 6):
offsets = np.cumsum([0] + [_CONDIM_EFC_COUNT[d] for d in dim])[:-1] n = (dim == condim).sum()
if n == 0:
continue
if condim == 1:
offsets = np.concatenate((offsets, [1] * n))
elif m.opt.cone == ConeType.PYRAMIDAL:
offsets = np.concatenate((offsets, [(condim - 1) * 2] * n))
elif m.opt.cone == ConeType.ELLIPTIC:
offsets = np.concatenate((offsets, [condim] * n))
else:
raise ValueError(f'Unknown cone: {m.opt.cone}')
return nc_start + offsets _, _, _, nc = counts(efc_type)
address = efc_type.size - nc + np.cumsum(offsets)[:-1]
return address
def make_constraint(m: Model, d: Data) -> Data: def make_constraint(m: Model, d: Data) -> Data:
@@ -390,15 +437,21 @@ def make_constraint(m: Model, d: Data) -> Data:
if m.opt.disableflags & DisableBit.CONSTRAINT: if m.opt.disableflags & DisableBit.CONSTRAINT:
efcs = () efcs = ()
else: else:
efcs = tuple(efc for efc in ( efcs = (
_instantiate_equality_connect(m, d), _efc_equality_connect(m, d),
_instantiate_equality_weld(m, d), _efc_equality_weld(m, d),
_instantiate_equality_joint(m, d), _efc_equality_joint(m, d),
_instantiate_friction(m, d), _efc_friction(m, d),
_instantiate_limit_ball(m, d), _efc_limit_ball(m, d),
_instantiate_limit_slide_hinge(m, d), _efc_limit_slide_hinge(m, d),
_instantiate_contact(m, d), _efc_contact_frictionless(m, d),
) if efc is not None) )
if m.opt.cone == ConeType.ELLIPTIC:
con_fn = _efc_contact_elliptic
else:
con_fn = _efc_contact_pyramidal
efcs += tuple(con_fn(m, d, dim) for dim in (3, 4, 6))
efcs = tuple(efc for efc in efcs if efc is not None)
if not efcs: if not efcs:
z = jp.empty(0) z = jp.empty(0)
@@ -410,13 +463,13 @@ def make_constraint(m: Model, d: Data) -> Data:
@jax.vmap @jax.vmap
def fn(efc): def fn(efc):
k, b, imp = _kbi(m, efc.solref, efc.solimp, efc.pos_norm) k, b, imp = _kbi(m, efc.solref, efc.solimp, efc.pos_imp)
r = jp.maximum(efc.invweight * (1 - imp) / imp, mujoco.mjMINVAL) r = jp.maximum(efc.invweight * (1 - imp) / imp, mujoco.mjMINVAL)
aref = -b * (efc.J @ d.qvel) - k * imp * efc.pos aref = -b * (efc.J @ d.qvel) - k * imp * efc.pos_aref
return aref, r return aref, r
aref, r = fn(efc) aref, r = fn(efc)
d = d.replace(efc_J=efc.J, efc_D=1 / r, efc_aref=aref) d = d.replace(efc_J=efc.J, efc_D=1 / r, efc_aref=aref)
d = d.replace(efc_frictionloss=efc.frictionloss) d = d.replace(efc_frictionloss=jp.zeros_like(r))
return d return d
+21 -29
View File
@@ -15,6 +15,7 @@
"""Tests for constraint functions.""" """Tests for constraint functions."""
from absl.testing import absltest from absl.testing import absltest
from absl.testing import parameterized
from jax import numpy as jp from jax import numpy as jp
import mujoco import mujoco
from mujoco import mjx from mujoco import mjx
@@ -38,41 +39,32 @@ def _assert_attr_eq(a, b, attr):
_assert_eq(getattr(a, attr), getattr(b, attr), attr) _assert_eq(getattr(a, attr), getattr(b, attr), attr)
class ConstraintTest(absltest.TestCase): class ConstraintTest(parameterized.TestCase):
def test_constraints(self): @parameterized.parameters(
mujoco.mjtCone.mjCONE_PYRAMIDAL, mujoco.mjtCone.mjCONE_ELLIPTIC
)
def test_constraints(self, cone):
"""Test constraints.""" """Test constraints."""
m = test_util.load_test_file('constraints.xml') m = test_util.load_test_file('constraints.xml')
m.opt.cone = cone
d = mujoco.MjData(m) d = mujoco.MjData(m)
mujoco.mj_step(m, d, 100) # at 100 steps mix of active/inactive constraints
mujoco.mj_forward(m, d)
mx = mjx.put_model(m)
dx = mjx.put_data(m, d)
dx = mjx.make_constraint(mx, dx)
d_efc_j = d.efc_J.reshape((-1, m.nv))
# ne, nf, nl order matches # sample a mix of active/inactive constraints at different timesteps
efl = d.ne + d.nf + d.nl for key in range(3):
_assert_eq(d_efc_j[:efl], dx.efc_J[:efl], 'efc_J') mujoco.mj_resetDataKeyframe(m, d, key)
_assert_eq(d.efc_D[:efl], dx.efc_D[:efl], 'efc_D') mujoco.mj_forward(m, d)
_assert_eq(d.efc_aref[:efl], dx.efc_aref[:efl], 'efc_aref') mx = mjx.put_model(m)
_assert_eq(dx.efc_frictionloss, 0, 'efc_frictionloss') dx = mjx.put_data(m, d)
dx = mjx.make_constraint(mx, dx)
# contact order might not match, so check efcs contact by contact order = test_util.efc_order(m, d, dx)
for i in range(d.ncon): d_efc_j = d.efc_J.reshape((-1, m.nv))
geom_match = (dx.contact.geom == d.contact.geom[i]).all(axis=-1) _assert_eq(d_efc_j, dx.efc_J[order][:d.nefc], 'efc_J')
geom_match &= (dx.contact.pos == d.contact.pos[i]).all(axis=-1) _assert_eq(0, dx.efc_J[order][d.nefc:], 'efc_J')
self.assertTrue(geom_match.any(), f'contact {i} not found in MJX contact') _assert_eq(d.efc_aref, dx.efc_aref[order][:d.nefc], 'efc_aref')
j = np.nonzero(geom_match)[0][0] _assert_eq(0, dx.efc_aref[order][d.nefc:], 'efc_aref')
self.assertEqual(d.contact.dim[i], dx.contact.dim[j]) _assert_eq(d.efc_D, dx.efc_D[order][:d.nefc], 'efc_D')
nc = max(1, (d.contact.dim[i] - 1) * 2)
d_beg, dx_beg = d.contact.efc_address[i], dx.contact.efc_address[j]
d_end, dx_end = d_beg + nc, dx_beg + nc
_assert_eq(d_efc_j[d_beg:d_end], dx.efc_J[dx_beg:dx_end], 'efc_J')
_assert_eq(d.efc_D[d_beg:d_end], dx.efc_D[dx_beg:dx_end], 'efc_D')
d_efc_aref = d.efc_aref[d_beg:d_end]
dx_efc_aref = dx.efc_aref[dx_beg:dx_end]
_assert_eq(d_efc_aref, dx_efc_aref, 'efc_aref')
def test_disable_refsafe(self): def test_disable_refsafe(self):
m = test_util.load_test_file('constraints.xml') m = test_util.load_test_file('constraints.xml')
+25 -11
View File
@@ -15,7 +15,7 @@
"""Functions to initialize, load, or save data.""" """Functions to initialize, load, or save data."""
import copy import copy
from typing import List, Union from typing import List, Tuple, Union
import jax import jax
from jax import numpy as jp from jax import numpy as jp
@@ -133,7 +133,7 @@ def make_data(m: Union[types.Model, mujoco.MjModel]) -> types.Data:
"""Allocate and initialize Data.""" """Allocate and initialize Data."""
dim = collision_driver.make_condim(m) dim = collision_driver.make_condim(m)
efc_type = constraint.make_efc_type(m, dim) efc_type = constraint.make_efc_type(m, dim)
efc_address = constraint.make_efc_address(efc_type, dim) efc_address = constraint.make_efc_address(m, dim, efc_type)
ne, nf, nl, nc = constraint.counts(efc_type) ne, nf, nl, nc = constraint.counts(efc_type)
ncon, nefc = dim.size, ne + nf + nl + nc ncon, nefc = dim.size, ne + nf + nl + nc
@@ -330,7 +330,7 @@ def _make_contact(
c: mujoco._structs._MjContactList, c: mujoco._structs._MjContactList,
dim: np.ndarray, dim: np.ndarray,
efc_address: np.ndarray, efc_address: np.ndarray,
) -> types.Contact: ) -> Tuple[types.Contact, np.ndarray]:
"""Converts mujoco.structs._MjContactList into mjx.Contact.""" """Converts mujoco.structs._MjContactList into mjx.Contact."""
fields = {f.name: getattr(c, f.name) for f in types.Contact.fields()} fields = {f.name: getattr(c, f.name) for f in types.Contact.fields()}
fields['frame'] = fields['frame'].reshape((-1, 3, 3)) fields['frame'] = fields['frame'].reshape((-1, 3, 3))
@@ -351,21 +351,21 @@ def _make_contact(
zero = jax.tree_util.tree_map( zero = jax.tree_util.tree_map(
lambda x: np.zeros((1,) + x.shape[1:], dtype=x.dtype), fields lambda x: np.zeros((1,) + x.shape[1:], dtype=x.dtype), fields
) )
zero['dist'][:] = np.finfo(float).max zero['dist'][:] = 1e10
fields = jax.tree_util.tree_map(lambda *x: np.concatenate(x), fields, zero) fields = jax.tree_util.tree_map(lambda *x: np.concatenate(x), fields, zero)
fields = jax.tree_util.tree_map(lambda x: x[contact_map], fields) fields = jax.tree_util.tree_map(lambda x: x[contact_map], fields)
fields['dim'] = dim fields['dim'] = dim
fields['efc_address'] = efc_address fields['efc_address'] = efc_address
return types.Contact(**fields) return types.Contact(**fields), contact_map
def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data: def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data:
"""Puts mujoco.MjData onto a device, resulting in mjx.Data.""" """Puts mujoco.MjData onto a device, resulting in mjx.Data."""
dim = collision_driver.make_condim(m) dim = collision_driver.make_condim(m)
efc_type = constraint.make_efc_type(m, dim) efc_type = constraint.make_efc_type(m, dim)
efc_address = constraint.make_efc_address(efc_type, dim) efc_address = constraint.make_efc_address(m, dim, efc_type)
ne, nf, nl, nc = constraint.counts(efc_type) ne, nf, nl, nc = constraint.counts(efc_type)
ncon, nefc = dim.size, ne + nf + nl + nc ncon, nefc = dim.size, ne + nf + nl + nc
@@ -388,6 +388,8 @@ def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data:
# MJX does not support islanding, so only transfer the first solver_niter # MJX does not support islanding, so only transfer the first solver_niter
fields['solver_niter'] = fields['solver_niter'][0] fields['solver_niter'] = fields['solver_niter'][0]
contact, contact_map = _make_contact(d.contact, dim, efc_address)
# pad efc fields: MuJoCo efc arrays are sparse for inactive constraints. # pad efc fields: MuJoCo efc arrays are sparse for inactive constraints.
# efc_J is also optionally column-sparse (typically for large nv). MJX is # efc_J is also optionally column-sparse (typically for large nv). MJX is
# neither: it contains zeros for inactive constraints, and efc_J is always # neither: it contains zeros for inactive constraints, and efc_J is always
@@ -403,13 +405,25 @@ def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data:
else: else:
fields['efc_J'] = fields['efc_J'].reshape((-1 if m.nv else 0, m.nv)) fields['efc_J'] = fields['efc_J'].reshape((-1 if m.nv else 0, m.nv))
# move efc rows to their correct offsets
for fname in ('efc_J', 'efc_frictionloss', 'efc_D', 'efc_aref', 'efc_force'): for fname in ('efc_J', 'efc_frictionloss', 'efc_D', 'efc_aref', 'efc_force'):
value = np.zeros((nefc, m.nv)) if fname == 'efc_J' else np.zeros(nefc) value = np.zeros((nefc, m.nv)) if fname == 'efc_J' else np.zeros(nefc)
for i in range(4): for i in range(3):
value_beg = sum([ne, nf, nl][:i]) value_beg = sum([ne, nf][:i])
d_beg = sum([d.ne, d.nf, d.nl][:i]) d_beg = sum([d.ne, d.nf][:i])
size = [d.ne, d.nf, d.nl, d.nefc - d.nl - d.nf - d.ne][i] size = [d.ne, d.nf, d.nl][i]
value[value_beg : value_beg + size] = fields[fname][d_beg : d_beg + size] value[value_beg : value_beg + size] = fields[fname][d_beg : d_beg + size]
# for nc, we may reorder contacts so they match MJX order: group by dim
for id_to, id_from in enumerate(contact_map):
if id_from == -1:
continue
num_rows = dim[id_to]
if num_rows > 1 and m.opt.cone == mujoco.mjtCone.mjCONE_PYRAMIDAL:
num_rows = (num_rows - 1) * 2
efc_i, efc_o = d.contact.efc_address[id_from], efc_address[id_to]
value[efc_o:efc_o + num_rows] = fields[fname][efc_i:efc_i + num_rows]
fields[fname] = value fields[fname] = value
# convert qM and qLD if jacobian is dense # convert qM and qLD if jacobian is dense
@@ -424,7 +438,7 @@ def put_data(m: mujoco.MjModel, d: mujoco.MjData, device=None) -> types.Data:
fields['qLD'] = np.zeros((m.nv, m.nv)) fields['qLD'] = np.zeros((m.nv, m.nv))
fields['qLDiagInv'] = np.zeros(0) fields['qLDiagInv'] = np.zeros(0)
fields['contact'] = _make_contact(d.contact, dim, efc_address) fields['contact'] = contact
fields.update(ne=ne, nf=nf, nl=nl, nefc=nefc, ncon=ncon, efc_type=efc_type) fields.update(ne=ne, nf=nf, nl=nl, nefc=nefc, ncon=ncon, efc_type=efc_type)
# copy because device_put is async: # copy because device_put is async:
+1 -9
View File
@@ -140,14 +140,6 @@ class ModelIOTest(parameterized.TestCase):
) )
) )
def test_cone_not_implemented(self):
with self.assertRaises(NotImplementedError):
mjx.put_model(
mujoco.MjModel.from_xml_string(
'<mujoco><option cone="elliptic"/><worldbody/></mujoco>'
)
)
def test_pgs_not_implemented(self): def test_pgs_not_implemented(self):
with self.assertRaises(NotImplementedError): with self.assertRaises(NotImplementedError):
mjx.put_model( mjx.put_model(
@@ -299,7 +291,7 @@ class DataIOTest(parameterized.TestCase):
self.assertEqual(dx.contact.dist.shape, (4,)) self.assertEqual(dx.contact.dist.shape, (4,))
self.assertEqual(d.ncon, 1) # however only 1 contact in this step self.assertEqual(d.ncon, 1) # however only 1 contact in this step
np.testing.assert_allclose(dx.contact.dist[0], d.contact.dist[0]) np.testing.assert_allclose(dx.contact.dist[0], d.contact.dist[0])
self.assertTrue(np.isinf(dx.contact.dist[1:]).all()) self.assertTrue((dx.contact.dist[1:] > 0).all())
self.assertEqual(dx.contact.frame.shape, (4, 3, 3)) self.assertEqual(dx.contact.frame.shape, (4, 3, 3))
np.testing.assert_allclose( np.testing.assert_allclose(
dx.contact.frame[0].reshape(9), d.contact.frame[0] dx.contact.frame[0].reshape(9), d.contact.frame[0]
+214 -40
View File
@@ -22,6 +22,7 @@ from mujoco.mjx._src import smooth
from mujoco.mjx._src import support from mujoco.mjx._src import support
# pylint: disable=g-importing-member # pylint: disable=g-importing-member
from mujoco.mjx._src.dataclasses import PyTreeNode from mujoco.mjx._src.dataclasses import PyTreeNode
from mujoco.mjx._src.types import ConeType
from mujoco.mjx._src.types import Data from mujoco.mjx._src.types import Data
from mujoco.mjx._src.types import DisableBit from mujoco.mjx._src.types import DisableBit
from mujoco.mjx._src.types import Model from mujoco.mjx._src.types import Model
@@ -45,8 +46,12 @@ class _Context(PyTreeNode):
cost: constraint + Gauss cost cost: constraint + Gauss cost
prev_cost: cost from previous iter prev_cost: cost from previous iter
solver_niter: number of solver iterations solver_niter: number of solver iterations
active: active (quadratic) constraints (nefc,)
fri: friction of regularized cone (num(con.dim > 1), 6)
dm: regularized constraint mass (num(con.dim > 1))
u: friction cone (normal and tangents) (num(con.dim > 1), 6)
h: cone hessian (num(con.dim > 1), 6, 6)
""" """
qacc: jax.Array qacc: jax.Array
qfrc_constraint: jax.Array qfrc_constraint: jax.Array
Jaref: jax.Array # pylint: disable=invalid-name Jaref: jax.Array # pylint: disable=invalid-name
@@ -59,6 +64,11 @@ class _Context(PyTreeNode):
cost: jax.Array cost: jax.Array
prev_cost: jax.Array prev_cost: jax.Array
solver_niter: jax.Array solver_niter: jax.Array
active: jax.Array
fri: jax.Array
dm: jax.Array
u: jax.Array
h: jax.Array
@classmethod @classmethod
def create(cls, m: Model, d: Data, grad: bool = True) -> '_Context': def create(cls, m: Model, d: Data, grad: bool = True) -> '_Context':
@@ -66,6 +76,15 @@ class _Context(PyTreeNode):
# TODO(robotics-team): determine nv at which sparse mul is faster # TODO(robotics-team): determine nv at which sparse mul is faster
ma = support.mul_m(m, d, d.qacc) ma = support.mul_m(m, d, d.qacc)
nv_0 = jp.zeros(m.nv) nv_0 = jp.zeros(m.nv)
fri = 0.0
if m.opt.cone == ConeType.ELLIPTIC:
friction = d.contact.friction[d.contact.dim > 1]
dim = d.contact.dim[d.contact.dim > 1]
mu = friction[:, 0] / jp.sqrt(m.opt.impratio)
fri = jp.concatenate((mu[:, None], friction), axis=1)
for condim in (3, 4, 6):
fri = fri.at[dim == condim, condim:].set(0)
ctx = _Context( ctx = _Context(
qacc=d.qacc, qacc=d.qacc,
qfrc_constraint=d.qfrc_constraint, qfrc_constraint=d.qfrc_constraint,
@@ -79,8 +98,13 @@ class _Context(PyTreeNode):
cost=jp.inf, cost=jp.inf,
prev_cost=0.0, prev_cost=0.0,
solver_niter=0, solver_niter=0,
active=0.0,
fri=fri,
dm=0.0,
u=0.0,
h=0.0,
) )
ctx = _update_constraint(d, ctx) ctx = _update_constraint(m, d, ctx)
if grad: if grad:
ctx = _update_gradient(m, d, ctx) ctx = _update_gradient(m, d, ctx)
ctx = ctx.replace(search=-ctx.Mgrad) # start with preconditioned gradient ctx = ctx.replace(search=-ctx.Mgrad) # start with preconditioned gradient
@@ -106,24 +130,68 @@ class _LSPoint(PyTreeNode):
@classmethod @classmethod
def create( def create(
cls, cls,
m: Model,
d: Data, d: Data,
ctx: _Context, ctx: _Context,
alpha: jax.Array, alpha: jax.Array,
jv: jax.Array, jv: jax.Array,
quad: jax.Array, quad: jax.Array,
quad_gauss: jax.Array, quad_gauss: jax.Array,
uu: jax.Array,
v0: jax.Array,
uv: jax.Array,
vv: jax.Array,
) -> '_LSPoint': ) -> '_LSPoint':
"""Creates a linesearch point with first and second derivatives.""" """Creates a linesearch point with first and second derivatives."""
# roughly corresponds to CGEval in mujoco/src/engine/engine_solver.c # roughly corresponds to CGEval in mujoco/src/engine/engine_solver.c
# TODO(robotics-team): change this to support friction constraints # TODO(robotics-team): change this to support friction constraints
active = ((ctx.Jaref + alpha * jv) < 0).at[:d.ne + d.nf].set(True) cost, deriv_0, deriv_1 = 0.0, 0.0, 0.0
quad = jax.vmap(jp.multiply)(quad, active) # only active quad_total = quad_gauss
quad_total = quad_gauss + jp.sum(quad, axis=0)
if m.opt.cone == ConeType.ELLIPTIC:
mu, u0 = ctx.fri[:, 0], ctx.u[:, 0]
n = u0 + alpha * v0
tsqr = uu + alpha * (2 * uv + alpha * vv)
t = jp.sqrt(tsqr) # tangential force
bottom_zone = ((tsqr <= 0) & (n < 0)) | ((tsqr > 0) & ((mu * n + t) <= 0))
middle_zone = (tsqr > 0) & (n < (mu * t)) & ((mu * n + t) > 0)
# quadratic cost for equality, friction, limits, frictionless contacts
dim1 = d.contact.efc_address[d.contact.dim == 1]
nefl = d.ne + d.nf + d.nl
active = ((ctx.Jaref + alpha * jv) < 0).at[:d.ne + d.nf].set(True)
active = active.at[nefl:].set(False).at[dim1].set(active[dim1])
quad_efld = jax.vmap(jp.multiply)(quad, active)
quad_total += jp.sum(quad_efld, axis=0)
# elliptic bottom zone: quadratic cost
efc_elliptic = d.contact.efc_address[d.contact.dim > 1]
quad_c = jax.vmap(jp.multiply)(quad[efc_elliptic], bottom_zone)
quad_total += jp.sum(quad_c, axis=0)
# elliptic middle zone
t += (t == 0) * mujoco.mjMINVAL
tsqr += (tsqr == 0) * mujoco.mjMINVAL
n1 = v0
t1 = (uv + alpha * vv) / t
t2 = vv / t - (uv + alpha * vv) * t1 / tsqr
dm = ctx.dm * middle_zone
nmt = n - mu * t
cost = 0.5 * jp.sum(dm * jp.square(nmt))
deriv_0 = jp.sum(dm * nmt * (n1 - mu * t1))
deriv_1 = jp.sum(dm * (jp.square(n1 - mu * t1) - nmt * mu * t2))
elif m.opt.cone == ConeType.PYRAMIDAL:
active = ((ctx.Jaref + alpha * jv) < 0).at[:d.ne + d.nf].set(True)
quad = jax.vmap(jp.multiply)(quad, active) # only active
quad_total += jp.sum(quad, axis=0)
else:
raise NotImplementedError(f'unsupported cone type: {m.opt.cone}')
alpha_sq = alpha * alpha
cost += alpha_sq * quad_total[2] + alpha * quad_total[1] + quad_total[0]
deriv_0 += 2 * alpha * quad_total[2] + quad_total[1]
deriv_1 += 2 * quad_total[2] + (quad_total[2] == 0) * mujoco.mjMINVAL
cost = alpha * alpha * quad_total[2] + alpha * quad_total[1] + quad_total[0]
deriv_0 = 2 * alpha * quad_total[2] + quad_total[1]
deriv_1 = 2 * quad_total[2] + (quad_total[2] == 0) * mujoco.mjMINVAL
return _LSPoint(alpha=alpha, cost=cost, deriv_0=deriv_0, deriv_1=deriv_1) return _LSPoint(alpha=alpha, cost=cost, deriv_0=deriv_0, deriv_1=deriv_1)
@@ -159,34 +227,95 @@ def _while_loop_scan(cond_fun, body_fun, init_val, max_iter):
return jax.lax.scan(_fun, init, None, length=max_iter)[0][0] return jax.lax.scan(_fun, init, None, length=max_iter)[0][0]
def _update_constraint(d: Data, ctx: _Context) -> _Context: def _update_constraint(m: Model, d: Data, ctx: _Context) -> _Context:
"""Updates constraint force and resulting cost given latst solver iteration. """Updates constraint force and resulting cost given latst solver iteration.
Corresponds to CGupdateConstraint in mujoco/src/engine/engine_solver.c Corresponds to CGupdateConstraint in mujoco/src/engine/engine_solver.c
Args: Args:
m: model defining constraints
d: data which contains latest qacc and smooth terms d: data which contains latest qacc and smooth terms
ctx: current solver context ctx: current solver context
Returns: Returns:
context with new constraint force and costs context with new constraint force and costs
""" """
# TODO(robotics-team): add friction constraints if m.opt.cone == ConeType.PYRAMIDAL:
# ne/nf constraints are always active, rest are non-negative constraints
active = (ctx.Jaref < 0).at[:d.ne + d.nf].set(True)
efc_force = d.efc_D * -ctx.Jaref * active
cost = 0.5 * jp.sum(d.efc_D * ctx.Jaref * ctx.Jaref * active)
dm, u, h = 0.0, 0.0, 0.0
elif m.opt.cone == ConeType.ELLIPTIC:
friction = d.contact.friction[d.contact.dim > 1]
efc_address = d.contact.efc_address[d.contact.dim > 1]
dim = d.contact.dim[d.contact.dim > 1]
slice_fn = jax.vmap(lambda x: jax.lax.dynamic_slice(ctx.Jaref, (x,), (6,)))
u = slice_fn(efc_address) * ctx.fri
mu, n, t = ctx.fri[:, 0], u[:, 0], jax.vmap(math.norm)(u[:, 1:])
# only count active constraints # bottom zone: quadratic
active = (ctx.Jaref < 0).at[:d.ne + d.nf].set(True) bottom_zone = ((t <= 0) & (n < 0)) | ((t > 0) & ((mu * n + t) <= 0))
active = (ctx.Jaref < 0).at[:d.ne + d.nf].set(True)
adr_i, adr_j = [], []
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
adr_i.extend(range(addr, addr + condim))
adr_j.extend([i] * condim)
active = active.at[jp.array(adr_i)].set(bottom_zone[jp.array(adr_j)])
efc_force = d.efc_D * -ctx.Jaref * active
cost = 0.5 * jp.sum(d.efc_D * ctx.Jaref * ctx.Jaref * active)
# middle zone: cone
middle_zone = (t > 0) & (n < (mu * t)) & ((mu * n + t) > 0)
dm = d.efc_D[efc_address] / jp.maximum(
mu * mu * (1 + mu * mu), mujoco.mjMINVAL
)
nmt = n - mu * t
cost += 0.5 * jp.sum(dm * nmt * nmt * middle_zone)
# tangent and friction for middle zone:
force = -dm * nmt * mu * middle_zone
force_fri = -force / (t + ~middle_zone * mujoco.mjMINVAL)
force_fri = force_fri[:, None] * u[:, 1:] * friction
efc_force = efc_force.at[efc_address].add(force)
efc_adr, adr_i, adr_j = [], [], []
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
efc_adr.extend(range(addr + 1, addr + condim))
adr_i.extend([i] * (condim - 1))
adr_j.extend(range(condim - 1))
efc_adr, adr_i, adr_j = jp.array(efc_adr), jp.array(adr_i), jp.array(adr_j)
efc_force = efc_force.at[efc_adr].add(force_fri[(adr_i, adr_j)])
# cone hessian
h = 0.0
if m.opt.solver == SolverType.NEWTON:
t = jp.maximum(t, mujoco.mjMINVAL)
# h = mu*N/T^3 * U*U'
ttt = jp.maximum(t * t * t, mujoco.mjMINVAL)
h = jax.vmap(lambda x, y: x * jp.outer(y, y.T))(mu * n / ttt, u)
# add to diagonal: (mu^2 - mu*N/T) * I
h += jax.vmap(lambda x: x * jp.eye(6, 6))(mu * mu - mu * n / t)
# set first row: (1, -mu/T * U)
h_0 = jax.vmap(lambda mu, t, u: jp.append(1, -mu / t * u[1:]))(mu, t, u)
h = h.at[:, 0].set(h_0).at[:, :, 0].set(h_0)
# pre and post multiply by diag(mu, friction), scale by Dm
h *= jax.vmap(lambda d, f: d * jp.outer(f, f.T))(dm, ctx.fri)
# only cone constraints
h = jax.vmap(jp.multiply)(h, middle_zone)
else:
raise NotImplementedError(f'unsupported cone type: {m.opt.cone}')
efc_force = d.efc_D * -ctx.Jaref * active
qfrc_constraint = d.efc_J.T @ efc_force qfrc_constraint = d.efc_J.T @ efc_force
gauss = 0.5 * jp.dot(ctx.Ma - d.qfrc_smooth, ctx.qacc - d.qacc_smooth) gauss = 0.5 * jp.dot(ctx.Ma - d.qfrc_smooth, ctx.qacc - d.qacc_smooth)
cost = 0.5 * jp.sum(d.efc_D * ctx.Jaref * ctx.Jaref * active) + gauss
ctx = ctx.replace( ctx = ctx.replace(
qfrc_constraint=qfrc_constraint, qfrc_constraint=qfrc_constraint,
gauss=gauss, gauss=gauss,
cost=cost, cost=cost + gauss,
prev_cost=ctx.cost, prev_cost=ctx.cost,
efc_force=efc_force, efc_force=efc_force,
active=active,
dm=dm,
u=u,
h=h,
) )
return ctx return ctx
@@ -213,8 +342,17 @@ def _update_gradient(m: Model, d: Data, ctx: _Context) -> _Context:
if m.opt.solver == SolverType.CG: if m.opt.solver == SolverType.CG:
mgrad = smooth.solve_m(m, d, grad) mgrad = smooth.solve_m(m, d, grad)
elif m.opt.solver == SolverType.NEWTON: elif m.opt.solver == SolverType.NEWTON:
active = (ctx.Jaref < 0).at[: d.ne + d.nf].set(True) if m.opt.cone == ConeType.ELLIPTIC:
h = (d.efc_J.T * d.efc_D * active) @ d.efc_J cm = jp.diag(d.efc_D * ctx.active)
efc_address = d.contact.efc_address[d.contact.dim > 1]
dim = d.contact.dim[d.contact.dim > 1]
# set efc of cone H along diagonal
for i, (condim, addr) in enumerate(zip(dim, efc_address)):
h_cone = ctx.h[i, :condim, :condim]
cm = cm.at[addr:addr+condim, addr:addr+condim].add(h_cone)
h = d.efc_J.T @ cm @ d.efc_J
else:
h = (d.efc_J.T * d.efc_D * ctx.active) @ d.efc_J
h = support.full_m(m, d) + h h = support.full_m(m, d) + h
h_ = jax.scipy.linalg.cho_factor(h) h_ = jax.scipy.linalg.cho_factor(h)
mgrad = jax.scipy.linalg.cho_solve(h_, grad) mgrad = jax.scipy.linalg.cho_solve(h_, grad)
@@ -256,8 +394,28 @@ def _linesearch(m: Model, d: Data, ctx: _Context) -> _Context:
)) ))
quad = jp.stack((0.5 * ctx.Jaref * ctx.Jaref, jv * ctx.Jaref, 0.5 * jv * jv)) quad = jp.stack((0.5 * ctx.Jaref * ctx.Jaref, jv * ctx.Jaref, 0.5 * jv * jv))
quad = (quad * d.efc_D).T quad = (quad * d.efc_D).T
uu, v0, uv, vv = 0.0, 0.0, 0.0, 0.0
if m.opt.cone == ConeType.ELLIPTIC:
mask = d.contact.dim > 1
# complete vector quadratic (for bottom zone)
efc_con, efc_fri = [], []
for condim, addr in zip(d.contact.dim[mask], d.contact.efc_address[mask]):
efc_con.extend([addr] * (condim - 1))
efc_fri.extend(range(addr + 1, addr + condim))
quad = quad.at[jp.array(efc_con)].add(quad[jp.array(efc_fri)])
point_fn = lambda a: _LSPoint.create(d, ctx, a, jv, quad, quad_gauss) # rescale to make primal cone circular
jv_fn = jax.vmap(lambda x: jax.lax.dynamic_slice(jv, (x,), (6,)))
efc_elliptic = d.contact.efc_address[mask]
v = jv_fn(efc_elliptic) * ctx.fri
uu = jp.sum(ctx.u[:, 1:] * ctx.u[:, 1:], axis=1)
v0 = v[:, 0]
uv = jp.sum(ctx.u[:, 1:] * v[:, 1:], axis=1)
vv = jp.sum(v[:, 1:] * v[:, 1:], axis=1)
point_fn = lambda a: _LSPoint.create(
m, d, ctx, a, jv, quad, quad_gauss, uu, v0, uv, vv
)
def cond(ctx: _LSContext) -> jax.Array: def cond(ctx: _LSContext) -> jax.Array:
done = ctx.ls_iter >= m.opt.ls_iterations done = ctx.ls_iter >= m.opt.ls_iterations
@@ -274,21 +432,34 @@ def _linesearch(m: Model, d: Data, ctx: _Context) -> _Context:
hi_next = point_fn(hi.alpha - hi.deriv_0 / hi.deriv_1) hi_next = point_fn(hi.alpha - hi.deriv_0 / hi.deriv_1)
mid = point_fn(0.5 * (lo.alpha + hi.alpha)) mid = point_fn(0.5 * (lo.alpha + hi.alpha))
# we swap lo/hi if: # swap lo/hi if the derivative points to a narrower bracket width
# 1) they are not correctly at a bracket boundary (e.g. lo.deriv_0 > 0), OR in_bracket = lambda x, y: ((x < y) & (y < 0) | (x > y) & (y > 0))
# 2) if moving to next or mid narrows the bracket swap_lo_next = in_bracket(lo.deriv_0, lo_next.deriv_0)
swap_lo_next = (lo.deriv_0 > 0) | (lo.deriv_0 < lo_next.deriv_0) lo = jax.tree_util.tree_map(
lo = jax.tree_util.tree_map(lambda x, y: jp.where(swap_lo_next, y, x), lo, lo_next) lambda x, y: jp.where(swap_lo_next, y, x), lo, lo_next
swap_lo_mid = (mid.deriv_0 < 0) & (lo.deriv_0 < mid.deriv_0) )
lo = jax.tree_util.tree_map(lambda x, y: jp.where(swap_lo_mid, y, x), lo, mid) swap_lo_mid = in_bracket(lo.deriv_0, mid.deriv_0)
lo = jax.tree_util.tree_map(
swap_hi_next = (hi.deriv_0 < 0) | (hi.deriv_0 > hi_next.deriv_0) lambda x, y: jp.where(swap_lo_mid, y, x), lo, mid
hi = jax.tree_util.tree_map(lambda x, y: jp.where(swap_hi_next, y, x), hi, hi_next) )
swap_hi_mid = (mid.deriv_0 > 0) & (hi.deriv_0 > mid.deriv_0) swap_lo_hi_next = in_bracket(lo.deriv_0, hi_next.deriv_0)
hi = jax.tree_util.tree_map(lambda x, y: jp.where(swap_hi_mid, y, x), hi, mid) lo = jax.tree_util.tree_map(
lambda x, y: jp.where(swap_lo_hi_next, y, x), lo, hi_next
swap = swap_lo_next | swap_lo_mid | swap_hi_next | swap_hi_mid )
swap_hi_next = in_bracket(hi.deriv_0, hi_next.deriv_0)
hi = jax.tree_util.tree_map(
lambda x, y: jp.where(swap_hi_next, y, x), hi, hi_next
)
swap_hi_mid = in_bracket(hi.deriv_0, mid.deriv_0)
hi = jax.tree_util.tree_map(
lambda x, y: jp.where(swap_hi_mid, y, x), hi, mid
)
swap_hi_lo_next = in_bracket(hi.deriv_0, lo_next.deriv_0)
hi = jax.tree_util.tree_map(
lambda x, y: jp.where(swap_hi_lo_next, y, x), hi, lo_next
)
swap = swap_lo_next | swap_lo_mid | swap_lo_hi_next
swap = swap | swap_hi_next | swap_hi_mid | swap_hi_lo_next
ctx = ctx.replace(lo=lo, hi=hi, swap=swap, ls_iter=ctx.ls_iter + 1) ctx = ctx.replace(lo=lo, hi=hi, swap=swap, ls_iter=ctx.ls_iter + 1)
return ctx return ctx
@@ -331,14 +502,17 @@ def solve(m: Model, d: Data) -> Data:
def body(ctx: _Context) -> _Context: def body(ctx: _Context) -> _Context:
ctx = _linesearch(m, d, ctx) ctx = _linesearch(m, d, ctx)
prev_grad, prev_Mgrad = ctx.grad, ctx.Mgrad # pylint: disable=invalid-name prev_grad, prev_Mgrad = ctx.grad, ctx.Mgrad # pylint: disable=invalid-name
ctx = _update_constraint(d, ctx) ctx = _update_constraint(m, d, ctx)
ctx = _update_gradient(m, d, ctx) ctx = _update_gradient(m, d, ctx)
# polak-ribiere: if m.opt.solver == SolverType.NEWTON:
beta = jp.dot(ctx.grad, ctx.Mgrad - prev_Mgrad) search = -ctx.Mgrad
beta = beta / jp.maximum(mujoco.mjMINVAL, jp.dot(prev_grad, prev_Mgrad)) else:
beta = jp.maximum(0, beta) # polak-ribiere:
search = -ctx.Mgrad + beta * ctx.search beta = jp.dot(ctx.grad, ctx.Mgrad - prev_Mgrad)
beta = beta / jp.maximum(mujoco.mjMINVAL, jp.dot(prev_grad, prev_Mgrad))
beta = jp.maximum(0, beta)
search = -ctx.Mgrad + beta * ctx.search
ctx = ctx.replace(search=search, solver_niter=ctx.solver_niter + 1) ctx = ctx.replace(search=search, solver_niter=ctx.solver_niter + 1)
return ctx return ctx
+58 -43
View File
@@ -15,16 +15,18 @@
"""Tests for constraint functions.""" """Tests for constraint functions."""
from absl.testing import absltest from absl.testing import absltest
from absl.testing import parameterized
import jax import jax
import mujoco import mujoco
from mujoco import mjx from mujoco import mjx
from mujoco.mjx._src import solver
from mujoco.mjx._src import test_util from mujoco.mjx._src import test_util
import numpy as np import numpy as np
# tolerance for difference between MuJoCo and MJX constraint calculations, # tolerance for difference between MuJoCo and MJX solver calculations,
# mostly due to float precision # mostly due to float precision
_TOLERANCE = 5e-5 _TOLERANCE = 5e-3
def _assert_eq(a, b, name, tol=_TOLERANCE): def _assert_eq(a, b, name, tol=_TOLERANCE):
@@ -37,72 +39,85 @@ def _assert_attr_eq(a, b, attr, tol=_TOLERANCE):
_assert_eq(getattr(a, attr), getattr(b, attr), attr, tol=tol) _assert_eq(getattr(a, attr), getattr(b, attr), attr, tol=tol)
class SolverTest(absltest.TestCase): class SolverTest(parameterized.TestCase):
def test_newton(self): @parameterized.parameters(
"""Test newton solver.""" # these scene challenges the solver, with CG you need to crank up
# the iterations, otherwise it diverges
(mujoco.mjtSolver.mjSOL_CG, mujoco.mjtCone.mjCONE_PYRAMIDAL, 100),
(mujoco.mjtSolver.mjSOL_CG, mujoco.mjtCone.mjCONE_ELLIPTIC, 100),
# Newton converges much more quickly, lower iterations to demonstrate
# mgrad is being calculated optimally
(mujoco.mjtSolver.mjSOL_NEWTON, mujoco.mjtCone.mjCONE_PYRAMIDAL, 2),
(mujoco.mjtSolver.mjSOL_NEWTON, mujoco.mjtCone.mjCONE_ELLIPTIC, 2),
)
def test_solver(self, solver_, cone, iterations):
"""Test newton, CG solver with pyramidal, elliptic cones."""
m = test_util.load_test_file('constraints.xml') m = test_util.load_test_file('constraints.xml')
# it's critical that mgrad is optimally calculated, so lower iterations m.opt.solver = solver_
# to be sure that MJX is converging as quickly as MuJoCo m.opt.cone = cone
m.opt.iterations = 1 m.opt.iterations = iterations
d = mujoco.MjData(m) d = mujoco.MjData(m)
mujoco.mj_step(m, d, 20) # significant constraint forces at 20 steps
# mj_forward overwrites qacc_warmstart, so let's restore it to what it was def cost(qacc):
# at the beginning of the step so that MJX does not have a trivial solution jaref = np.zeros(d.nefc, dtype=float)
warmstart = d.qacc_warmstart.copy() cost = np.zeros(1)
mujoco.mj_forward(m, d) mujoco.mj_mulJacVec(m, d, jaref, qacc)
d.qacc_warmstart = warmstart mujoco.mj_constraintUpdate(m, d, jaref - d.efc_aref, cost, 0)
return cost
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d)) # sample a mix of active/inactive constraints at different timesteps
for key in range(0, 3):
mujoco.mj_resetDataKeyframe(m, d, key)
mujoco.mj_step(m, d) # step to generate warmstart
_assert_attr_eq(d, dx, 'qacc') # compare costs
_assert_attr_eq(d, dx, 'qfrc_constraint') mj_cost = cost(d.qacc)
nnz = dx.efc_J.any(axis=1) ctx = solver._Context.create(mjx.put_model(m), mjx.put_data(m, d))
_assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force') mjx_cost = ctx.cost - ctx.gauss
_assert_eq(mj_cost, mjx_cost, 'cost')
def test_cg(self): # mj_forward overwrites qacc_warmstart, so let's restore it to what it was
"""Test CG solver.""" # before the step so that MJX does not have a trivial solution
m = test_util.load_test_file('constraints.xml') warmstart = d.qacc_warmstart.copy()
d = mujoco.MjData(m) mujoco.mj_forward(m, d)
mujoco.mj_step(m, d, 20) # significant constraint forces at 20 steps d.qacc_warmstart = warmstart
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
# CG does not converge as quickly as Newton but is cheaper to calculate # MJX finds very similar solutions with the newton solver
m.opt.solver = mujoco.mjtSolver.mjSOL_CG if solver_ == mujoco.mjtSolver.mjSOL_NEWTON:
m.opt.iterations = 8 nnz = dx.efc_J.any(axis=1)
_assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force')
_assert_attr_eq(d, dx, 'qfrc_constraint')
_assert_attr_eq(d, dx, 'qacc')
# mj_forward overwrites qacc_warmstart, so let's restore it to what it was # both CG and Newton find costs that are nearly the same as MuJoCo, often
# at the beginning of the step so that MJX does not have a trivial solution # lower (due to slight differences in the MJX linsearch algorithm)
warmstart = d.qacc_warmstart.copy() mj_cost = cost(d.qacc)
mujoco.mj_forward(m, d) mjx_cost = cost(dx.qacc)
d.qacc_warmstart = warmstart self.assertLess(mjx_cost, mj_cost * 1.01)
dx = jax.jit(mjx.solve)(mjx.put_model(m), mjx.put_data(m, d))
_assert_attr_eq(d, dx, 'qacc')
_assert_attr_eq(d, dx, 'qfrc_constraint', tol=8e-4)
nnz = dx.efc_J.any(axis=1)
_assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force', tol=5e-4)
def test_no_warmstart(self): def test_no_warmstart(self):
"""Test no warmstart.""" """Test no warmstart."""
m = test_util.load_test_file('constraints.xml') m = test_util.load_test_file('constraints.xml')
d = mujoco.MjData(m) d = mujoco.MjData(m)
mujoco.mj_step(m, d, 20) # significant constraint forces at 20 steps # significant constraint forces keyframe 2
mujoco.mj_resetDataKeyframe(m, d, 2)
m.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_WARMSTART m.opt.disableflags |= mujoco.mjtDisableBit.mjDSBL_WARMSTART
mujoco.mj_forward(m, d) mujoco.mj_forward(m, d)
mx = mjx.put_model(m) mx = mjx.put_model(m)
dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d)) dx = jax.jit(mjx.solve)(mx, mjx.put_data(m, d))
nnz = dx.efc_J.any(axis=1) nnz = dx.efc_J.any(axis=1)
# without warmstart, the solution is not as close # even without warmstart, newton converges quickly
_assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force', tol=2e-2) _assert_eq(d.efc_force, dx.efc_force[nnz], 'efc_force', tol=2e-4)
def test_sparse(self): def test_sparse(self):
"""Test solver works with sparse mass matrices.""" """Test solver works with sparse mass matrices."""
m = test_util.load_test_file('constraints.xml') m = test_util.load_test_file('constraints.xml')
m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE m.opt.jacobian = mujoco.mjtJacobian.mjJAC_SPARSE
d = mujoco.MjData(m) d = mujoco.MjData(m)
mujoco.mj_step(m, d, 20) # significant constraint forces at 20 steps # significant constraint forces keyframe 2
mujoco.mj_resetDataKeyframe(m, d, 2)
# mj_forward overwrites qacc_warmstart, so let's restore it to what it was # mj_forward overwrites qacc_warmstart, so let's restore it to what it was
# at the beginning of the step so that MJX does not have a trivial solution # at the beginning of the step so that MJX does not have a trivial solution
+23
View File
@@ -26,6 +26,7 @@ import mujoco
# pylint: disable=g-importing-member # pylint: disable=g-importing-member
from mujoco.mjx._src import forward from mujoco.mjx._src import forward
from mujoco.mjx._src import io from mujoco.mjx._src import io
from mujoco.mjx._src.types import Data
# pylint: enable=g-importing-member # pylint: enable=g-importing-member
import numpy as np import numpy as np
@@ -104,6 +105,28 @@ def benchmark(
return jit_time, run_time, steps return jit_time, run_time, steps
def efc_order(m: mujoco.MjModel, d: mujoco.MjData, dx: Data) -> np.ndarray:
"""Returns a sort order such that dx.efc_*[order][:d.nefc] == d.efc_*."""
# reorder efc rows to skip inactive constraints and match contact order
efl = dx.ne + dx.nf + dx.nl
order = np.arange(efl)
order[(dx.efc_J[:efl] == 0).all(axis=1)] = 2**16 # move empty rows to end
for i in range(dx.ncon):
num_rows = dx.contact.dim[i]
if dx.contact.dim[i] > 1 and m.opt.cone == mujoco.mjtCone.mjCONE_PYRAMIDAL:
num_rows = (dx.contact.dim[i] - 1) * 2
if dx.contact.dist[i] > 0: # move empty contacts to end
order = np.append(order, np.repeat(2 ** 16, num_rows))
continue
contact_match = (d.contact.geom == dx.contact.geom[i]).all(axis=-1)
contact_match &= (d.contact.pos == dx.contact.pos[i]).all(axis=-1)
assert contact_match.any(), f'contact {i} not found'
contact_id = np.nonzero(contact_match)[0][0]
order = np.append(order, np.repeat(efl + contact_id, num_rows))
return np.argsort(order, kind='stable')
_ACTUATOR_TYPES = ['motor', 'velocity', 'position', 'general', 'intvelocity'] _ACTUATOR_TYPES = ['motor', 'velocity', 'position', 'general', 'intvelocity']
_DYN_TYPES = ['none', 'integrator', 'filter', 'filterexact'] _DYN_TYPES = ['none', 'integrator', 'filter', 'filterexact']
_DYN_PRMS = ['0.189', '2.1'] _DYN_PRMS = ['0.189', '2.1']
+4 -3
View File
@@ -135,9 +135,10 @@ class ConeType(enum.IntEnum):
Attributes: Attributes:
PYRAMIDAL: pyramidal PYRAMIDAL: pyramidal
ELLIPTIC: elliptic
""" """
PYRAMIDAL = mujoco.mjtCone.mjCONE_PYRAMIDAL PYRAMIDAL = mujoco.mjtCone.mjCONE_PYRAMIDAL
# unsupported: ELLIPTIC ELLIPTIC = mujoco.mjtCone.mjCONE_ELLIPTIC
class JacobianType(enum.IntEnum): class JacobianType(enum.IntEnum):
@@ -245,7 +246,7 @@ class ConstraintType(enum.IntEnum):
# unsupported: LIMIT_TENDON # unsupported: LIMIT_TENDON
CONTACT_FRICTIONLESS = mujoco.mjtConstraint.mjCNSTR_CONTACT_FRICTIONLESS CONTACT_FRICTIONLESS = mujoco.mjtConstraint.mjCNSTR_CONTACT_FRICTIONLESS
CONTACT_PYRAMIDAL = mujoco.mjtConstraint.mjCNSTR_CONTACT_PYRAMIDAL CONTACT_PYRAMIDAL = mujoco.mjtConstraint.mjCNSTR_CONTACT_PYRAMIDAL
# unsupported: CONTACT_ELLIPTIC CONTACT_ELLIPTIC = mujoco.mjtConstraint.mjCNSTR_CONTACT_ELLIPTIC
class CamLightType(enum.IntEnum): class CamLightType(enum.IntEnum):
@@ -703,7 +704,7 @@ class Contact(PyTreeNode):
solref: jax.Array solref: jax.Array
solreffriction: jax.Array solreffriction: jax.Array
solimp: jax.Array solimp: jax.Array
# unsupported: mu, H # unsupported: mu, H (calculated locally in solver.py)
dim: np.ndarray dim: np.ndarray
geom1: jax.Array geom1: jax.Array
geom2: jax.Array geom2: jax.Array
+10 -1
View File
@@ -58,7 +58,7 @@
<geom class="box" condim="4"/> <geom class="box" condim="4"/>
</body> </body>
<body name="box_condim6" pos="6 0 0"> <body name="box_condim6" pos="7 0 0">
<freejoint/> <freejoint/>
<geom class="box" condim="6"/> <geom class="box" condim="6"/>
</body> </body>
@@ -75,4 +75,13 @@
<motor gear="50000" joint="joint3" name="act2"/> <motor gear="50000" joint="joint3" name="act2"/>
<motor gear="75000" joint="joint4" name="act3"/> <motor gear="75000" joint="joint4" name="act3"/>
</actuator> </actuator>
<keyframe>
<!-- keyframe 0: default position with some motion, zero contacts -->
<key qpos='1 0 0 0 -1 0 0 1 0 0 0 0 0 1 0 0 0 4 0 0 1 0 0 0 5 0 0 1 0 0 0 6 0 0 1 0 0 0 7 0 0 1 0 0 0' qvel='1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1'/>
<!-- keyframe 1: some contacts but constraints are quadratic -->
<key qpos='1 0.0011 -8.2e-07 -0.0011 -0.82 1.9 -0.58 0.97 -0.14 0.17 -0.069 0.22 0.29 0.93 0.2 0.15 0.28 4.3 0.23 -0.26 0.97 0.12 0.11 0.18 5.4 0.25 -0.26 0.97 0.11 0.16 0.17 6.4 0.25 -0.26 0.97 0.11 0.16 0.17 7.4 0.25 -0.26 0.97 0.11 0.16 0.17' qvel='-0.14 3.3e-05 0.14 -0.14 -0.93 -3.6 -1.8 1 -0.57 -0.16 0.21 1.1 1 2.2 0.65 1.3 -4.2 -2.6 -5.6 0.54 1.6 2.8 -3.7 -2.3 0.7 -1.8 1.6 2.8 -3.7 -2.3 0.62 -1.8 1.6 2.8 -3.7 -2.3 0.61 -1.8'/>
<!-- keyframe 2: some contacts and some constraints are in cone state (for elliptic) -->
<key qpos='1 0.0087 2.4e-07 -0.0086 -0.89 1.8 -0.77 0.98 -0.2 -0.0022 -0.026 0.19 0.33 0.86 0.32 0.064 0.38 4.4 0.36 -0.81 0.97 -0.0013 -0.0011 0.25 5.6 0.52 -0.75 0.98 -0.018 0.17 0.094 6.6 0.52 -0.75 0.98 -0.017 0.16 0.094 7.6 0.52 -0.76 0.98 -0.017 0.16 0.094' qvel='0.2 -1.8e-05 -0.2 -0.72 0.072 0.025 0.015 -4.9 0.35 -0.22 0.26 0.99 -4.6 1.7 1.1 0.52 -0.73 0.16 7.9 1 0.043 -0.042 1.3 1.6 -1.6 0.98 0.027 -0.024 1.3 1.6 -1.8 0.96 0.025 -0.022 1.3 1.6 -1.8 0.96'/>
</keyframe>
</mujoco> </mujoco>