diff --git a/doc/changelog.rst b/doc/changelog.rst index e448e8bf..25baeac6 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -6,11 +6,6 @@ Changelog Upcoming version (not yet released) ----------------------------------- -General -^^^^^^^ -- Removed id attribute from :ref:`mjSpec` objects in Python bindings. Using ids is error prone in scenarios of repeated - attachment and detachment. Python users are encouraged to use names for unique identification of model elements. - MJX ^^^ - Added muscle actuators. @@ -18,6 +13,8 @@ MJX Python bindings ^^^^^^^^^^^^^^^ - Provide prebuilt wheels for Python 3.13. +- Added ``bind`` method and removed id attribute from :ref:`mjSpec` objects. Using ids is error prone in scenarios of repeated attachment and + detachment. Python users are encouraged to use names for unique identification of model elements. Bug fixes ^^^^^^^^^ diff --git a/doc/python.rst b/doc/python.rst index d5e4a165..3af46b6d 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -585,16 +585,17 @@ Model Editing includes a reimplementation of the ``PyMJCF`` example in the ``dm_control`` `tutorial notebook `__. -``PyMJCF`` provides a notion of "binding", giving access to :ref:`mjModel` and :ref:`mjData` values via the constructing -elements. In the native API, this is done with object ids. For example, say we have multiple geoms containing the string -"torso" in their name. We want to get their Cartesian positions in the XY plane from ``mjData``. This can be done as -follows: +``PyMJCF`` provides a notion of "binding", giving access to :ref:`mjModel` and :ref:`mjData` values via a helper class. +In the native API, the helper class is not needed, so it is possible to directly bind an ``mjs`` object to +:ref:`mjModel` and :ref:`mjData`. This requires the objects to have a non-empty name. For example, say we have multiple +geoms containing the string "torso" in their name. We want to get their Cartesian positions in the XY plane from +``mjData``. This can be done as follows: .. code-block:: python - torsos = [geom.id for geom in spec.geoms if 'torso' in geom.name] - pos_x = data.geom_xpos[torsos, 0] - pos_y = data.geom_xpos[torsos, 1] + torsos = [data.bind(geom) for geom in spec.geoms if 'torso' in geom.name] + pos_x = [torso.xpos[0] for torso in torsos] + pos_y = [torso.xpos[1] for torso in torsos] Notes ----- diff --git a/python/mjspec.ipynb b/python/mjspec.ipynb index 886032f4..f6fa9fe9 100644 --- a/python/mjspec.ipynb +++ b/python/mjspec.ipynb @@ -457,9 +457,9 @@ "pos_x = []\n", "pos_y = []\n", "geoms = arena.worldbody.find_all(mj.mjtObj.mjOBJ_GEOM)\n", - "torsos_data = [data.geom(geom.name) for geom in geoms if 'torso' in geom.name]\n", - "torsos_model = [model.geom(geom.name) for geom in geoms if 'torso' in geom.name]\n", - "actuators = [data.actuator(actuator.name) for actuator in arena.actuators]\n", + "torsos_data = [data.bind(geom) for geom in geoms if 'torso' in geom.name]\n", + "torsos_model = [model.bind(geom) for geom in geoms if 'torso' in geom.name]\n", + "actuators = [data.bind(actuator) for actuator in arena.actuators]\n", "\n", "# Control signal frequency, phase, amplitude.\n", "freq = 5\n", diff --git a/python/mujoco/indexer_xmacro.h b/python/mujoco/indexer_xmacro.h index 6f9d2f9d..2b2b8a43 100644 --- a/python/mujoco/indexer_xmacro.h +++ b/python/mujoco/indexer_xmacro.h @@ -307,6 +307,28 @@ XGROUP( MjModelTupleViews, tuple, ntuple, MJMODEL_TUPLE ) \ XGROUP( MjModelKeyframeViews, key, nkey, MJMODEL_KEYFRAME ) +#define MJMODEL_BIND_GROUPS \ + XGROUP( mjsActuator, actuator) \ + XGROUP( mjsBody, body ) \ + XGROUP( mjsCamera, cam ) \ + XGROUP( mjsEquality, eq ) \ + XGROUP( mjsExclude, exclude ) \ + XGROUP( mjsGeom, geom ) \ + XGROUP( mjsHField, hfield ) \ + XGROUP( mjsJoint, jnt ) \ + XGROUP( mjsLight, light ) \ + XGROUP( mjsMaterial, mat ) \ + XGROUP( mjsMesh, mesh ) \ + XGROUP( mjsNumeric, numeric ) \ + XGROUP( mjsPair, pair ) \ + XGROUP( mjsSensor, sensor ) \ + XGROUP( mjsSite, site ) \ + XGROUP( mjsSkin, skin ) \ + XGROUP( mjsTendon, tendon ) \ + XGROUP( mjsTexture, tex ) \ + XGROUP( mjsTuple, tuple ) \ + XGROUP( mjsKey, key ) + #define MJMODEL_VIEW_GROUPS_ALTNAMES \ XGROUP( cam, camera, MJMODEL_CAMERA ) \ XGROUP( eq, equality, MJMODEL_EQUALITY ) \ @@ -399,6 +421,17 @@ XGROUP( MjDataSiteViews, site, nsite, MJDATA_SITE ) \ XGROUP( MjDataTendonViews, tendon, ntendon, MJDATA_TENDON ) +#define MJDATA_BIND_GROUPS \ + XGROUP( mjsActuator, actuator) \ + XGROUP( mjsBody, body ) \ + XGROUP( mjsCamera, cam ) \ + XGROUP( mjsGeom, geom ) \ + XGROUP( mjsJoint, jnt ) \ + XGROUP( mjsLight, light ) \ + XGROUP( mjsSensor, sensor ) \ + XGROUP( mjsSite, site ) \ + XGROUP( mjsTendon, tendon ) + #define MJDATA_VIEW_GROUPS_ALTNAMES \ XGROUP( cam, camera, MJDATA_CAMERA ) \ XGROUP( jnt, joint, MJDATA_JOINT ) \ diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 42686f1b..7d42d3d0 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -94,9 +94,12 @@ class SpecsTest(absltest.TestCase): # Compile the spec and check for expected values in the model. model = spec.compile() + data = mujoco.MjData(model) + mujoco.mj_forward(model, data) self.assertEqual(model.nbody, 2) # 2 bodies, including the world body - np.testing.assert_array_equal(model.body('baz').pos, [1, 2, 3]) - np.testing.assert_array_equal(model.body('baz').quat, [0, 1, 0, 0]) + np.testing.assert_array_equal(model.bind(body).pos, [1, 2, 3]) + np.testing.assert_array_equal(model.bind(body).quat, [0, 1, 0, 0]) + np.testing.assert_array_equal(data.bind(body).xpos, [1, 2, 3]) self.assertEqual(model.nsite, 1) self.assertEqual(model.nuser_site, 6) np.testing.assert_array_equal(model.site_user[0], [1, 2, 3, 4, 5, 6]) diff --git a/python/mujoco/structs.cc b/python/mujoco/structs.cc index 56a6c8ee..53d1a72b 100644 --- a/python/mujoco/structs.cc +++ b/python/mujoco/structs.cc @@ -1716,6 +1716,18 @@ This is useful for example when the MJB is not available as a file on disk.)")); MJMODEL_VIEW_GROUPS #undef XGROUP +#define XGROUP(spectype, field) \ + mjModel.def( \ + "bind", \ + [](MjModelWrapper& m, spectype& spec) -> auto& { \ + return m.indexer().field##_by_name(mjs_getString(spec.name)); \ + }, \ + py::return_value_policy::reference_internal, \ + py::arg_v("spec", py::none())); + + MJMODEL_BIND_GROUPS +#undef XGROUP + #define XGROUP(field, altname, FIELD_XMACROS) \ mjModel.def( \ #altname, \ @@ -2050,6 +2062,18 @@ This is useful for example when the MJB is not available as a file on disk.)")); MJDATA_VIEW_GROUPS #undef XGROUP +#define XGROUP(spectype, field) \ + mjData.def( \ + "bind", \ + [](MjDataWrapper& d, spectype& spec) -> auto& { \ + return d.indexer().field##_by_name(mjs_getString(spec.name)); \ + }, \ + py::return_value_policy::reference_internal, \ + py::arg_v("spec", py::none())); + + MJDATA_BIND_GROUPS +#undef XGROUP + #define XGROUP(field, altname, FIELD_XMACROS) \ mjData.def( \ #altname, \