Implement get_data_into_cpp and make_data_cpp.
PiperOrigin-RevId: 855192298 Change-Id: I9f9fd07094c40b9df1b645c2c893bb2605941e32
This commit is contained in:
committed by
Copybara-Service
parent
5979c5aa03
commit
a6a9a85007
@@ -912,6 +912,44 @@ def _make_data_warp(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _make_data_cpp(
|
||||||
|
m: Union[types.Model, mujoco.MjModel],
|
||||||
|
device: Optional[jax.Device] = None,
|
||||||
|
) -> types.Data:
|
||||||
|
"""Allocate and initialize Data for the CPP implementation."""
|
||||||
|
if isinstance(m, mujoco.MjModel):
|
||||||
|
mj_model = m
|
||||||
|
else:
|
||||||
|
# Get the underlying MjModel from the types.Model
|
||||||
|
m_impl = m._impl # pylint: disable=protected-access
|
||||||
|
if not isinstance(m_impl, types.ModelCPP):
|
||||||
|
raise ValueError(f'Expected ModelCPP impl, got {type(m_impl)}')
|
||||||
|
mj_model = m_impl._model # pylint: disable=protected-access
|
||||||
|
|
||||||
|
# Create the raw MuJoCo data
|
||||||
|
mj_data = mujoco.MjData(mj_model)
|
||||||
|
|
||||||
|
# Get the pointer address
|
||||||
|
addr = mj_data._address # pytype: disable=attribute-error
|
||||||
|
pointer_lo = jp.array(addr & 0xFFFFFFFF, dtype=jp.uint32)
|
||||||
|
pointer_hi = jp.array(addr >> 32, dtype=jp.uint32)
|
||||||
|
|
||||||
|
fields = _put_data_public_fields(mj_data)
|
||||||
|
|
||||||
|
c_pointers_impl = types.DataCPP(
|
||||||
|
pointer_lo=pointer_lo,
|
||||||
|
pointer_hi=pointer_hi,
|
||||||
|
_data=[mj_data],
|
||||||
|
)
|
||||||
|
|
||||||
|
data = types.Data(
|
||||||
|
_impl=c_pointers_impl,
|
||||||
|
**fields,
|
||||||
|
)
|
||||||
|
data = jax.device_put(data, device=device)
|
||||||
|
return _strip_weak_type(data)
|
||||||
|
|
||||||
|
|
||||||
def make_data(
|
def make_data(
|
||||||
m: Union[types.Model, mujoco.MjModel],
|
m: Union[types.Model, mujoco.MjModel],
|
||||||
device: Optional[jax.Device] = None,
|
device: Optional[jax.Device] = None,
|
||||||
@@ -965,6 +1003,8 @@ def make_data(
|
|||||||
return _make_data_jax(m, device)
|
return _make_data_jax(m, device)
|
||||||
elif impl == types.Impl.C:
|
elif impl == types.Impl.C:
|
||||||
return _make_data_c(m, device)
|
return _make_data_c(m, device)
|
||||||
|
elif impl == types.Impl.CPP:
|
||||||
|
return _make_data_cpp(m, device)
|
||||||
elif impl == types.Impl.WARP:
|
elif impl == types.Impl.WARP:
|
||||||
naconmax = nconmax if naconmax is None else naconmax
|
naconmax = nconmax if naconmax is None else naconmax
|
||||||
return _make_data_warp(m, device, naconmax, njmax)
|
return _make_data_warp(m, device, naconmax, njmax)
|
||||||
@@ -1265,12 +1305,8 @@ def _put_data_cpp(
|
|||||||
def _copy_and_get_addr(unused_jax_array):
|
def _copy_and_get_addr(unused_jax_array):
|
||||||
# We use the input to the callback as a dummy dependency to ensure
|
# We use the input to the callback as a dummy dependency to ensure
|
||||||
# io_callback runs for each element in the batch.
|
# io_callback runs for each element in the batch.
|
||||||
try:
|
new_d = mujoco.MjData(m)
|
||||||
new_d = mujoco.MjData(m)
|
mujoco.mj_copyData(new_d, m, d)
|
||||||
except mujoco.FatalError as e:
|
|
||||||
raise ValueError('Failed to create new MjData') from e
|
|
||||||
mujoco.mj_copyState(m, d, new_d, int(mujoco.mjtState.mjSTATE_FULLPHYSICS))
|
|
||||||
mujoco.mj_forward(m, new_d)
|
|
||||||
data_list.append(new_d)
|
data_list.append(new_d)
|
||||||
addr = new_d._address
|
addr = new_d._address
|
||||||
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
|
# To ensure that we retain the full pointer even if jax.config.enable_x64 is
|
||||||
@@ -1586,6 +1622,58 @@ def _get_data_into(
|
|||||||
mujoco.mj_factorM(m, result_i)
|
mujoco.mj_factorM(m, result_i)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_data_into_cpp(
|
||||||
|
result: Union[mujoco.MjData, List[mujoco.MjData]],
|
||||||
|
m: mujoco.MjModel,
|
||||||
|
d: types.Data,
|
||||||
|
):
|
||||||
|
"""Gets mjx.Data from CPP impl into an existing mujoco.MjData or list.
|
||||||
|
|
||||||
|
For the CPP implementation, the mjx.Data wraps underlying mujoco.MjData
|
||||||
|
objects that are stored in DataCPP._data. This function simply copies the
|
||||||
|
data from those underlying MjData objects to the result using mj_copyData.
|
||||||
|
"""
|
||||||
|
|
||||||
|
batched = isinstance(result, list)
|
||||||
|
d = jax.device_get(d)
|
||||||
|
batch_size = d.qpos.shape[0] if batched else 1
|
||||||
|
|
||||||
|
d_impl = d._impl # pylint: disable=protected-access
|
||||||
|
if not isinstance(d_impl, types.DataCPP):
|
||||||
|
raise ValueError(f'Expected DataCPP impl, got {type(d_impl)}')
|
||||||
|
|
||||||
|
mj_data_list = d_impl._data # pylint: disable=protected-access
|
||||||
|
|
||||||
|
if batch_size > len(mj_data_list):
|
||||||
|
raise ValueError(
|
||||||
|
f'Batch size {batch_size} exceeds number of underlying MjData objects '
|
||||||
|
f'({len(mj_data_list)}). Cannot copy data.'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify that the underlying MjData state matches the mjx.Data state
|
||||||
|
# Ideally we'd use mj_getState and get_state here but that requires an
|
||||||
|
# mjx.Model which we don't have access to in this function.
|
||||||
|
fields_to_check = ['qpos', 'qvel', 'act', 'mocap_pos', 'mocap_quat']
|
||||||
|
for i in range(batch_size):
|
||||||
|
d_i = jax.tree_util.tree_map(lambda x, i=i: x[i], d) if batched else d
|
||||||
|
src_data = mj_data_list[i]
|
||||||
|
|
||||||
|
for field in fields_to_check:
|
||||||
|
mj_value = getattr(src_data, field)
|
||||||
|
mjx_value = np.asarray(getattr(d_i, field))
|
||||||
|
if not np.allclose(mj_value, mjx_value):
|
||||||
|
raise ValueError(
|
||||||
|
f'State mismatch at batch index {i}, field {field}: underlying '
|
||||||
|
'MjData does not match mjx.Data. The mjx.Data may have been '
|
||||||
|
'modified without updating the underlying MjData.'
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(batch_size):
|
||||||
|
result_i = result[i] if batched else result
|
||||||
|
src_data = mj_data_list[i]
|
||||||
|
mujoco.mj_copyData(result_i, m, src_data)
|
||||||
|
|
||||||
|
|
||||||
def get_data_into(
|
def get_data_into(
|
||||||
result: Union[mujoco.MjData, List[mujoco.MjData]],
|
result: Union[mujoco.MjData, List[mujoco.MjData]],
|
||||||
m: mujoco.MjModel,
|
m: mujoco.MjModel,
|
||||||
@@ -1604,6 +1692,9 @@ def get_data_into(
|
|||||||
# TODO(stunya): Split out _get_data_into once codepaths diverge enough.
|
# TODO(stunya): Split out _get_data_into once codepaths diverge enough.
|
||||||
return _get_data_into(result, m, d)
|
return _get_data_into(result, m, d)
|
||||||
|
|
||||||
|
if d.impl == types.Impl.CPP:
|
||||||
|
return _get_data_into_cpp(result, m, d)
|
||||||
|
|
||||||
if d.impl == types.Impl.WARP:
|
if d.impl == types.Impl.WARP:
|
||||||
return _get_data_into_warp(result, m, d)
|
return _get_data_into_warp(result, m, d)
|
||||||
|
|
||||||
|
|||||||
@@ -371,7 +371,7 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
self.tempdir = tempfile.TemporaryDirectory()
|
self.tempdir = tempfile.TemporaryDirectory()
|
||||||
wp.config.kernel_cache_dir = self.tempdir.name
|
wp.config.kernel_cache_dir = self.tempdir.name
|
||||||
|
|
||||||
@parameterized.parameters('jax', 'c')
|
@parameterized.parameters('jax', 'c', 'cpp')
|
||||||
def test_make_data(self, impl: str):
|
def test_make_data(self, impl: str):
|
||||||
"""Test that make_data returns the correct shapes."""
|
"""Test that make_data returns the correct shapes."""
|
||||||
m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONVEX_OBJECTS)
|
m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONVEX_OBJECTS)
|
||||||
@@ -384,7 +384,7 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
nv = 19
|
nv = 19
|
||||||
nefc = 185
|
nefc = 185
|
||||||
|
|
||||||
self.assertEqual(d._impl.nefc, nefc)
|
# Check public Data fields that exist for all impls
|
||||||
self.assertEqual(d.qpos.shape, (nq,))
|
self.assertEqual(d.qpos.shape, (nq,))
|
||||||
self.assertEqual(d.qvel.shape, (nv,))
|
self.assertEqual(d.qvel.shape, (nv,))
|
||||||
self.assertEqual(d.act.shape, (0,))
|
self.assertEqual(d.act.shape, (0,))
|
||||||
@@ -406,17 +406,20 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
self.assertEqual(d.geom_xmat.shape, (6, 3, 3))
|
self.assertEqual(d.geom_xmat.shape, (6, 3, 3))
|
||||||
self.assertEqual(d.subtree_com.shape, (nbody, 3))
|
self.assertEqual(d.subtree_com.shape, (nbody, 3))
|
||||||
self.assertEqual(d.cdof.shape, (nv, 6))
|
self.assertEqual(d.cdof.shape, (nv, 6))
|
||||||
|
self.assertEqual(d.actuator_length.shape, (1,))
|
||||||
|
|
||||||
|
if impl == 'cpp':
|
||||||
|
self.assertTrue(hasattr(d._impl, 'pointer_lo'))
|
||||||
|
self.assertTrue(hasattr(d._impl, 'pointer_hi'))
|
||||||
|
return # cpp does not populate other _impl fields
|
||||||
|
|
||||||
|
self.assertEqual(d._impl.nefc, nefc)
|
||||||
self.assertEqual(d._impl.cinert.shape, (nbody, 10))
|
self.assertEqual(d._impl.cinert.shape, (nbody, 10))
|
||||||
self.assertEqual(d._impl.crb.shape, (nbody, 10))
|
self.assertEqual(d._impl.crb.shape, (nbody, 10))
|
||||||
self.assertEqual(d.actuator_length.shape, (1,))
|
|
||||||
if impl == 'jax':
|
if impl == 'jax':
|
||||||
self.assertEqual(d._impl.actuator_moment.shape, (1, nv))
|
self.assertEqual(d._impl.actuator_moment.shape, (1, nv))
|
||||||
elif impl == 'c':
|
elif impl == 'c':
|
||||||
self.assertEqual(d._impl.actuator_moment.shape, (m.nJmom,))
|
self.assertEqual(d._impl.actuator_moment.shape, (m.nJmom,))
|
||||||
elif impl == 'cpp':
|
|
||||||
self.assertTrue(hasattr(d._impl, 'pointer_lo'))
|
|
||||||
self.assertTrue(hasattr(d._impl, 'pointer_hi'))
|
|
||||||
return # cpp does not populate other fields in _impl
|
|
||||||
self.assertEqual(d._impl.contact.dist.shape, (ncon,))
|
self.assertEqual(d._impl.contact.dist.shape, (ncon,))
|
||||||
self.assertEqual(d._impl.contact.pos.shape, (ncon, 3))
|
self.assertEqual(d._impl.contact.pos.shape, (ncon, 3))
|
||||||
self.assertEqual(d._impl.contact.frame.shape, (ncon, 3, 3))
|
self.assertEqual(d._impl.contact.frame.shape, (ncon, 3, 3))
|
||||||
@@ -472,7 +475,7 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
self.assertEqual(d._impl.contact__dist.shape[0], 9)
|
self.assertEqual(d._impl.contact__dist.shape[0], 9)
|
||||||
self.assertEqual(d._impl.efc__pos.shape[0], 23)
|
self.assertEqual(d._impl.efc__pos.shape[0], 23)
|
||||||
|
|
||||||
@parameterized.parameters('jax', 'c')
|
@parameterized.parameters('jax', 'c', 'cpp')
|
||||||
def test_put_data(self, impl: str):
|
def test_put_data(self, impl: str):
|
||||||
"""Test that put_data puts the correct data for dense and sparse."""
|
"""Test that put_data puts the correct data for dense and sparse."""
|
||||||
m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS)
|
m = mujoco.MjModel.from_xml_string(_MULTIPLE_CONSTRAINTS)
|
||||||
@@ -699,7 +702,7 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
self.assertEqual(ds[0].ncon, 1)
|
self.assertEqual(ds[0].ncon, 1)
|
||||||
self.assertEqual(ds[1].ncon, 0)
|
self.assertEqual(ds[1].ncon, 0)
|
||||||
|
|
||||||
@parameterized.parameters('jax', 'c')
|
@parameterized.parameters('jax', 'c', 'cpp')
|
||||||
def test_get_data_into(self, impl):
|
def test_get_data_into(self, impl):
|
||||||
"""Test that get_data_into correctly populates an MjData."""
|
"""Test that get_data_into correctly populates an MjData."""
|
||||||
|
|
||||||
@@ -809,7 +812,7 @@ class DataIOTest(parameterized.TestCase):
|
|||||||
|
|
||||||
_ = jax.tree.map_with_path(check_ndim, dx)
|
_ = jax.tree.map_with_path(check_ndim, dx)
|
||||||
|
|
||||||
@parameterized.parameters('jax', 'warp')
|
@parameterized.parameters('jax', 'warp', 'cpp')
|
||||||
def test_data_slice(self, impl):
|
def test_data_slice(self, impl):
|
||||||
"""Tests that slice on Data works as expected."""
|
"""Tests that slice on Data works as expected."""
|
||||||
if impl == 'warp' and not mjxw.WARP_INSTALLED:
|
if impl == 'warp' and not mjxw.WARP_INSTALLED:
|
||||||
|
|||||||
Reference in New Issue
Block a user