Add batch support to MJX data bind.

PiperOrigin-RevId: 729033713
Change-Id: I331317a54a7882dc67bf329da8aeaff7246754e4
This commit is contained in:
Alessio Quaglino
2025-02-20 03:09:42 -08:00
committed by Copybara-Service
parent 19624aef9d
commit 41c2918819
3 changed files with 79 additions and 7 deletions
+24 -5
View File
@@ -19,6 +19,7 @@ from typing import Optional, Tuple, Union
import jax import jax
from jax import numpy as jp from jax import numpy as jp
import mujoco import mujoco
from mujoco.introspect import mjxmacro
from mujoco.mjx._src import math from mujoco.mjx._src import math
from mujoco.mjx._src import scan from mujoco.mjx._src import scan
# pylint: disable=g-importing-member # pylint: disable=g-importing-member
@@ -368,8 +369,17 @@ class BindModel(object):
else: else:
self.id = ids self.id = ids
def _slice(self, name: str, idx: Union[int, slice, Sequence[int]]):
_, expected_dim = mjxmacro.MJMODEL[name]
var = getattr(self.model, name)
if expected_dim == '1':
return var[..., idx]
elif expected_dim == '9':
return var[..., idx, :, :]
return var[..., idx, :]
def __getattr__(self, name: str): def __getattr__(self, name: str):
return getattr(self.model, self.prefix + name)[self.id, ...] return self._slice(self.prefix + name, self.id)
def _bind_model( def _bind_model(
@@ -453,6 +463,15 @@ class BindData(object):
else: else:
return self.prefix + name return self.prefix + name
def _slice(self, name: str, idx: Union[int, slice, Sequence[int]]):
_, expected_dim = mjxmacro.MJDATA[name]
var = getattr(self.data, name)
if expected_dim == '1':
return var[..., idx]
elif expected_dim == '9':
return var[..., idx, :, :]
return var[..., idx, :]
def __getattr__(self, name: str): def __getattr__(self, name: str):
if name in ('sensordata', 'qpos', 'qvel', 'qacc'): if name in ('sensordata', 'qpos', 'qvel', 'qacc'):
adr = num = 0 adr = num = 0
@@ -471,12 +490,12 @@ class BindData(object):
idx = [] idx = []
for a, n in zip(adr, num): for a, n in zip(adr, num):
idx.extend(a + j for j in range(n)) idx.extend(a + j for j in range(n))
return getattr(self.data, self.__getname(name))[idx, ...] return self._slice(self.__getname(name), idx)
elif num > 1: elif num > 1:
return getattr(self.data, self.__getname(name))[adr : adr + num, ...] return self._slice(self.__getname(name), slice(adr, adr + num))
else: else:
return getattr(self.data, self.__getname(name))[adr, ...] return self._slice(self.__getname(name), adr)
return getattr(self.data, self.__getname(name))[self.id, ...] return self._slice(self.__getname(name), self.id)
def set(self, name: str, value: jax.Array) -> Data: def set(self, name: str, value: jax.Array) -> Data:
"""Set the value of an array in an MJX Data.""" """Set the value of an array in an MJX Data."""
+12 -2
View File
@@ -305,11 +305,11 @@ class SupportTest(parameterized.TestCase):
): ):
print(dx.bind(mx, s.geoms).ctrl) print(dx.bind(mx, s.geoms).ctrl)
with self.assertRaises( with self.assertRaises(
AttributeError, msg='ctrl is not available for this type' KeyError, msg='actuator_actuator_ctrl'
): ):
print(dx.bind(mx, s.actuators).actuator_ctrl) print(dx.bind(mx, s.actuators).actuator_ctrl)
with self.assertRaises( with self.assertRaises(
AttributeError, msg='ctrl is not available for this type' AttributeError, msg='actuator_actuator_ctrl'
): ):
print(dx.bind(mx, s.actuators).set('actuator_ctrl', [1, 2, 3])) print(dx.bind(mx, s.actuators).set('actuator_ctrl', [1, 2, 3]))
with self.assertRaises( with self.assertRaises(
@@ -323,6 +323,16 @@ class SupportTest(parameterized.TestCase):
s.geoms[0].name = 'invalid_geom_name' s.geoms[0].name = 'invalid_geom_name'
print(mx.bind(s.geoms).pos) print(mx.bind(s.geoms).pos)
# test batched data
batch_size = 16
ds = [d for _ in range(batch_size)]
vdx = jax.vmap(lambda xpos: dx.replace(xpos=xpos))(
jp.array([d.xpos for d in ds], device=jax.devices('cpu')[0]))
for i in range(m.nbody):
np.testing.assert_array_equal(
vdx.bind(mx, s.bodies[i]).xpos, [d.xpos[i, :]] * batch_size
)
_CONTACTS = """ _CONTACTS = """
<mujoco> <mujoco>
<worldbody> <worldbody>
+43
View File
@@ -0,0 +1,43 @@
# 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.
# ==============================================================================
"""Generate X macros for Mujoco structs."""
from . import structs
MJMODEL_S = structs.STRUCTS['mjModel']
MJDATA_S = structs.STRUCTS['mjData']
MJMODEL = dict()
MJDATA = dict()
for field in MJMODEL_S.fields:
if not isinstance(field, structs.StructFieldDecl):
continue
if field.array_extent is None:
continue
if len(field.array_extent) == 1:
MJMODEL[field.name] = (field.array_extent[0], '1')
else:
MJMODEL[field.name] = (field.array_extent[0], str(field.array_extent[1]))
for field in MJDATA_S.fields:
if not isinstance(field, structs.StructFieldDecl):
continue
if field.array_extent is None:
continue
if len(field.array_extent) == 1:
MJDATA[field.name] = (field.array_extent[0], '1')
else:
MJDATA[field.name] = (field.array_extent[0], str(field.array_extent[1]))