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
+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.