diff --git a/doc/changelog.rst b/doc/changelog.rst index cdc66f8d..8c2ccf60 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -7,6 +7,9 @@ Upcoming version (not yet released) General ^^^^^^^ + +- The :doc:`Model Editing` 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` attribute. - The not-useful ``convexhull`` compiler option (to disable computation of mesh convex hulls) has been removed. diff --git a/doc/modeling.rst b/doc/modeling.rst index f0d3a0e6..066c5b95 100644 --- a/doc/modeling.rst +++ b/doc/modeling.rst @@ -136,21 +136,21 @@ the model. We start with an example. .. code-block:: xml - - - - - + + + + + - - - - - - - - + + + + + + + + This example will not actually compile because some required information is missing, but here we are only interested diff --git a/doc/programming/modeledit.rst b/doc/programming/modeledit.rst index d7752964..9c95e7c5 100644 --- a/doc/programming/modeledit.rst +++ b/doc/programming/modeledit.rst @@ -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`, 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` an empty :ref:`mjSpec` or :ref:`parse` 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` an empty mjSpec or :ref:`parse` an existing XML file. + 2. Programmatically edit the mjSpec datastructure by adding, modifying and removing elements. + 3. :ref:`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`. -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`. Advanced users can refer to `user_api_test.cc +`__ and the MJCF parser in +`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` and :ref:`settersmodelname, "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 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` section of the struct reference. For example, an MJCF +:ref:`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` like :ref:`mjs_defaultGeom`. +These functions are defined in `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 `__. - .. _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` and +:ref:`replicate` meta-elements in MJCF. It is possible to :ref:`attach a body to a frame` and +to :ref:`attach a body to a site`: -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` and :ref:`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`: -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 `__ and the MJCF - parser in `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 ` 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. diff --git a/doc/python.rst b/doc/python.rst index cef23eb5..9c824b86 100644 --- a/doc/python.rst +++ b/doc/python.rst @@ -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` 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 `__, 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 `__. -Below is a simple example of how to use the model editing API. For more examples, please refer to -`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 `__'s +`PyMJCF `__ 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 `__ +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: + +.. 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: