Skip to content

Commit 1f6cf40

Browse files
thowellcopybara-github
authored andcommitted
Import google-deepmind/mujoco_warp from GitHub.
PiperOrigin-RevId: 941234486 Change-Id: Ie5d8cc0c9975e66f9a81bb6a86e33f640a5d19e3
1 parent bb80b55 commit 1f6cf40

37 files changed

Lines changed: 10283 additions & 7082 deletions

mjx/mujoco/mjx/_src/io.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,6 @@ def f(leaf):
174174

175175
def _wp_to_np_type(wp_field: Any, name: str = '') -> Any:
176176
"""Converts a warp type to an MJX compatible numpy type."""
177-
if hasattr(wp_field, '_is_batched'):
178-
wp_field.strides = wp_field.strides[1:]
179-
wp_field.shape = wp_field.shape[1:]
180177
# warp scalars
181178
wp_dtype = type(wp_field)
182179
if wp_dtype in wp._src.types.warp_type_to_np_dtype:
@@ -202,7 +199,13 @@ def _wp_to_np_type(wp_field: Any, name: str = '') -> Any:
202199
wp_field[0], mjwp_types.TileSet
203200
):
204201
return tuple(
205-
mjxw.types.TileSet(wp_field[i].adr.numpy(), wp_field[i].size)
202+
mjxw.types.TileSet(
203+
wp_field[i].adr.numpy(),
204+
wp_field[i].size,
205+
wp_field[i].elemid.numpy()
206+
if wp_field[i].elemid is not None
207+
else np.zeros(0, dtype=np.int32),
208+
)
206209
for i in range(len(wp_field))
207210
)
208211
if isinstance(wp_field, mjwp_types.BlockDim):
@@ -272,11 +275,15 @@ def _put_option(
272275

273276

274277
if impl == types.Impl.WARP:
275-
if not mjxw.mjwp_io.ENABLE_ISLANDS:
276-
fields['disableflags'] = types.DisableBit(
277-
fields['disableflags'] | mjwp_types.DisableBit.ISLAND
278-
)
279-
impl_fields = {k: _wp_to_np_type(v) for k, v in impl_fields.items()}
278+
impl_fields = {
279+
k: (
280+
v_np.reshape(v_np.shape[1:])
281+
if isinstance(v_np := _wp_to_np_type(v, k), np.ndarray)
282+
and mjxw.types._BATCH_DIM['Option'].get(k, False) # pylint: disable=protected-access
283+
else v_np
284+
)
285+
for k, v in impl_fields.items()
286+
}
280287
return types.Option(**fields, _impl=mjxw.types.OptionWarp(**impl_fields))
281288

282289
raise NotImplementedError(f'Unsupported implementation: {impl}')
@@ -460,6 +467,8 @@ def _put_model_warp(
460467
if not hasattr(mw, k) or k in ('stat', 'opt'):
461468
continue
462469
field = _wp_to_np_type(getattr(mw, k), k)
470+
if mjxw.types._BATCH_DIM['Model'].get(k, False): # pylint: disable=protected-access
471+
field = field.reshape(field.shape[1:])
463472
if k == 'geom_dataid' and field.ndim > 1:
464473
# Batched geom_dataid is not supported in MJX.
465474
field = field[0]
@@ -468,6 +477,8 @@ def _put_model_warp(
468477
impl_fields = {}
469478
for k in mjxw.types.ModelWarp.__annotations__.keys():
470479
field = _wp_to_np_type(getattr(mw, k), k)
480+
if mjxw.types._BATCH_DIM['Model'].get(k, False): # pylint: disable=protected-access
481+
field = field.reshape(field.shape[1:])
471482
impl_fields[k] = field
472483

473484
model = types.Model(
@@ -1298,18 +1309,19 @@ def _get_data_into_warp(
12981309
'ten_J',
12991310
'flexedge_J',
13001311
'M',
1312+
'map_efc2iefc', 'map_iefc2efc'
13011313
):
13021314
continue
13031315
if field.name.startswith('efc_'):
13041316
continue
13051317

1318+
# Skip island fields; host MjData arena memory cannot be reallocated from Python if not stepped.
1319+
if field.name.startswith('island_'):
1320+
continue
1321+
13061322
if isinstance(value, np.ndarray) and value.shape:
13071323
result_field = getattr(result_i, field.name)
13081324
if result_field.shape != value.shape:
1309-
# When ENABLE_ISLANDS is False, mujoco_warp allocates island fields with width 0,
1310-
# while the host MjData sizes to nv/ntree. Skip to prevent mismatch.
1311-
if value.size == 0 and not mjxw.mjwp_io.ENABLE_ISLANDS:
1312-
continue
13131325
raise ValueError(
13141326
f'Input field {field.name} has shape {value.shape}, but output'
13151327
f' has shape {result_field.shape}'

mjx/mujoco/mjx/_src/io_test.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,15 @@ def test_put_data(self, impl: str):
550550
elif impl == 'warp':
551551
qm = np.zeros((m.nv, m.nv), dtype=np.float64)
552552
mujoco.mju_sym2dense(qm, d.M, m.M_rownnz, m.M_rowadr, m.M_colind)
553-
np.testing.assert_allclose(dx._impl.M[:m.nv, :m.nv], qm)
553+
warp_M = np.zeros((m.nv, m.nv))
554+
mujoco.mju_sym2dense(
555+
warp_M,
556+
np.array(dx._impl.M),
557+
m.M_rownnz,
558+
m.M_rowadr,
559+
m.M_colind,
560+
)
561+
np.testing.assert_allclose(warp_M, qm)
554562
# TODO(taylorhowell): test efc__J
555563
np.testing.assert_allclose(dx._impl.efc__aref[:3], d.efc_aref[:3])
556564

@@ -811,12 +819,6 @@ def test_get_data_into_warp(self):
811819
dx = mjx.make_data(m, impl='warp')
812820
mjx.get_data_into(d, mx, dx)
813821

814-
# Island data is not populated when ENABLE_ISLANDS is False, so the host
815-
# MjData island fields should be left at their default (nv,)/(ntree,)
816-
# shapes rather than triggering a shape mismatch.
817-
self.assertEqual(d.dof_island.shape, (m.nv,))
818-
self.assertEqual(d.tree_island.shape, (m.ntree,))
819-
820822
@parameterized.parameters(('jax',))
821823
def test_get_data_into_wrong_shape(self, impl):
822824
"""Tests that get_data_into throwsif input and output shapes don't match."""

mjx/mujoco/mjx/codegen/generate_warp_shim.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,12 @@ def _jax_shim_fn(
327327
f'Unknown param source: {mjwarp_field_info[arg].param_source}'
328328
)
329329

330-
if arg in field_usage.data_out_fields:
330+
# Only treat as output if it is a Warp array (excludes scalars like
331+
# naconmax).
332+
if (
333+
arg in field_usage.data_out_fields
334+
and 'array' in mjwarp_field_info[arg].expected_type
335+
):
331336
# all out fields are in_out, since JAX already allocated them
332337
in_out_argnames.append(f"'{arg}'")
333338
num_outputs += 1
@@ -344,7 +349,11 @@ def _jax_shim_fn(
344349
if field_usage.render_context_in_caller:
345350
jax_args.append('ctx.key')
346351

347-
needs_dummy_output = not field_usage.data_out_fields
352+
# If there are no Warp array outputs, we need a dummy output for JAX FFI.
353+
needs_dummy_output = not any(
354+
'array' in mjwarp_field_info[f].expected_type
355+
for f in field_usage.data_out_fields
356+
)
348357
if needs_dummy_output and fn_name != 'render':
349358
num_outputs = 1
350359
if field_usage.render_context_in_caller:

mjx/mujoco/mjx/codegen/generate_warp_types.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,13 @@ def _get_annotations_recursive(
181181

182182

183183
def _build_new_class_body_ast(
184-
keys: Set[str],
184+
keys: typing.Iterable[str],
185185
cls_name: str,
186186
target_annotations: Dict[str, Any],
187+
defaults: Optional[Dict[str, Any]] = None,
187188
shape_property: Optional[str] = None,
188189
add_docstring: bool = True,
190+
sort_keys: bool = True,
189191
) -> List[ast.AST]:
190192
"""Builds the list of AST nodes for the new class body."""
191193
new_body_nodes: List[ast.AST] = []
@@ -194,16 +196,29 @@ def _build_new_class_body_ast(
194196
docstring = f'Derived fields from {cls_name}.'
195197
new_body_nodes.append(ast.Expr(value=ast.Constant(value=docstring)))
196198

197-
# Sort keys alphabetically before creating AST nodes
198-
sorted_keys = sorted(list(keys))
199-
for key in sorted_keys:
199+
if sort_keys:
200+
maybe_sorted_keys = sorted(list(keys))
201+
else:
202+
maybe_sorted_keys = list(keys)
203+
204+
for key in maybe_sorted_keys:
200205
annotation_node = _get_target_annotation_node(key, target_annotations)
201206

207+
value_node = None
208+
if defaults and key in defaults:
209+
value_node = ast.Constant(value=defaults[key])
210+
211+
if value_node and value_node.value is None:
212+
annotation_node = _ast_parse_type(
213+
f'typing.Optional[{ast.unparse(annotation_node)}]'
214+
)
215+
202216
new_body_nodes.append(
203217
ast.AnnAssign(
204218
target=ast.Name(id=key, ctx=ast.Store()),
205219
annotation=annotation_node,
206-
simple=1, # No value assignment
220+
value=value_node,
221+
simple=1,
207222
)
208223
)
209224

@@ -316,11 +331,21 @@ def __hash__(self) -> int:
316331

317332

318333
def write_nested_dataclass(target_fpath: epath.Path, cls: Any):
334+
fields = dataclasses.fields(cls)
335+
keys = [f.name for f in fields]
336+
defaults = {
337+
f.name: f.default
338+
for f in fields
339+
if f.default is not dataclasses.MISSING
340+
}
341+
319342
new_class_body = _build_new_class_body_ast(
320-
set(cls.__annotations__.keys()),
343+
keys,
321344
cls.__name__,
322345
dict(cls.__annotations__),
346+
defaults=defaults,
323347
add_docstring=False,
348+
sort_keys=False,
324349
)
325350
cls_str = '\n'.join([' ' + ast.unparse(node) for node in new_class_body])
326351
cls_str = cls_str.replace('jax.Array', 'np.ndarray')

mjx/mujoco/mjx/codegen/trace.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ def _get_imported_module_fpaths(fpath: epath.Path) -> Dict[str, str]:
6363

6464
all_resolved_fpaths = {}
6565
for fully_qualified_name, alias in all_imported_names:
66+
# only trace mujoco and mujoco warp
67+
if not (fully_qualified_name.startswith('mujoco_warp') or
68+
fully_qualified_name.startswith('mujoco')):
69+
continue
6670
fpath = _resolve_module_name_to_fpath(fully_qualified_name)
6771
if fpath:
6872
all_resolved_fpaths[alias] = fpath
@@ -181,9 +185,17 @@ def visit_Call(self, node: ast.Call):
181185
self.add_field_usage(in_node, False)
182186
return
183187

184-
for arg in node.args:
185-
if isinstance(arg, ast.Attribute):
186-
self.add_field_usage(arg, is_output=True)
188+
# do not trace array creation
189+
is_input_only_wp_fn = (
190+
len(parts) == 2
191+
and parts[0] == 'wp'
192+
and parts[1] in ('empty', 'zeros', 'array', 'clone')
193+
)
194+
195+
if not is_input_only_wp_fn:
196+
for arg in node.args:
197+
if isinstance(arg, ast.Attribute):
198+
self.add_field_usage(arg, is_output=True)
187199

188200
called_fn_name = '.'.join(parts[1:])
189201
key = (hash(self._current_fpath), called_fn_name)

mjx/mujoco/mjx/third_party/mujoco_warp/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
from mujoco.mjx.third_party.mujoco_warp._src.io import set_const as set_const
6262
from mujoco.mjx.third_party.mujoco_warp._src.io import set_const_0 as set_const_0
6363
from mujoco.mjx.third_party.mujoco_warp._src.io import set_const_fixed as set_const_fixed
64+
from mujoco.mjx.third_party.mujoco_warp._src.io import set_const_spring as set_const_spring
6465
from mujoco.mjx.third_party.mujoco_warp._src.io import set_length_range as set_length_range
6566
from mujoco.mjx.third_party.mujoco_warp._src.island import island as island
6667
from mujoco.mjx.third_party.mujoco_warp._src.passive import passive as passive

mjx/mujoco/mjx/third_party/mujoco_warp/_src/cli.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from etils import epath
2626

2727
import mujoco.mjx.third_party.mujoco_warp as mjw
28-
from mujoco.mjx.third_party.mujoco_warp._src import io
2928
from mujoco.mjx.third_party.mujoco_warp._src import warp_util
3029
from mujoco.mjx.third_party.mujoco_warp._src.io import load_trajectory
3130
from mujoco.mjx.third_party.mujoco_warp._src.io import override_model
@@ -43,12 +42,12 @@
4342
EVENT_TRACE = flags.DEFINE_bool("event_trace", False, "print an event trace report")
4443
NOISE_STD = flags.DEFINE_float("noise_std", 0.01, "add noise to ctrl signal (standard deviation)")
4544
NOISE_RATE = flags.DEFINE_float("noise_rate", 0.1, "add noise to ctrl signal (noise rate)")
46-
ENABLE_ISLANDS = flags.DEFINE_bool(
47-
"enable_islands",
48-
False,
49-
"Enable constraint islands solver",
50-
)
45+
NVMAX = flags.DEFINE_integer("nvmax", None, "maximum active DOFs per world")
46+
5147

48+
INIT_ASLEEP = flags.DEFINE_bool(
49+
"init_asleep", False, "initialize all trees as asleep before simulation (requires sleep enabled)"
50+
)
5251

5352
DEVICE = flags.DEFINE_string("device", None, "override the default Warp device")
5453
REPLAY = flags.DEFINE_string("replay", None, "NPZ file with ctrl sequence to replay")
@@ -141,8 +140,6 @@ def init_structs(
141140
fn: Callable[..., None], mjm: mujoco.MjModel
142141
) -> Tuple[mjw.Model, mjw.Data, mjw.RenderContext | None, list[np.ndarray] | None]:
143142
"""Initialize device structs."""
144-
io.ENABLE_ISLANDS = ENABLE_ISLANDS.value
145-
146143
mjd = mujoco.MjData(mjm)
147144
ctrls = None
148145
if REPLAY.value:
@@ -158,8 +155,18 @@ def init_structs(
158155
m = mjw.put_model(mjm)
159156
if OVERRIDE.value:
160157
override_model(m, OVERRIDE.value)
158+
if INIT_ASLEEP.value:
159+
mjd.tree_asleep[:] = np.arange(mjm.ntree, dtype=np.int32)
160+
161161
d = mjw.put_data(
162-
mjm, mjd, nworld=NWORLD.value, nconmax=NCONMAX.value, njmax=NJMAX.value, njmax_nnz=NJMAX_NNZ.value, nccdmax=NCCDMAX.value
162+
mjm,
163+
mjd,
164+
nworld=NWORLD.value,
165+
nconmax=NCONMAX.value,
166+
njmax=NJMAX.value,
167+
njmax_nnz=NJMAX_NNZ.value,
168+
nccdmax=NCCDMAX.value,
169+
nvmax=NVMAX.value,
163170
)
164171

165172
if mjw.RenderContext not in get_type_hints(fn).values():

0 commit comments

Comments
 (0)