Improve mjSpec documentation. Fixes #2074.

PiperOrigin-RevId: 692251710
Change-Id: Ia46bbbe5b7eaa890433b38fed66824f716cc9ea0
This commit is contained in:
Yuval Tassa
2024-11-01 12:05:26 -07:00
committed by Copybara-Service
parent a51f346059
commit b6037d1759
4 changed files with 196 additions and 71 deletions
+3
View File
@@ -7,6 +7,9 @@ Upcoming version (not yet released)
General
^^^^^^^
- The :doc:`Model Editing<programming/modeledit>` framework afforded by :ref:`mjSpec`, introduced in 3.2.0 as an
in-development feature, is now stable and recommended for general use.
- The global compiler flag ``exactmeshinertia`` has been removed and replaced with the mesh-specific
:ref:`inertia<asset-mesh-inertia>` attribute.
- The not-useful ``convexhull`` compiler option (to disable computation of mesh convex hulls) has been removed.
+13 -13
View File
@@ -136,21 +136,21 @@ the model. We start with an example.
.. code-block:: xml
<mujoco>
<default class="main">
<geom rgba="1 0 0 1"/>
<default class="sub">
<geom rgba="0 1 0 1"/>
</default>
<default class="main">
<geom rgba="1 0 0 1"/>
<default class="sub">
<geom rgba="0 1 0 1"/>
</default>
</default>
<worldbody>
<geom type="box"/>
<body childclass="sub">
<geom type="ellipsoid"/>
<geom type="sphere" rgba="0 0 1 1"/>
<geom type="cylinder" class="main"/>
</body>
</worldbody>
<worldbody>
<geom type="box"/>
<body childclass="sub">
<geom type="ellipsoid"/>
<geom type="sphere" rgba="0 0 1 1"/>
<geom type="cylinder" class="main"/>
</body>
</worldbody>
</mujoco>
This example will not actually compile because some required information is missing, but here we are only interested
+107 -43
View File
@@ -1,13 +1,13 @@
Model Editing
-------------
.. admonition:: Unstable API
:class: attention
.. admonition:: New API
:class: note
The API described below is new and unstable. There may be latent bugs and function signatures may change. Early
adopters are welcome (indeed, encouraged) to try it out and report any issues on GitHub.
The API described below is new but feature complete. It is recommended for general use, but latent bugs are still
possible. Please report any issues on GitHub.
As of MuJoCo 3.2, it is possible to create and modify models using the :ref:`mjSpec` struct and related API.
As of MuJoCo 3.2.0, it is possible to create and modify models using the :ref:`mjSpec` struct and related API.
This datastructure is in one-to-one correspondence with MJCF and indeed, MuJoCo's own XML parsers (both MJCF and URDF)
use this API when loading a model.
@@ -21,25 +21,28 @@ The new API augments the traditional workflow of creating and editing models usi
*compile* steps. As summarized in the :ref:`Overview chapter<Instance>`, the traditional workflow is:
1. Create an XML model description file (MJCF or URDF) and associated assets. |br|
2. Call :ref:`mj_loadXML`, obtain an :ref:`mjModel` instance.
2. Call :ref:`mj_loadXML`, obtain an mjModel instance.
The new workflow is:
The new workflow using :ref:`mjSpec` is:
1. :ref:`Create<mj_makeSpec>` an empty :ref:`mjSpec` or :ref:`parse<mj_parseXML>` an existing XML file to an
:ref:`mjSpec`.
2. Edit the mutable :ref:`mjSpec` datastructure adding, changing and removing elements.
3. Compile the :ref:`mjSpec` at any point, obtaining an updated :ref:`mjModel` instance. After compilation, the
:ref:`mjSpec` remains editable, so steps 2 and 3 are interchangeable.
1. :ref:`Create<mj_makeSpec>` an empty mjSpec or :ref:`parse<mj_parseXML>` an existing XML file.
2. Programmatically edit the mjSpec datastructure by adding, modifying and removing elements.
3. :ref:`Compile<mj_compile>` the mjSpec to an mjModel instance.
After compilation, the mjSpec remains editable, so steps 2 and 3 are interchangeable.
.. _meUsage:
Usage
~~~~~
Here we describe the C API for procedural model editing, but it is also exposed in the
:ref:`Python bindings<PyModelEdit>`.
After creating a new :ref:`mjSpec` or parsing an existing XML file to an :ref:`mjSpec`, procedural editing corresponds
to setting attributes. For example, in order to change the timestep, one can do:
Here we describe the C API for procedural model editing, but it is also exposed in the :ref:`Python
bindings<PyModelEdit>`. Advanced users can refer to `user_api_test.cc
<https://github.com/google-deepmind/mujoco/blob/main/test/user/user_api_test.cc>`__ and the MJCF parser in
`xml_native_reader.cc <https://github.com/google-deepmind/mujoco/blob/main/src/xml/xml_native_reader.cc>`__ for more
usage examples. After creating a new :ref:`mjSpec` or parsing an existing XML file to an :ref:`mjSpec`, procedural
editing corresponds to setting attributes. For example, in order to change the timestep, one can do:
.. code-block:: C
@@ -55,61 +58,122 @@ In C one uses the provided :ref:`getters<AttributeGetters>` and :ref:`setters<At
mjs_setString(model->modelname, "my_model");
In C++ one can use these directly:
In C++, one can use vectors and strings directly:
.. code-block:: C++
std::string modelname = "my_model";
*spec->modelname = modelname;
Loading a spec from XML can be done as follows:
.. code-block:: C
std::array<char, 1000> error;
mjSpec* s = mj_parseXML(filename, vfs, error.data(), error.size());
.. _meMjsElements:
Model elements
^^^^^^^^^^^^^^
Model elements coresponding to MJCF are exposed to the user as C structs with the ``mjs`` prefix, the definitions are
listed under the :ref:`Model Editing<tySpecStructure>` section of the struct reference. For example, an MJCF
:ref:`geom<body-geom>` corresponds to an :ref:`mjsGeom`.
Model elements corresponding to MJCF are added to the spec using the corresponding functions. For example, to add a box
geom to the world body, one would do
Global defaults for all elements are set by :ref:`initializers<ElementInitialization>` like :ref:`mjs_defaultGeom`.
These functions are defined in `user_init.c
<https://github.com/google-deepmind/mujoco/blob/main/src/user/user_init.c>`__ and are the source of truth for all
default values.
Elements cannot be created directly; they are returned to the user by the corresponding constructor function, e.g.
:ref:`mjs_addGeom`. For example, to add a box geom to the world body, one would do
.. code-block:: C
mjSpec* spec = mj_makeSpec();
mjsBody* world = mjs_findBody(spec, "world");
mjsGeom* my_geom = mjs_addGeom(world, NULL);
my_geom->type = mjGEOM_BOX;
my_geom->size[0] = my_geom->size[1] = my_geom->size[2] = 0.5;
mjModel* model = mj_compile(spec);
mjSpec* spec = mj_makeSpec(); // make an empty spec
mjsBody* world = mjs_findBody(spec, "world"); // find the world body
mjsGeom* my_geom = mjs_addGeom(world, NULL); // add a geom to the world
my_geom->type = mjGEOM_BOX; // set geom type
my_geom->size[0] = my_geom->size[1] = my_geom->size[2] = 0.5; // set box size
mjModel* model = mj_compile(spec); // compile to mjModel
The ``NULL`` second argument to :ref:`mjs_addGeom` is the optional default class pointer. When using defaults
procedurally, default classes are passed in explicitly to element constructors. The global defaults of all elements
(used when no default class is passed in) can be inspected in
`user_init.c <https://github.com/google-deepmind/mujoco/blob/main/src/user/user_init.c>`__.
.. _meAttachment:
Attachment
^^^^^^^^^^
The new framework introduces a powerful new feature: attaching and detaching model subtrees. Attachment allows the user
This framework introduces a powerful new feature: attaching and detaching model subtrees. Attachment allows the user
copy a subtree from one model into another, while also copying related referenced assets and referencing elements from
outside the kinematic tree (e.g., actuators and sensors). Similarly, detaching a subtree will remove all associated
elements from the model.
elements from the model. This feature is already used to power the :ref:`attach<body-attach>` and
:ref:`replicate<replicate>` meta-elements in MJCF. It is possible to :ref:`attach a body to a frame<mjs_attachBody>` and
to :ref:`attach a body to a site<mjs_attachToSite>`:
This feature is incomplete and will be described in detail once it is fully implemented, but it is already used to power
the :ref:`attach<body-attach>` and :ref:`replicate<replicate>` meta-elements in MJCF.
.. code-block:: C
mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
mjsFrame* frame = mjs_addFrame(mjs_findBody(parent, "world"), NULL);
mjsSite* site = mjs_addSite(mjs_findBody(parent, "world"), NULL);
mjsBody* body = mjs_addBody(mjs_findBody(child, "world"), NULL);
mjsBody* attached_body_1 = mjs_attachBody(frame, body, "attached-", "-1");
mjsBody* attached_body_2 = mjs_attachToSite(site, body, "attached-", "-2");
.. _meKnownIssues:
or :ref:`attach a frame to a body<mjs_attachFrame>`:
Known issues
~~~~~~~~~~~~
.. code-block:: C
- Better documentation is still missing and will be added in the future. In the meantime, advanced users can refer
to `user_api_test.cc <https://github.com/google-deepmind/mujoco/blob/main/test/user/user_api_test.cc>`__ and the MJCF
parser in `xml_native_reader.cc <https://github.com/google-deepmind/mujoco/blob/main/src/xml/xml_native_reader.cc>`__,
which is already using this API.
- One of the central design considerations of the new API is incremental compilation, meaning that after making small
changes to a spec that has already been compiled, subsequent re-compilation will be very fast. While the code is
written to support incremental compilation, this functionality is not fully implemented and will be added in the
future, resulting in faster re-compilation times.
- Since the main test for the new API is the MJCF parser, which always constructs a model from scratch, there
might be latent bugs related to model editing. Please report such bugs if you encounter them.
mjSpec* parent = mj_makeSpec();
mjSpec* child = mj_makeSpec();
mjsBody* body = mjs_addBody(mjs_findBody(parent, "world"), NULL);
mjsFrame* frame = mjs_addFrame(mjs_findBody(child, "world"), NULL);
mjsFrame* attached_frame = mjs_attachFrame(body, frame, "attached-", "-1");
.. _meDefault:
Default classes
^^^^^^^^^^^^^^^
Default classes are fully supported in the new API, however using them requires an understanding of how defaults
are implemented. As explained in the :ref:`Default settings <CDefault>` section, default classes are first loaded as a
tree of dummy elements, which are then used to initialize elements which reference them. When editing models with
defaults, this initialization is explicit:
.. code-block:: C
mjSpec* spec = mj_makeSpec();
mjsDefault* main = mjs_getSpecDefault(spec);
main->geom.type = mjGEOM_BOX;
mjsGeom* geom = mjs_addGeom(mjs_findBody(spec, "world"), main);
Importantly, changing a default class after it has been used to initialize elements will not change the properties of
already initialized elements.
.. admonition:: Possible future change
:class: note
The behaviour described above, where defaults are only applied at initialization, is a remnant of the old, XML-only
loading pipeline. A future API change could allow defaults to be changed and applied after initialization. If you
think this feature is important to you, please let us know on GitHub.
.. _meSaving:
XML saving
^^^^^^^^^^
Specs can be saved to an XML file or string using :ref:`mj_saveXML` or :ref:`mj_saveXMLString`, respectively.
Saving requires that the spec first be compiled.
Importantly, the saved XML will take into account any defined defaults. This is useful when a model has many repeated
values, for example if loaded from URDF, which does not support defaults. In such a case one can add default classes,
set the class of the relevant elements, and save; the resulting XML will use the defaults and be more human-readable.
.. _meRecompilation:
In-place recompilation
^^^^^^^^^^^^^^^^^^^^^^
Compilation with :ref:`mj_compile` can be called at any point to obtain a new mjModel instance. In contrast,
:ref:`mj_recompile` updates an existing mjModel and mjData pair in-place, while preserving the simulation state. This
allows model editing to occur **during simulation**, for example adding or removing bodies.
+73 -15
View File
@@ -469,14 +469,11 @@ the raw callback pointer, and the GIL will **not** be acquired each time the cal
Model editing
=============
The :doc:`Model Editing<programming/modeledit>` framework which allows for procedural model manipulation is exposed
via Python. In many ways this API is conceptually similar to ``dm_control``'s
`PyMJCF module <https://github.com/google-deepmind/dm_control/tree/main/dm_control/mjcf#readme>`__, where ``MjSpec``
plays the role of ``mjcf_model``. The largest difference between these two APIs is speed. Native model manipulation via
``MjSpec`` is around ~100x faster than PyMJCF.
The C API for model editing is documented in the :doc:`Programming<../programming/modeledit>` chapter.
This functionality is mirrored in the Python API, with the addition of several convenience methods.
Below is a minimal usage example, more examples can be found in the Model Editing
`colab notebook <https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb>`__.
Below is a simple example of how to use the model editing API. For more examples, please refer to
`specs_test.py <https://github.com/google-deepmind/mujoco/blob/main/python/mujoco/specs_test.py>`__.
.. code-block:: python
@@ -495,18 +492,79 @@ Below is a simple example of how to use the model editing API. For more examples
...
model = spec.compile()
.. admonition:: Missing features
:class: attention
Construction
------------
We are aware of multiple missing features in the Python API, including:
The ``MjSpec`` object wraps the :ref:`mjSpec` struct and can be constructed in three ways:
- Better tree traversal utilities like :python:`children = body.children()` etc.
- PyMJCF's notion of "binding", allowing access to :ref:`mjModel` and :ref:`mjData` values via the associated ``mjs``
elements.
1. Create an empty spec: ``spec = mujoco.MjSpec()``
2. Load the spec from XML string: ``spec = mujoco.MjSpec.from_string(xml_string)``
3. Load the spec from XML file: ``spec = mujoco.MjSpec.from_file(file_path)``
There are certainly other missing features that we are not aware of. Please contact us on GitHub with feature
requests or bug reports and we will prioritize accordingly.
Note the ``from_string()`` and ``from_file()`` methods can only be called at construction time.
Convenience methods
-------------------
The Python bindings provide a number of convenience methods and attributes not directly available in the C API in order
to make model editing easier:
Element lists
^^^^^^^^^^^^^
Lists of all elements in a spec can be accessed using named properties, using the plural form. For example,
``spec.meshes`` returns a list of all meshes in the spec.
The following properties are implemented: ``sites``, ``geoms``, ``joints``, ``lights``, ``cameras``, ``bodies``,
``frames``, ``materials``, ``meshes``, ``pairs``, ``equalities``, ``tendons``, ``actuators``, ``skins``, ``textures``,
``texts``, ``tuples``, ``flexes``, ``hfields``, ``keys``, ``numerics``, ``excludes``, ``sensors``, ``plugins``.
Tree traversal
^^^^^^^^^^^^^^
Traversal of the kinematic tree is aided by the following methods which return tree-related lists of elements:
Direct children:
Like the spec-level element lists described above, bodies have properties which return lists of all direct children.
For example, ``body.geoms`` returns a list of all geoms that are direct children of the body. This works for all
in tree elements namely ``bodies``, ``joints``, ``geoms``, ``sites``, ``cameras``, ``lights`` and ``frames``.
Recursive search:
``body.find_all()`` returns a list of all elements of the given type which are in the subtree of the given body.
Element types can be specified with the :ref:`mjtObj` enum, or with the corresponding string. For example either
``body.find_all(mujoco.mjtObj.mjOBJ_SITE)`` or ``body.find_all('site')`` will return a list of all sites under the
body.
Relationship to ``PyMJCF``
--------------------------
`dm_control <https://github.com/google-deepmind/dm_control/tree/main>`__'s
`PyMJCF <https://github.com/google-deepmind/dm_control/blob/main/dm_control/mjcf/README.md>`__ module provides similar
functionality to the native model editing API described here, but is roughly two orders of magnitude slower due to its
reliance on Python manipulation of strings.
For users familiar with ``PyMJCF``, the ``MjSpec`` object is conceptually similar to ``dm_control``'s
``mjcf_model``. A more detailed migration guide could be added here in the future; in the meantime, note that the
Model Editing
`colab notebook <https://colab.research.google.com/github/google-deepmind/mujoco/blob/main/python/mjspec.ipynb>`__
includes a reimplementation of the ``PyMJCF`` example in the ``dm_control``
`tutorial notebook <https://github.com/google-deepmind/dm_control/blob/main/dm_control/mjcf/tutorial.ipynb>`__.
``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:
.. 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]
Notes
-----
- :ref:`mj_recompile` works differently than in the C API. In the C API, it modifies the model and the data in place,
while in the Python API it returns new :ref:`MjModel` and :ref:`MjData` objects. This is to avoid dangling references.
.. _PyBuild: