Improve mjSpec documentation. Fixes #2074.
PiperOrigin-RevId: 692251710 Change-Id: Ia46bbbe5b7eaa890433b38fed66824f716cc9ea0
This commit is contained in:
committed by
Copybara-Service
parent
a51f346059
commit
b6037d1759
@@ -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
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user