Improve Python bindings documentation.

PiperOrigin-RevId: 526658768
Change-Id: Ife5640ed4328427e24e9f0302ee8a5424d82a974
This commit is contained in:
Yuval Tassa
2023-04-24 09:00:17 -07:00
committed by Copybara-Service
parent 66def34e0e
commit 62a675ba28
2 changed files with 187 additions and 182 deletions
+2 -2
View File
@@ -38,8 +38,8 @@ Python bindings
or `segmentation fault <https://github.com/deepmind/mujoco/issues/790>`_. or `segmentation fault <https://github.com/deepmind/mujoco/issues/790>`_.
#. The ``viewer.launch_passive`` function now returns a handle which can be used to interact with the viewer. The #. The ``viewer.launch_passive`` function now returns a handle which can be used to interact with the viewer. The
passive viewer now also requires an explicit call to ``sync`` on its handle to pick up any update to the physics passive viewer now also requires an explicit call to ``sync`` on its handle to pick up any update to the physics
state. This is to avoid race conditions that can result in visual artifacts. See :ref:`documentation<PyViewer>` for state. This is to avoid race conditions that can result in visual artifacts. See
details. :ref:`documentation<PyViewerPassive>` for details.
#. The ``viewer.launch_repl`` function has been removed since its functionality is superceded by ``launch_passive``. #. The ``viewer.launch_repl`` function has been removed since its functionality is superceded by ``launch_passive``.
#. Added a small number of missing struct fields discovered through the new ``introspect`` metadata. #. Added a small number of missing struct fields discovered through the new ``introspect`` metadata.
+150 -145
View File
@@ -44,123 +44,67 @@ The recommended way to install this package is via `PyPI <https://pypi.org/proje
A copy of the MuJoCo library is provided as part of the package and does **not** need to be downloaded or installed A copy of the MuJoCo library is provided as part of the package and does **not** need to be downloaded or installed
separately. separately.
.. _PyBuild:
Building from source
--------------------
.. note::
Building from source is only necessary if you are modifying the
Python bindings (or are trying to run on exceptionally old Linux systems).
If that's not the case, then we recommend installing the prebuilt binaries
from PyPI.
1. Make sure you have CMake and a C++17 compiler installed.
2. Download the `latest binary release <https://github.com/deepmind/mujoco/releases>`__
from GitHub. On macOS, the download corresponds to a DMG file from which you
can drag ``MuJoCo.app`` into your ``/Applications`` folder.
3. Clone the entire ``mujoco`` repository from GitHub and ``cd`` into the python
directory:
.. code-block:: shell
git clone https://github.com/deepmind/mujoco.git
cd mujoco/python
4. Create a virtual environment:
.. code-block:: shell
python3 -m venv /tmp/mujoco
source /tmp/mujoco/bin/activate
5. Generate a `source distribution <https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist>`__
tarball with the ``make_sdist.sh`` script.
.. code-block:: shell
cd python
bash make_sdist.sh
The ``make_sdist.sh`` script generates additional C++ header files that are
needed to build the bindings, and also pulls in required files from elsewhere
in the repository outside the ``python`` directory into the sdist. Upon
completion, the script will create a ``dist`` directory with a
``mujoco-x.y.z.tar.gz`` file (where ``x.y.z`` is the version number).
6. Use the generated source distribution to build and install the bindings.
You'll need to specify the path to the MuJoCo library you downloaded earlier
in the ``MUJOCO_PATH`` environment variable.
.. note::
For macOS, this can be the path to a directory that contains the
``mujoco.framework``. In particular, you can set
``MUJOCO_PATH=/Applications/MuJoCo.app`` if you installed MuJoCo as
suggested in step 1.
.. code-block:: shell
cd dist
MUJOCO_PATH=/PATH/TO/MUJOCO pip install mujoco-x.y.z.tar.gz
The Python bindings should now be installed! To check that they've been
successfully installed, ``cd`` outside of the ``mujoco`` directory and run
``python -c "import mujoco"``.
As a reference, a working build configuration can be found in MuJoCo's
[continuous integration setup](https://github.com/deepmind/mujoco/blob/main/.github/workflows/build.yml) on GitHub.
.. _PyViewer: .. _PyViewer:
Interactive viewer Interactive viewer
================== ==================
An interactive GUI viewer is provided as part of the Python package in the ``mujoco.viewer`` module. This is the same An interactive GUI viewer is provided as part of the Python package in the ``mujoco.viewer`` module. It is based on the
viewer as the ``simulate`` application that ships with the MuJoCo binary releases. same codebase as the :ref:`simulate<saSimulate>` application that ships with the MuJoCo binary releases. Three distinct
use cases are supported:
Three distinct use cases are supported: .. _PyViewerApp:
#. As a **standalone application**: Standalone application
----------------------
- ``python -m mujoco.viewer`` launches an empty visualization session, where a model can be loaded by drag-and-drop. - ``python -m mujoco.viewer`` launches an empty visualization session, where a model can be loaded by drag-and-drop.
- ``python -m mujoco.viewer --mjcf=/path/to/some/mjcf.xml`` launches a visualization session for the specified - ``python -m mujoco.viewer --mjcf=/path/to/some/mjcf.xml`` launches a visualization session for the specified
model file. model file.
#. As a **fully managed viewer** in a Python program/script, through the function ``viewer.launch``. This function .. _PyViewerManaged:
**blocks the user's script completely** to take care of running and timing a physics loop.
- ``viewer.launch()`` launches an empty visualization session, where a model can be loaded by drag-and-drop. Managed viewer
- ``viewer.launch(model)`` launches a visualization session for the given ``mjModel`` where the visualizer --------------
Called from a Python program/script, through the function ``viewer.launch``. This function *blocks user code* to
support precise timing of the physics loop. This mode should be used if user code is implemented as
:ref:`engine plugins<exPlugin>` or :ref:`physics callbacks<glPhysics>`, and is called by MuJoCo during :ref:`mj_step`.
- ``viewer.launch()`` launches an empty visualization session, where a model can be loaded by drag-and-drop.
- ``viewer.launch(model)`` launches a visualization session for the given ``mjModel`` where the visualizer
internally creates its own instance of ``mjData`` internally creates its own instance of ``mjData``
- ``viewer.launch(model, data)`` is the same as above, except that the visualizer operates directly on the given - ``viewer.launch(model, data)`` is the same as above, except that the visualizer operates directly on the given
``mjData`` instance -- upon exit the ``data`` object will have been modified. ``mjData`` instance -- upon exit the ``data`` object will have been modified.
#. As a **passive viewer**, by calling ``viewer.launch_passive(model, data)``. This function **does not block**, .. _PyViewerPassive:
allowing the user script to continue execution. In this mode, the user's script is responsible for timing and
advancing the physics state, and mouse-drag perturbations will not work unless the user explicitly handles incoming
events.
.. warning:: Passive viewer
On macOS, ``launch_passive`` requires that the user script is executed via a special ``mjpython`` launcher. --------------
By calling ``viewer.launch_passive(model, data)``. This function *does not block*, allowing user code to continue
execution. In this mode, the user's script is responsible for timing and advancing the physics state, and mouse-drag
perturbations will not work unless the user explicitly synchronizes incoming events.
.. warning::
On MacOS, ``launch_passive`` requires that the user script is executed via a special ``mjpython`` launcher.
The ``mjpython`` command is installed as part of the ``mujoco`` package, and can be used as a drop-in replacement The ``mjpython`` command is installed as part of the ``mujoco`` package, and can be used as a drop-in replacement
for the usual ``python`` command and supports an identical set of command line flags and arguments. For example, for the usual ``python`` command and supports an identical set of command line flags and arguments. For example,
a script can be executed via ``mjpython my_script.py``, and an IPython shell can be launched via a script can be executed via ``mjpython my_script.py``, and an IPython shell can be launched via
``mjpython -m IPython``. ``mjpython -m IPython``.
The ``launch_passive`` function returns a handle which can be used to interact with the viewer. It has the following The ``launch_passive`` function returns a handle which can be used to interact with the viewer. It has the following
attributes: attributes:
- ``scn``, ``cam``, ``opt``, and ``pert`` properties: correspond to :ref:`mjvScene`, :ref:`mjvCamera`, - ``scn``, ``cam``, ``opt``, and ``pert`` properties: correspond to :ref:`mjvScene`, :ref:`mjvCamera`,
:ref:`mjvOption`, and :ref:`mjvPerturb` structs, respectively. :ref:`mjvOption`, and :ref:`mjvPerturb` structs, respectively.
- ``lock()``: provides a mutex lock for the viewer as a context manager. Since the viewer operates its own - ``lock()``: provides a mutex lock for the viewer as a context manager. Since the viewer operates its own
thread, user code must ensure that it is holding the viewer lock before modifying any physics or visualization thread, user code must ensure that it is holding the viewer lock before modifying any physics or visualization
state. These include the ``mjModel`` and ``mjData`` instance passed to ``launch_passive``, and also the ``scn``, state. These include the ``mjModel`` and ``mjData`` instance passed to ``launch_passive``, and also the ``scn``,
``cam``, ``opt``, and ``pert`` properties of the viewer handle. ``cam``, ``opt``, and ``pert`` properties of the viewer handle.
- ``sync()``: synchronizes state between ``mjModel``, ``mjData``, and GUI user inputs since the previous call to - ``sync()``: synchronizes state between ``mjModel``, ``mjData``, and GUI user inputs since the previous call to
``sync``. In order to allow user scripts to make arbitrary modifications to ``mjModel`` and ``mjData`` without ``sync``. In order to allow user scripts to make arbitrary modifications to ``mjModel`` and ``mjData`` without
needing to hold the viewer lock, the passive viewer does not access or modify these structs outside of ``sync`` needing to hold the viewer lock, the passive viewer does not access or modify these structs outside of ``sync``
calls. calls.
@@ -169,16 +113,16 @@ Three distinct use cases are supported:
also transfers user inputs from the GUI back into ``mjOption`` (inside ``mjModel``) and ``mjData``, including also transfers user inputs from the GUI back into ``mjOption`` (inside ``mjModel``) and ``mjData``, including
enable/disable flags, control inputs, and mouse perturbations. enable/disable flags, control inputs, and mouse perturbations.
- ``close()``: programmatically closes the viewer window. This method can be safely called without locking. - ``close()``: programmatically closes the viewer window. This method can be safely called without locking.
- ``is_running()``: returns ``True`` if the viewer window is running and ``False`` if it is closed. - ``is_running()``: returns ``True`` if the viewer window is running and ``False`` if it is closed.
This method can be safely called without locking. This method can be safely called without locking.
The viewer handle can also be used as a context manager which calls ``close()`` automatically upon exit. A minimal The viewer handle can also be used as a context manager which calls ``close()`` automatically upon exit. A minimal
example of a user script that uses ``launch_passive`` might look like the following. (Note that example is a simple example of a user script that uses ``launch_passive`` might look like the following. (Note that example is a simple
illustrative example that does **not** necessarily keep the physics ticking at the correct wallclock rate.) illustrative example that does **not** necessarily keep the physics ticking at the correct wallclock rate.)
.. code-block:: python .. code-block:: python
import time import time
@@ -189,23 +133,23 @@ Three distinct use cases are supported:
d = mujoco.MjData(m) d = mujoco.MjData(m)
with mujoco.viewer.launch_passive(m, d) as viewer: with mujoco.viewer.launch_passive(m, d) as viewer:
# Close the viewer automatically after 30 seconds. # Close the viewer automatically after 30 wall-seconds.
start = time.time() start = time.time()
while viewer.is_running() and time.time() - start < 30: while viewer.is_running() and time.time() - start < 30:
step_start = time.time() step_start = time.time()
# The mj_step call can be replaced with a user-defined function that evaluates # mj_step can be replaced with code that also evaluates
# a policy, applies a control signal, and steps an environment. # a policy and applies a control signal before stepping the physics.
mujoco.mj_step(m, d) mujoco.mj_step(m, d)
# Example of modifying a viewer option: toggle contact points every second.
# Example modification of a viewer option: toggle contact points every two seconds.
with viewer.lock(): with viewer.lock():
viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(d.time % 2) viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(d.time % 2)
# Synchronize so that the viewer picks up changes to the physics state. # Pick up changes to the physics state, apply perturbations, update options from GUI.
viewer.sync() viewer.sync()
# Rudimentary time keeping, doesn't attempt to catch up if physics stepping # Rudimentary time keeping, will drift relative to wall clock.
# takes too long.
time_until_next_step = m.opt.timestep - (time.time() - step_start) time_until_next_step = m.opt.timestep - (time.time() - step_start)
if time_until_next_step > 0: if time_until_next_step > 0:
time.sleep(time_until_next_step) time.sleep(time_until_next_step)
@@ -226,9 +170,9 @@ Structs
The bindings include Python classes that expose MuJoCo data structures. For maximum performance, these classes provide The bindings include Python classes that expose MuJoCo data structures. For maximum performance, these classes provide
access to the raw memory used by MuJoCo without copying or buffering. This means that some MuJoCo functions (e.g., access to the raw memory used by MuJoCo without copying or buffering. This means that some MuJoCo functions (e.g.,
:ref:`mj_step`) change the content of fields *in place*. The user is therefore advised to create their own copies :ref:`mj_step`) change the content of fields *in place*. The user is therefore advised to create copies where required.
where required. For example, when logging the position of a body, one would write For example, when logging the position of a body, one could write
``body_positions.append(data.body('my_body').xpos.copy())``: without the ``.copy()``, the list would contain identical ``positions.append(data.body('my_body').xpos.copy())``. Without the ``.copy()``, the list would contain identical
elements, all pointing to the most recent value. elements, all pointing to the most recent value.
In order to conform to `PEP 8 <https://peps.python.org/pep-0008/>`__ In order to conform to `PEP 8 <https://peps.python.org/pep-0008/>`__
@@ -246,7 +190,7 @@ used, the corresponding deallocation function ``mj_freeFoo/mj_deleteFoo`` is aut
object is deleted. The user does not need to manually free resources. object is deleted. The user does not need to manually free resources.
The ``mujoco.MjModel`` class does not a have Python constructor. Instead, we provide three static factory functions The ``mujoco.MjModel`` class does not a have Python constructor. Instead, we provide three static factory functions
that create a new ``mjModel`` instance: ``mujoco.MjModel.from_xml_string``, ``mujoco.MjModel.from_xml_path``, and that create a new :ref:`mjModel` instance: ``mujoco.MjModel.from_xml_string``, ``mujoco.MjModel.from_xml_path``, and
``mujoco.MjModel.from_binary_path``. The first function accepts a model XML as a string, while the latter two ``mujoco.MjModel.from_binary_path``. The first function accepts a model XML as a string, while the latter two
functions accept the path to either an XML or MJB model file. All three functions optionally accept a Python functions accept the path to either an XML or MJB model file. All three functions optionally accept a Python
dictionary which is converted into a MuJoCo :ref:`Virtualfilesystem` for use during model compilation. dictionary which is converted into a MuJoCo :ref:`Virtualfilesystem` for use during model compilation.
@@ -453,15 +397,15 @@ the raw callback pointer, and the GIL will **not** be acquired each time the cal
.. _PySample: .. _PySample:
Code Sample: open-loop rollout Open-loop rollouts
============================== ==================
We include a code sample showing how to add additional C/C++ functionality, exposed as a Python module via pybind11. The We include a code sample showing how to add additional C/C++ functionality, exposed as a Python module via pybind11. The
sample, implemented in ``rollout.cc`` and wrapped in ``rollout.py``, implements a common use case where tight loops sample, implemented in ``rollout.cc`` and wrapped in ``rollout.py``, implements a common use case where tight loops
implemented outside of Python are beneficial: rolling out a trajectory (i.e., calling ``mj_step()`` in a loop), given an implemented outside of Python are beneficial: rolling out a trajectory (i.e., calling ``mj_step()`` in a loop), given an
intial state and sequence of controls, and returning subsequent states and sensor values. The canonical usage form is intial state and sequence of controls, and returning subsequent states and sensor values. The canonical usage form is
.. code-block:: python .. code-block:: python
state, sensordata = rollout.rollout(model, data, initial_state, ctrl) state, sensordata = rollout.rollout(model, data, initial_state, ctrl)
@@ -476,7 +420,9 @@ all inputs including ``time`` and ``qacc_warmstart`` are set to default values,
(``qfrc_applied``, ``xfrc_applied`` and ``mocap_{pos,quat}``). These can also be optionally set by the user. (``qfrc_applied``, ``xfrc_applied`` and ``mocap_{pos,quat}``). These can also be optionally set by the user.
Since the Global Interpreter Lock can be released, this function can be efficiently threaded using Python threads. See Since the Global Interpreter Lock can be released, this function can be efficiently threaded using Python threads. See
the ``test_threading`` function in ``rollout_test.py`` for an example of threaded operation. the ``test_threading`` function in
`rollout_test.py <https://github.com/deepmind/mujoco/blob/main/python/mujoco/rollout_test.py>`_ for an example of
threaded operation.
.. _PyMjpy_migration: .. _PyMjpy_migration:
@@ -493,48 +439,107 @@ While a complete survey of mujoco-py is beyond the scope of this document, we of
non-exhaustive list of specific mujoco-py features: non-exhaustive list of specific mujoco-py features:
``mujoco_py.load_model_from_xml(bstring)`` ``mujoco_py.load_model_from_xml(bstring)``
------------------------------------------ This factory function constructs a stateful ``MjSim`` instance. When using ``mujoco``, the user should call the
factory function ``mujoco.MjModel.from_xml_*`` as described :ref:`above <PyStructs>`. The user is then responsible
This factory function constructs a stateful ``MjSim`` instance. When using ``mujoco``, the user should call the factory for holding the resulting ``MjModel`` struct instance and explicitly generating the corresponding ``MjData`` by
function ``mujoco.MjModel.from_xml_*`` as described :ref:`above <PyStructs>`. The user is then responsible for holding calling ``mujoco.MjData(model)``.
the resulting ``MjModel`` struct instance and explicitly generating the corresponding ``MjData`` by calling
``mujoco.MjData(model)``.
``sim.reset()``, ``sim.forward()``, ``sim.step()`` ``sim.reset()``, ``sim.forward()``, ``sim.step()``
-------------------------------------------------- Here as above, ``mujoco`` users needs to call the underlying library functions, passing instances of ``MjModel`` and
``MjData``: :ref:`mujoco.mj_resetData(model, data) <mj_resetData>`, :ref:`mujoco.mj_forward(model, data)
Here as above, ``mujoco`` users needs to call the underlying library functions, passing instances of ``MjModel`` and <mj_forward>`, and :ref:`mujoco.mj_step(model, data) <mj_step>`.
``MjData``: :ref:`mujoco.mj_resetData(model, data) <mj_resetData>`, :ref:`mujoco.mj_forward(model, data) <mj_forward>`,
and :ref:`mujoco.mj_step(model, data) <mj_step>`.
``sim.get_state()``, ``sim.set_state(state)``, ``sim.get_flattened_state()``, ``sim.set_state_from_flattened(state)`` ``sim.get_state()``, ``sim.set_state(state)``, ``sim.get_flattened_state()``, ``sim.set_state_from_flattened(state)``
--------------------------------------------------------------------------------------------------------------------- The MuJoCo library’s computation is deterministic given a specific input, as explained in the :ref:`Programming
section <Simulation>`. mujoco-py implements methods for getting and setting some of the relevant fields (and
The MuJoCo library’s computation is deterministic given a specific input, as explained in the :ref:`Programming section similarly ``dm_control.Physics`` offers methods that correspond to the flattened case). ``mujoco`` do not offer such
<Simulation>`. mujoco-py implements methods for getting and setting some of the relevant fields (and similarly abstraction, and the user is expected to get/set the values of the relevant fields explicitly.
``dm_control.Physics`` offers methods that correspond to the flattened case). ``mujoco`` do not offer such abstraction,
and the user is expected to get/set the values of the relevant fields explicitly.
``sim.model.get_joint_qvel_addr(joint_name)`` ``sim.model.get_joint_qvel_addr(joint_name)``
--------------------------------------------- This is a convenience method in mujoco-py that returns a list of contiguous indices corresponding to this joint. The
list starts from ``model.jnt_qposadr[joint_index]``, and its length depends on the joint type. ``mujoco`` doesn't
This is a convenience method in mujoco-py that returns a list of contiguous indices corresponding to this joint. The offer this functionality, but this list can be easily constructed using ``model.jnt_qposadr[joint_index]`` and
list starts from ``model.jnt_qposadr[joint_index]``, and its length depends on the joint type. ``mujoco`` doesn't offer ``xrange``.
this functionality, but this list can be easily constructed using ``model.jnt_qposadr[joint_index]`` and ``xrange``.
``sim.model.*_name2id(name)`` ``sim.model.*_name2id(name)``
----------------------------- mujoco-py creates dicts in ``MjSim`` that allow for efficient lookup of indices for objects of different types:
``site_name2id``, ``body_name2id`` etc. These functions replace the function :ref:`mujoco.mj_name2id(model,
mujoco-py creates dicts in ``MjSim`` that allow for efficient lookup of indices for objects of different types: type_enum, name) <mj_name2id>`. ``mujoco`` offers a different approach for using entity names – :ref:`named access
``site_name2id``, ``body_name2id`` etc. These functions replace the function :ref:`mujoco.mj_name2id(model, type_enum, <PyNamed>`, as well as access to the native :ref:`mj_name2id`.
name) <mj_name2id>`. ``mujoco`` offers a different approach for using entity names – :ref:`named access <PyNamed>`,
as well as access to the native :ref:`mj_name2id`.
``sim.save(fstream, format_name)`` ``sim.save(fstream, format_name)``
---------------------------------- This is the one context in which the MuJoCo library (and therefore also ``mujoco``) is stateful: it holds a copy in
memory of the last XML that was compiled, which is used in :ref:`mujoco.mj_saveLastXML(fname) <mj_saveLastXML>`. Note
that mujoco-py’s implementation has a convenient extra feature, whereby the pose (as determined by ``sim.data``’s
state) is transformed to a keyframe that’s added to the model before saving. This extra feature is not currently
available in ``mujoco``.
This is the one context in which the MuJoCo library (and therefore also ``mujoco``) is stateful: it holds a copy in
memory of the last XML that was compiled, which is used in :ref:`mujoco.mj_saveLastXML(fname) <mj_saveLastXML>`. Note .. _PyBuild:
that mujoco-py’s implementation has a convenient extra feature, whereby the pose (as determined by ``sim.data``’s
state) is transformed to a keyframe that’s added to the model before saving. This extra feature is not currently Building from source
available in ``mujoco``. ====================
.. note::
Building from source is only necessary if you are modifying the
Python bindings (or are trying to run on exceptionally old Linux systems).
If that's not the case, then we recommend installing the prebuilt binaries
from PyPI.
1. Make sure you have CMake and a C++17 compiler installed.
2. Download the `latest binary release <https://github.com/deepmind/mujoco/releases>`__
from GitHub. On macOS, the download corresponds to a DMG file from which you
can drag ``MuJoCo.app`` into your ``/Applications`` folder.
3. Clone the entire ``mujoco`` repository from GitHub and ``cd`` into the python
directory:
.. code-block:: shell
git clone https://github.com/deepmind/mujoco.git
cd mujoco/python
4. Create a virtual environment:
.. code-block:: shell
python3 -m venv /tmp/mujoco
source /tmp/mujoco/bin/activate
5. Generate a `source distribution <https://packaging.python.org/en/latest/glossary/#term-Source-Distribution-or-sdist>`__
tarball with the ``make_sdist.sh`` script.
.. code-block:: shell
cd python
bash make_sdist.sh
The ``make_sdist.sh`` script generates additional C++ header files that are
needed to build the bindings, and also pulls in required files from elsewhere
in the repository outside the ``python`` directory into the sdist. Upon
completion, the script will create a ``dist`` directory with a
``mujoco-x.y.z.tar.gz`` file (where ``x.y.z`` is the version number).
6. Use the generated source distribution to build and install the bindings.
You'll need to specify the path to the MuJoCo library you downloaded earlier
in the ``MUJOCO_PATH`` environment variable.
.. note::
For macOS, this can be the path to a directory that contains the
``mujoco.framework``. In particular, you can set
``MUJOCO_PATH=/Applications/MuJoCo.app`` if you installed MuJoCo as
suggested in step 1.
.. code-block:: shell
cd dist
MUJOCO_PATH=/PATH/TO/MUJOCO pip install mujoco-x.y.z.tar.gz
The Python bindings should now be installed! To check that they've been
successfully installed, ``cd`` outside of the ``mujoco`` directory and run
``python -c "import mujoco"``.
.. tip::
As a reference, a working build configuration can be found in MuJoCo's
`continuous integration setup <https://github.com/deepmind/mujoco/blob/main/.github/workflows/build.yml>`_ on GitHub.