fa062768f4
PiperOrigin-RevId: 829017789 Change-Id: I2756e349297cd0ae53a63a38fb203e2fd2d5cb99
207 lines
6.4 KiB
Python
207 lines
6.4 KiB
Python
# Copyright 2025 DeepMind Technologies Limited
|
|
#
|
|
# 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.
|
|
# ==============================================================================
|
|
"""Utilities for testing MJX MjWarp integration."""
|
|
import jax
|
|
import jax.numpy as jp
|
|
import mujoco
|
|
from mujoco import mjx
|
|
from mujoco.mjx._src import test_util as mjx_test_util
|
|
import numpy as np
|
|
|
|
try:
|
|
from mujoco.mjx.warp import forward as mjxw_forward # pylint: disable=g-import-not-at-top
|
|
except ImportError:
|
|
mjxw_forward = None
|
|
|
|
# tolerance for difference between MuJoCo and MJX smooth calculations - mostly
|
|
# due to float precision
|
|
_TOLERANCE = 5e-5
|
|
|
|
|
|
def assert_eq(a, b, name):
|
|
tol = _TOLERANCE * 10 # avoid test noise
|
|
err_msg = f'mismatch: {name}'
|
|
np.testing.assert_allclose(a, b, err_msg=err_msg, atol=tol, rtol=tol)
|
|
|
|
|
|
def assert_attr_eq(a, b, attr):
|
|
assert_eq(getattr(a, attr), getattr(b, attr), attr)
|
|
|
|
|
|
def make_data(
|
|
m: mujoco.MjModel, worldid: int, nconmax: int = 1_000, njmax: int = 200
|
|
):
|
|
"""Make data for a given worldid using keyframes when available."""
|
|
dx = mjx.make_data(m, impl='warp', nconmax=nconmax, njmax=njmax)
|
|
|
|
rng = jax.random.PRNGKey(worldid)
|
|
rng, key = jax.random.split(rng)
|
|
qpos = m.qpos0 + jax.random.uniform(key, (m.nq,), minval=-0.05, maxval=0.05)
|
|
key_qpos = (
|
|
jp.array(m.key_qpos)[worldid] if m.nkey > 0 else jp.zeros_like(qpos)
|
|
)
|
|
qpos = jp.where(worldid < m.nkey, key_qpos, qpos)
|
|
|
|
rng, key = jax.random.split(rng)
|
|
qvel = jax.random.uniform(key, (m.nv,), minval=-0.05, maxval=0.05)
|
|
key_qvel = (
|
|
jp.array(m.key_qvel)[worldid] if m.nkey > 0 else jp.zeros_like(qvel)
|
|
)
|
|
qvel = jp.where(worldid < m.nkey, key_qvel, qvel)
|
|
|
|
rng, key1, key2 = jax.random.split(rng, 3)
|
|
mocap_pos = jax.random.normal(key1, (m.nmocap, 3))
|
|
key_mpos = (
|
|
jp.array(m.key_mpos)[worldid]
|
|
if m.nkey > 0 and m.nmocap > 0
|
|
else jp.zeros_like(mocap_pos)
|
|
)
|
|
mocap_pos = jp.where(worldid < m.nkey, key_mpos, mocap_pos)
|
|
mocap_quat = jax.random.normal(key2, (m.nmocap, 4))
|
|
key_mquat = (
|
|
jp.array(m.key_mquat)[worldid]
|
|
if m.nkey > 0 and m.nmocap > 0
|
|
else jp.zeros_like(mocap_quat)
|
|
)
|
|
mocap_quat = jp.where(worldid < m.nkey, key_mquat, mocap_quat)
|
|
|
|
_, key = jax.random.split(rng)
|
|
ctrl = jax.random.uniform(key, (m.nu,), minval=-0.1, maxval=0.1)
|
|
key_ctrl = (
|
|
jp.array(m.key_ctrl)[worldid] if m.nkey > 0 else jp.zeros_like(ctrl)
|
|
)
|
|
ctrl = jp.where(worldid < m.nkey, key_ctrl, ctrl)
|
|
|
|
dx = dx.replace(
|
|
qpos=qpos,
|
|
qvel=qvel,
|
|
mocap_pos=mocap_pos,
|
|
mocap_quat=mocap_quat,
|
|
ctrl=ctrl,
|
|
)
|
|
|
|
# Mimic forward call within a vmap trace (since make_data gets called in a
|
|
# vmap in tests). Some fields get vmapped (qpos, qvel) while others need to be
|
|
# broadcasted by the custom vmap rule (e.g. geom_xpos).
|
|
mx = mjx.put_model(m, impl='warp')
|
|
dx = mjxw_forward.forward(mx, dx)
|
|
|
|
return dx
|
|
|
|
|
|
def _mjx_contact(dx, worldid: int):
|
|
keys = []
|
|
for i in range(dx._impl.nacon[0]):
|
|
if dx._impl.contact__worldid[i] != worldid:
|
|
continue
|
|
|
|
g1, g2 = tuple(map(int, dx._impl.contact__geom[i]))
|
|
dist = float(dx._impl.contact__dist[i])
|
|
keys.append((g1, g2, -dist, i))
|
|
|
|
keys = sorted(keys)
|
|
geom1 = np.array([k[0] for k in keys])
|
|
geom2 = np.array([k[1] for k in keys])
|
|
dist = np.array([dx._impl.contact__dist[k[-1]] for k in keys])
|
|
normal = np.array([dx._impl.contact__frame[k[-1]][0] for k in keys])
|
|
return geom1, geom2, dist, normal
|
|
|
|
|
|
def _mj_contact(d):
|
|
keys = []
|
|
for i in range(d.ncon):
|
|
g1, g2 = tuple(map(int, d.contact.geom[i]))
|
|
dist = float(d.contact.dist[i])
|
|
keys.append((g1, g2, -dist, i))
|
|
|
|
keys = sorted(keys)
|
|
geom1 = np.array([k[0] for k in keys])
|
|
geom2 = np.array([k[1] for k in keys])
|
|
dist = np.array([d.contact.dist[k[-1]] for k in keys])
|
|
normal = np.array([d.contact.frame[k[-1]][:3] for k in keys])
|
|
return geom1, geom2, dist, normal
|
|
|
|
|
|
def assert_contact_eq(d, dx, worldid: int):
|
|
*geom, dist, normal = _mj_contact(d)
|
|
*geomp, distp, normalp = _mjx_contact(dx, worldid)
|
|
assert_eq(geomp, geom, 'geom')
|
|
assert_eq(distp, dist, 'dist')
|
|
assert_eq(normalp, normal, 'normal')
|
|
|
|
|
|
def _mjx_efc(dx, worldid: int):
|
|
"""Gets unpacked efc data for a given worldid."""
|
|
select = lambda x: x if dx._impl.nefc.ndim == 0 else x[worldid]
|
|
nefc = select(dx._impl.nefc)
|
|
keys = np.arange(nefc)
|
|
if not keys.size:
|
|
empty = np.array([])
|
|
return 0, empty, empty, np.zeros((0, dx.qvel.shape[-1])), empty, empty
|
|
efc_pos = select(dx._impl.efc__pos)[:nefc]
|
|
efc_type = select(dx._impl.efc__type)[:nefc]
|
|
efc_d = select(dx._impl.efc__D)[:nefc]
|
|
keys_sorted = np.lexsort((-efc_pos, efc_type, efc_d))
|
|
keys = keys[keys_sorted]
|
|
|
|
nefc = len(keys)
|
|
type_ = efc_type[keys]
|
|
pos = efc_pos[keys]
|
|
# MuJoCo Warp may pad efc_J for tiled ops.
|
|
j = select(dx._impl.efc__J)[:nefc][keys][:, :dx.qvel.shape[-1]]
|
|
aref = select(dx._impl.efc__aref)[:nefc][keys]
|
|
d_ = select(dx._impl.efc__D)[:nefc][keys]
|
|
return nefc, type_, pos, j, aref, d_
|
|
|
|
|
|
def _mj_efc(d):
|
|
"""Gets unpacked efc data."""
|
|
efc_j = np.zeros((d.efc_J_rownnz.shape[0], d.qvel.shape[0]))
|
|
if d.efc_J.shape[0] < efc_j.shape[0] * efc_j.shape[1]:
|
|
mujoco.mju_sparse2dense(
|
|
efc_j,
|
|
d.efc_J,
|
|
d.efc_J_rownnz,
|
|
d.efc_J_rowadr,
|
|
d.efc_J_colind,
|
|
)
|
|
else:
|
|
efc_j = d.efc_J.reshape((-1, d.qvel.shape[0]))
|
|
|
|
keys = np.lexsort((-d.efc_pos, d.efc_type, d.efc_D))
|
|
type_ = d.efc_type[keys]
|
|
pos = d.efc_pos[keys]
|
|
efc_j = efc_j[keys]
|
|
aref = d.efc_aref[keys]
|
|
d_ = d.efc_D[keys]
|
|
return d.nefc, type_, pos, efc_j, aref, d_
|
|
|
|
|
|
def assert_efc_eq(d, dx, worldid: int):
|
|
nefc, type_, pos, j, aref, d_ = _mj_efc(d)
|
|
nefcp, typep, posp, jp_, arefp, dp = _mjx_efc(dx, worldid)
|
|
|
|
assert_eq(nefcp, nefc, 'nefc')
|
|
assert_eq(typep, type_, 'type')
|
|
assert_eq(posp, pos, 'pos')
|
|
assert_eq(jp_, j, 'J')
|
|
assert_eq(arefp, aref, 'aref')
|
|
assert_eq(dp, d_, 'D')
|
|
|
|
|
|
def load_test_file(name: str) -> mujoco.MjModel:
|
|
"""Loads a mujoco.MjModel based on the file name."""
|
|
return mjx_test_util.load_test_file(name)
|