Python#

MuJoCo 附带原生的 Python 绑定,这些绑定是使用 pybind11 在 C++ 中开发的。Python API 与底层的 C API 保持一致。这导致了一些非 Python 风格的代码结构(例如函数参数的顺序),但其优点是 API 文档 适用于这两种语言。

Python 绑定以 mujoco 包的形式发布在 PyPI 上。这些是低级绑定,旨在尽可能提供对 MuJoCo 库的直接访问。然而,为了提供开发人员在典型 Python 库中所期望的 API 和语义,这些绑定在某些地方特意偏离了原始的 MuJoCo API,相关内容已在本页面中详细说明。

Google DeepMind 的 dm_control 强化学习库依赖于 mujoco 包,并继续得到 Google DeepMind 的支持。对于依赖于 1.0.0 版本之前的 dm_control 的代码,请参阅 迁移指南

对于 mujoco-py 的用户,我们在下方包含了 迁移说明

教程笔记本#

使用 Python 绑定的 MuJoCo 教程可在此处找到:mjcolab

安装#

推荐的安装方式是通过 PyPI

pip install mujoco

MuJoCo 库的副本作为该包的一部分提供,不需要单独下载或安装。

交互式查看器#

Python 包中提供了一个交互式 GUI 查看器,位于 mujoco.viewer 模块中。它基于与 MuJoCo 二进制发行版附带的 simulate 应用程序相同的代码库。支持三种不同的用例:托管查看器独立应用被动查看器

托管查看器#

viewer.launch 函数会启动交互式查看器并阻塞用户代码,这对于支持物理循环的精确计时非常有用。如果用户代码作为 引擎插件物理回调 实现,并由 MuJoCo 在 mj_step 期间调用,则应使用此模式。

  • viewer.launch() 启动一个空的可视化会话,可以通过拖放方式加载模型。

  • viewer.launch(model) 为给定的 mjModel 启动一个可视化会话,可视化器在内部创建自己的 mjData 实例。

  • viewer.launch(model, data) 与上述相同,不同之处在于可视化器直接在给定的 mjData 实例上运行——退出时 data 对象已被修改。

独立应用#

mujoco.viewer Python 包使用 if __name__ == '__main__' 机制,允许直接从命令行将 托管查看器 作为独立应用程序调用。

  • python -m mujoco.viewer 启动一个空的可视化会话,可以通过拖放方式加载模型。

  • python -m mujoco.viewer --mjcf=/path/to/some/mjcf.xml 为指定的模型文件启动可视化会话。

被动查看器#

viewer.launch_passive 函数以非阻塞方式启动交互式查看器,允许用户代码继续执行。在此模式下,用户的脚本负责计时和推进物理状态,并且除非用户显式同步传入的事件,否则鼠标拖拽扰动将不起作用。

警告

在 macOS 上,launch_passive 要求用户脚本通过特殊的 mjpython 启动器执行,这是为了绕过平台限制,即渲染必须在主线程中进行。mjpython 命令作为 mujoco 包的一部分安装,可以用作常规 python 命令的直接替代,并支持相同的命令行标志和参数。例如,可以通过 mjpython my_script.py 执行脚本,通过 mjpython -m IPython 启动 IPython shell。

launch_passive 函数返回一个可用于与查看器交互的句柄。它具有以下属性:

  • camoptpert 属性:分别对应 mjvCameramjvOptionmjvPerturb 结构体。

  • lock():作为上下文管理器为查看器提供互斥锁。由于查看器运行在自己的线程中,用户代码必须确保在修改任何物理或可视化状态之前持有查看器锁。这些包括传递给 launch_passivemjModelmjData 实例,以及查看器句柄的 camoptpert 属性。

  • sync(state_only=False):在用户的 mjModelmjData 和 GUI 之间进行同步。为了允许用户脚本在无需持有查看器锁的情况下对 mjModelmjData 进行任意修改,被动查看器不会在 sync 调用之外访问或修改这些结构体。如果 state_only 参数为 True,则不同步所有内容,仅同步对应于 mjSTATE_INTEGRATIONmjData 字段,然后调用 mj_forward。后一种选项速度快得多,但不会像默认情况那样获取任意更改。无论哪种情况,都会获取通过 GUI 所做的更改,但通过代码修改 mjModel.geom_rgba 等内容时,只有在 state_only=False 时才会被获取。

    用户脚本必须调用 sync 才能使查看器反映物理状态的变化。sync 函数还将用户输入从 GUI 传回 mjOption(在 mjModel 内)和 mjData 中,包括启用/禁用标志、控制输入和鼠标扰动。

  • update_hfield(hfieldid):更新指定 hfieldid 处的高度场数据,以供后续渲染。

  • update_mesh(meshid):更新指定 meshid 处的网格数据,以供后续渲染。

  • update_texture(texid):更新指定 texid 处的纹理数据,以供后续渲染。

  • close():以编程方式关闭查看器窗口。此方法可以在不加锁的情况下安全调用。

  • is_running():如果查看器窗口正在运行,返回 True;如果已关闭,返回 False。此方法可以在不加锁的情况下安全调用。

  • user_scn:一个 mjvScene 对象,允许用户添加更改渲染标志,并向渲染场景添加自定义可视化几何体。这与查看器内部用于渲染最终场景的 mjvScene 是分开的,完全受用户控制。用户脚本可以调用例如 mjv_initGeommjv_connector 将可视化几何体添加到 user_scn,在下一次调用 sync() 时,查看器会将这些几何体包含到未来的渲染图像中。同样,用户脚本可以对 user_scn.flags 进行更改,这些更改将在下一次调用 sync() 时被获取。sync() 调用还会将通过 GUI 所做的渲染标志更改复制回 user_scn,以保持一致性。

    with mujoco.viewer.launch_passive(m, d, key_callback=key_callback) as viewer:
    
      # Enable wireframe rendering of the entire scene.
      viewer.user_scn.flags[mujoco.mjtRndFlag.mjRND_WIREFRAME] = 1
      viewer.sync()
    
      while viewer.is_running():
        ...
        # Step the physics.
        mujoco.mj_step(m, d)
    
        # Add a 3x3x3 grid of variously colored spheres to the middle of the scene.
        viewer.user_scn.ngeom = 0
        i = 0
        for x, y, z in itertools.product(*((range(-1, 2),) * 3)):
          mujoco.mjv_initGeom(
              viewer.user_scn.geoms[i],
              type=mujoco.mjtGeom.mjGEOM_SPHERE,
              size=[0.02, 0, 0],
              pos=0.1*np.array([x, y, z]),
              mat=np.eye(3).flatten(),
              rgba=0.5*np.array([x + 1, y + 1, z + 1, 2])
          )
          i += 1
        viewer.user_scn.ngeom = i
        viewer.sync()
        ...
    

查看器句柄也可以用作上下文管理器,在退出时自动调用 close()。一个使用 launch_passive 的用户脚本的最小示例如下。(请注意,该示例是一个简单的说明性示例,并不一定能让物理模拟以正确的挂钟时间步进。)

import time

import mujoco
import mujoco.viewer

m = mujoco.MjModel.from_xml_path('/path/to/mjcf.xml')
d = mujoco.MjData(m)

with mujoco.viewer.launch_passive(m, d) as viewer:
  # Close the viewer automatically after 30 wall-seconds.
  start = time.time()
  while viewer.is_running() and time.time() - start < 30:
    step_start = time.time()

    # mj_step can be replaced with code that also evaluates
    # a policy and applies a control signal before stepping the physics.
    mujoco.mj_step(m, d)

    # Example modification of a viewer option: toggle contact points every two seconds.
    with viewer.lock():
      viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(d.time % 2)

    # Pick up changes to the physics state, apply perturbations, update options from GUI.
    viewer.sync()

    # Rudimentary time keeping, will drift relative to wall clock.
    time_until_next_step = m.opt.timestep - (time.time() - step_start)
    if time_until_next_step > 0:
      time.sleep(time_until_next_step)

或者,viewer.launch_passive 接受以下关键字参数。

  • key_callback:一个可调用对象,在查看器窗口中每次发生键盘事件时都会被调用。这允许用户脚本对各种按键做出反应,例如按下空格键时暂停或恢复运行循环。

    paused = False
    
    def key_callback(keycode):
      if chr(keycode) == ' ':
        nonlocal paused
        paused = not paused
    
    ...
    
    with mujoco.viewer.launch_passive(m, d, key_callback=key_callback) as viewer:
      while viewer.is_running():
        ...
        if not paused:
          mujoco.mj_step(m, d)
          viewer.sync()
        ...
    
  • show_left_uishow_right_ui:布尔参数,指示启动查看器时 UI 面板应该是可见还是隐藏。请注意,无论指定什么值,用户在启动后仍然可以通过按 Tab 或 Shift+Tab 切换这些面板的可见性。

基本用法#

安装完成后,可以通过 import mujoco 导入该包。结构体、函数、常量和枚举直接从顶层 mujoco 模块中可用。

结构体#

这些绑定包括公开 MuJoCo 数据结构的 Python 类。为了获得最高性能,这些类提供了对 MuJoCo 所用原始内存的访问,而无需复制或缓冲。这意味着某些 MuJoCo 函数(例如 mj_step)会就地更改字段的内容。因此,建议用户在需要时创建副本。例如,在记录身体位置时,可以写 positions.append(data.body('my_body').xpos.copy())。如果没有 .copy(),列表将包含相同的元素,全部指向最新的值。NumPy 切片同样适用。例如,如果创建了局部变量 qpos_slice = data.qpos[3:8] 然后调用了 mj_step,则 qpos_slice 中的值将会改变。

为了符合 PEP 8 命名准则,结构体名称以大写字母开头,例如在 Python 中 mjData 变为 mujoco.MjData

除了 mjModel 之外的所有结构体在 Python 中都有构造函数。对于具有 mj_defaultFoo 类型初始化函数的结构体,Python 构造函数会自动调用默认初始化程序,因此例如 mujoco.MjOption() 会创建一个新的 mjOption 实例,该实例已使用 mj_defaultOption 预初始化。否则,Python 构造函数会将底层的 C 结构体零初始化。

具有 mj_makeFoo 类型初始化函数的结构体在 Python 中具有相应的构造函数重载,例如 Python 中的 mujoco.MjvScene(model, maxgeom=10) 会创建一个新的 mjvScene 实例,该实例在 C 中通过 mjv_makeScene(model, [新的 mjvScene 实例], 10) 初始化。当使用这种初始化形式时,当删除 Python 对象时,会自动调用相应的释放函数 mj_freeFoo/mj_deleteFoo。用户无需手动释放资源。

mujoco.MjModel 类没有 Python 构造函数。相反,我们提供了三个静态工厂函数来创建新的 mjModel 实例:mujoco.MjModel.from_xml_stringmujoco.MjModel.from_xml_pathmujoco.MjModel.from_binary_path。第一个函数接受 XML 字符串作为模型,而后两个函数接受 XML 或 MJB 模型文件的路径。所有三个函数均可选择接受一个 Python 字典,该字典在模型编译期间被转换为 MuJoCo 虚拟文件系统

函数#

MuJoCo 函数作为同名的 Python 函数公开。与结构体不同,我们不尝试使函数名称符合 PEP 8 标准,因为 MuJoCo 同时使用下划线和驼峰命名法。在大多数情况下,函数参数的形式与 C 中的完全相同,并且支持使用在 mujoco.h 中声明的相同名称的关键字参数。接受数组输入参数的 C 函数的 Python 绑定期望获得 NumPy 数组或可转换为 NumPy 数组的可迭代对象(例如列表)。输出参数(即 MuJoCo 期望回写值的数组参数)必须始终是可写的 NumPy 数组。

在 C API 中,接受动态大小数组作为输入的函数期望数组的指针参数以及指定数组大小的整数参数。在 Python 中,由于我们可以自动(并且更安全地)从 NumPy 数组中推断出大小,因此省略了大小参数。调用这些函数时,请按 mujoco.h 中出现的顺序传递除数组大小之外的所有参数,或者使用关键字参数。例如,mj_jac 在 Python 中应调用为 mujoco.mj_jac(m, d, jacp, jacr, point, body)

这些绑定在调用底层 MuJoCo 函数之前会释放 Python 全局解释器锁 (GIL)。这允许进行一些基于线程的并行处理,但用户应记住,GIL 仅在 MuJoCo C 函数本身的持续时间内释放,而在执行任何其他 Python 代码期间不会释放。

注意

绑定提供附加功能的一个地方是顶层 mj_step 函数。由于它经常在循环中调用,我们添加了一个额外的 nstep 参数,指示应调用底层的 mj_step 多次。如果未指定,nstep 采用默认值 1。以下两个代码片段执行相同的计算,但第一个片段在后续物理步骤之间不会获取 GIL。

mj_step(model, data, nstep=20)
for _ in range(20):
  mj_step(model, data)

枚举和常量#

MuJoCo 枚举可用作 mujoco.mjtEnumType.ENUM_VALUE,例如 mujoco.mjtObj.mjOBJ_SITE。MuJoCo 常量可在 mujoco 模块下直接使用相同的名称访问,例如 mujoco.mjVISSTRING

最小示例#

import mujoco

XML=r"""
<mujoco>
  <asset>
    <mesh file="gizmo.stl"/>
  </asset>
  <worldbody>
    <body>
      <freejoint/>
      <geom type="mesh" name="gizmo" mesh="gizmo"/>
    </body>
  </worldbody>
</mujoco>
"""

ASSETS=dict()
with open('/path/to/gizmo.stl', 'rb') as f:
  ASSETS['gizmo.stl'] = f.read()

model = mujoco.MjModel.from_xml_string(XML, ASSETS)
data = mujoco.MjData(model)
while data.time < 1:
  mujoco.mj_step(model, data)
  print(data.geom_xpos)

命名访问#

大多数设计良好的 MuJoCo 模型都会为感兴趣的对象(关节、几何体、身体等)分配名称。当模型编译为 mjModel 实例时,这些名称将与用于索引各个数组元素的数字 ID 相关联。为了方便和提高代码可读性,Python 绑定在 MjModelMjData 上提供了“命名访问”API。mjModel 结构体中的每个 name_fooadr 字段都定义了一个名称类别 foo

对于每个名称类别 foomujoco.MjModelmujoco.MjData 对象提供了一个名为 foo 的方法,该方法接受一个字符串参数,并返回一个用于给定名称的实体 foo 的所有对应数组的访问器对象。访问器对象包含名称对应于 mujoco.MjModelmujoco.MjData 字段的属性,但去除了下划线之前的部分。此外,访问器对象还提供了 idname 属性,它们可以分别用作 mj_name2idmj_id2name 的替代品。例如:

  • m.geom('gizmo') 返回一个访问器,用于 MjModel 对象 m 中与名为“gizmo”的几何体关联的数组。

  • m.geom('gizmo').rgba 是一个长度为 4 的 NumPy 数组视图,指定了该几何体的 RGBA 颜色。具体而言,它对应于 m.geom_rgba[4*i:4*i+4] 的部分,其中 i = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, 'gizmo')

  • m.geom('gizmo').idmujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, 'gizmo') 返回的数字相同。

  • m.geom(i).name'gizmo',其中 i = mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, 'gizmo')

此外,Python API 为某些名称类别定义了许多别名,这些别名对应于 MJCF 模式中定义该类别实体的 XML 元素名称。例如,m.joint('foo')m.jnt('foo') 相同。以下提供了这些别名的完整列表。

关节的访问器与其他类别略有不同。一些 mjModelmjData 字段(大小为 nqnv 的字段)与自由度 (DoFs) 相关联,而不是关节。这是因为不同类型的关节具有不同数量的自由度。尽管如此,我们还是将这些字段与它们对应的关节相关联,例如通过 d.joint('foo').qposd.joint('foo').qvel,但是这些数组的大小在访问器之间会根据关节类型而有所不同。

命名访问保证在模型中的实体数量方面为 O(1)。换句话说,按名称访问实体的耗时不会随着模型中名称或实体数量的增加而增加。

为了完整起见,我们在下面提供 MuJoCo 中所有名称类别的完整列表,以及 Python API 中定义的相应别名。

  • body

  • jntjoint

  • geom

  • site

  • camcamera

  • light

  • mesh

  • skin

  • hfield

  • textexture

  • matmaterial

  • pair

  • exclude

  • eqequality

  • tendonten

  • actuator

  • sensor

  • numeric

  • text

  • tuple

  • keykeyframe

渲染#

MuJoCo 本身要求用户在调用其任何 mjr_ 渲染例程之前设置好有效的 OpenGL 上下文。Python 绑定提供了一个基本的 mujoco.GLContext 类,帮助用户为离屏渲染设置上下文。要创建上下文,请调用 ctx = mujoco.GLContext(max_width, max_height)。上下文创建后,必须使其成为当前上下文,然后才能调用 MuJoCo 渲染函数,这可以通过 ctx.make_current() 完成。请注意,一个上下文在任何给定时间只能在一个线程上成为当前上下文,并且所有后续的渲染调用都必须在同一个线程上进行。

ctx 对象被删除时,上下文会自动释放,但在某些多线程场景下,可能需要显式释放底层 OpenGL 上下文。为此,请调用 ctx.free(),之后用户有责任确保不再在该上下文上进行渲染调用。

上下文创建后,用户可以遵循 MuJoCo 的标准渲染方式,例如在 可视化 部分中所述。

错误处理#

MuJoCo 通过 mju_error 机制报告不可恢复的错误,这会立即终止整个进程。用户可以通过 mju_user_error 回调安装自定义错误处理程序,但该处理程序也应终止进程,否则 MuJoCo 在回调返回后的行为是未定义的。实际上,确保错误回调不返回给 MuJoCo 就足够了,但允许使用 longjmp 跳过 MuJoCo 的调用栈返回到外部调用点。

Python 绑定利用 longjmp 将不可恢复的 MuJoCo 错误转换为 mujoco.FatalError 类型的 Python 异常,这些异常可以以通常的 Python 方式捕获和处理。此外,它以线程本地的方式使用当前私有 API 安装其错误回调,从而允许从多个线程并发调用 MuJoCo。

回调#

MuJoCo 允许用户安装自定义回调函数以修改其计算流程的某些部分。例如,mjcb_sensor 可用于实现自定义传感器,而 mjcb_control 可用于实现自定义执行器。回调通过 mujoco.h 中以 mjcb_ 为前缀的函数指针公开。

对于每个回调 mjcb_foo,用户可以通过 mujoco.set_mjcb_foo(some_callable) 将其设置为 Python 可调用对象。要重置它,请调用 mujoco.set_mjcb_foo(None)。要检索当前安装的回调,请调用 mujoco.get_mjcb_foo()。(如果回调不是通过 Python 绑定安装的,则不应使用该 getter。)绑定在每次进入回调时会自动获取 GIL,并在重新进入 MuJoCo 之前释放它。这可能会导致严重的性能影响,因为回调在 MuJoCo 的计算流程中会被多次触发,因此不太可能适用于“生产”用例。但是,预计此功能对于复杂模型的原型设计将非常有用。

另外,如果回调是在原生动态库中实现的,用户可以使用 ctypes 获取 Python 到 C 函数指针的句柄,并将其传递给 mujoco.set_mjcb_foo。绑定随后会检索底层函数指针并将其直接分配给原始回调指针,这样每次进入回调时都不会获取 GIL。

模型编辑#

用于模型编辑的 C API 在 编程 章节中有记录。此功能在 Python API 中得到了镜像,并增加了若干便利方法。下面是一个最小用法示例,更多示例可以在模型编辑 colab 笔记本 中找到。

import mujoco
spec = mujoco.MjSpec()
spec.modelname = "my model"
body = spec.worldbody.add_body(
    pos=[1, 2, 3],
    quat=[0, 1, 0, 0],
)
geom = body.add_geom(
    name='my_geom',
    type=mujoco.mjtGeom.mjGEOM_SPHERE,
    size=[1, 0, 0],
    rgba=[1, 0, 0, 1],
)
...
model = spec.compile()

构造#

MjSpec 对象包装了 mjSpec 结构体,可以通过三种方式构造:

  1. 创建空规格:spec = mujoco.MjSpec()

  2. 从 XML 字符串加载规格:spec = mujoco.MjSpec.from_string(xml_string)

  3. 从 XML 文件加载规格:spec = mujoco.MjSpec.from_file(file_path)

请注意,from_string()from_file() 方法只能在构造时调用。

资源#

MuJoCo 可选择使用 虚拟文件系统 (VFS) 从内存中加载资产(如网格和纹理)。某些 解码器 也可能选择利用 VFS 作为按需加载资产的方式,例如在寻址归档格式中的文件时。这要求在将规格(及所有附加规格)解析并编译为模型时使用相同的 VFS。

Python 绑定提供了 mujoco.MjVfs 作为 mjVFS C 结构体的包装器。

MjVfs 支持上下文管理器协议,这确保了在离开块时正确释放资源。

with mujoco.MjVfs() as vfs:
    vfs["model.xml"] = b"<mujoco/>"
    spec = mujoco.MjSpec.from_string("model.xml", vfs=vfs)
    spec.compile(vfs=vfs)

你也可以直接创建一个实例,并在完成后调用 close()

vfs = mujoco.MjVfs()
vfs["model.xml"] = some_xml_string.encode("utf-8")
spec = mujoco.MjSpec.from_file("model.xml", vfs=vfs)
spec.compile(vfs=vfs)
vfs.close()

MjVfs 对象支持类似于字典的操作来管理缓冲区。

  • vfs["name"] = data:向 VFS 添加缓冲区。data 必须是 bytes 类型。

  • del vfs["name"]:从 VFS 中删除文件。

  • "name" in vfs:检查文件是否存在于 VFS 中。

静态工厂函数 mujoco.MjModel.from_xml_stringmujoco.MjModel.from_xml_pathmujoco.MjSpec.from_stringmujoco.MjSpec.from_file 接受一个可选的 vfs 参数。此外,spec.compile() 函数也接受一个可选的 vfs 参数。

警告

先前通过映射资产名称到字节的字典来传递资产的方式已被弃用,并将在下一次发布中删除。你不能同时指定 assets 字典和 vfs 参数。MjVfs 应作为直接替代品使用。

参考而言,弃用的 assets 字典方法如下所示:

assets = {'image.png': b'image_data'}
spec = mujoco.MjSpec.from_string(xml_referencing_image_png, assets=assets)
model = spec.compile()

# Or

spec = mujoco.MjSpec.from_string(xml_referencing_image_png)
spec.assets = {'image.png': b'image_data'}
model = spec.compile()

保存到 XML#

编译后的 MjSpec 对象可以使用 to_xml() 方法保存为 XML 字符串。

print(spec.to_xml())
<mujoco model="my model">
  <compiler angle="radian"/>

  <worldbody>
    <body pos="1 2 3" quat="0 1 0 0">
      <geom name="my_geom" size="1" rgba="1 0 0 1"/>
    </body>
  </worldbody>
</mujoco>

或者,规格可以使用 encode() 直接保存到文件中。

spec.encode('model.xml', model)

附加#

可以通过使用附件组合多个规格。以下选项是可行的:

  • 将子规格中的身体附加到父规格中的框架:body.attach_body(body, prefix, suffix),返回附加身体的引用,该引用应与作为输入的身体相同。

  • 将子规格中的框架附加到父规格中的身体:body.attach_frame(frame, prefix, suffix),返回附加框架的引用,该引用应与作为输入的框架相同。

  • 将子规格附加到父规格中的站点:parent_spec.attach(child_spec, site=site_name_or_obj),返回框架的引用,该框架是转换为框架后的附加 worldbody。站点必须属于子规格。前缀和后缀也可以作为关键字参数指定。

  • 将子规格附加到父规格中的框架:parent_spec.attach(child_spec, frame=frame_name_or_obj),返回框架的引用,该框架是转换为框架后的附加 worldbody。框架必须属于子规格。前缀和后缀也可以作为关键字参数指定。

附加的默认行为是不复制,因此所有的子引用(除 worldbody 外)在父规格中仍然有效,因此修改子规格将修改父规格。这对于 MJCF 中的 attachreplicate 元元素是不成立的,它们在附加时会创建深层副本。但是,可以通过将 spec.copy_during_attach 设置为 True 来覆盖默认行为。在这种情况下,子规格将被复制,对子规格的引用将不再指向父规格。

import mujoco

# Create the parent spec.
parent = mujoco.MjSpec()
body = parent.worldbody.add_body()
frame = parent.worldbody.add_frame()
site = parent.worldbody.add_site()

# Create the child spec.
child = mujoco.MjSpec()
child_body = child.worldbody.add_body()
child_frame = child.worldbody.add_frame()

# Attach the child to the parent in different ways.
body_in_frame = frame.attach_body(child_body, 'child-', '')
frame_in_body = body.attach_frame(child_frame, 'child-', '')
worldframe_in_site = parent.attach(child, site=site, prefix='child-')
worldframe_in_frame = parent.attach(child, frame=frame, prefix='child-')

便利方法#

Python 绑定提供了许多在 C API 中不可用的便利方法和属性,以使模型编辑更容易。

命名访问#

MjSpec 对象具有如 .body().joint().site() 等方法,用于元素的命名访问。spec.geom('my_geom') 将返回名为“my_geom”的 mjsGeom,如果不存在,则返回 None

元素列表#

可以使用复数形式的命名属性访问规格中所有元素的列表。例如,spec.meshes 返回规格中所有网格的列表。实现了以下属性:sitesgeomsjointslightscamerasbodiesframesmaterialsmeshespairsequalitiestendonsactuatorsskinstexturestextstuplesflexeshfieldskeysnumericsexcludessensorsplugins

元素移除#

delete() 方法从规格中删除相应的元素,例如 spec.delete(spec.geom('my_geom')) 将删除名为“my_geom”的几何体以及引用它的所有元素。对于可以有子元素的元素(身体和默认值),delete 也会删除它们所有的子元素。删除身体子树时,所有引用子树中元素的元素也将被删除。

树遍历#

kinematic 树的遍历得益于以下返回树相关元素列表的方法:

直接子元素

像上述规格级元素列表一样,身体具有返回所有直接子元素列表的属性。例如,body.geoms 返回作为身体直接子元素的所有几何体列表。这适用于树中的所有元素,即 bodiesjointsgeomssitescameraslightsframes

递归搜索

body.find_all() 返回给定身体子树中所有给定类型的元素列表。元素类型可以使用 mjtObj 枚举或相应的字符串指定。例如,body.find_all(mujoco.mjtObj.mjOBJ_SITE)body.find_all('site') 都将返回身体下的所有站点列表。

父级

给定元素(包括身体和框架)的父身体可以通过 parent 属性访问。例如,站点的父级可以通过 site.parent 访问。

序列化#

MjSpec 对象可以使用函数 spec.to_zip(file) 连同其所有资产一起序列化,其中 file 可以是文件路径或文件对象。要从 zip 文件加载规格,请使用 spec = MjSpec.from_zip(file),其中 file 是 zip 文件的路径或 zip 文件对象。

网格创建#

mjsMesh 对象包含用于模型创建的具有命名属性的便利方法,对应于 mesh/builtin 语义。参见 specs_test.py

mesh = spec.add_mesh(name='prism')
mesh.make_cone(nedge=5, radius=1)

纹理编辑#

mjsTexture 缓冲区选项将纹理字节存储在 data 属性中。此属性可以读取和修改,例如:

texture = spec.add_texture(name='texture', height=1, width=3, nchannel=3)
texture.data = bytes([255, 0, 0, 0, 255, 0, 0, 0, 255])  # Assign red, green and blue pixels.
texture.data[1] = 255  # Change the first pixel to yellow.

PyMJCFbind 的关系#

dm_controlPyMJCF 模块提供了类似于本文所述原生模型编辑 API 的功能,但由于依赖于对字符串的 Python 操作,速度大约慢了两个数量级。

对于熟悉 PyMJCF 的用户,MjSpec 对象在概念上类似于 dm_controlmjcf_model。未来可能会添加更详细的迁移指南;在此期间,请注意模型编辑 colab 笔记本 包含了 dm_control 教程笔记本PyMJCF 示例的重新实现。

PyMJCF 提供了“绑定”的概念,通过辅助类访问 mjModelmjData 值。在原生 API 中,不需要辅助类,因此可以将 mjs 对象直接绑定到 mjModelmjData。例如,假设我们有多个几何体,其名称中包含字符串“torso”。我们想要从 mjData 获取它们在 XY 平面中的笛卡尔坐标。这可以按如下方式完成:

torsos = [data.bind(geom) for geom in spec.geoms if 'torso' in geom.name]
pos_x = [torso.xpos[0] for torso in torsos]
pos_y = [torso.xpos[1] for torso in torsos]

使用 bind 方法要求 mjModelmjData 是从 mjSpec 编译而来的。如果自上次编译以来有对象被添加到 mjSpec 中或从中移除,则会引发错误。

说明#

  • mj_recompile 的工作方式与 C API 不同。在 C API 中,它就地修改模型和数据,而在 Python API 中,它返回新的 mjModelmjData 对象。这是为了避免悬空引用。

从源代码构建#

注意

仅在修改 Python 绑定(或尝试在非常旧的 Linux 系统上运行)时,才需要从源代码构建。如果不是这种情况,我们建议从 PyPI 安装预构建的二进制文件。

  1. 确保安装了 CMake 和 C++17 编译器。

  2. 从 GitHub 克隆整个 mujoco 存储库。

    git clone https://github.com/google-deepmind/mujoco.git
    
  3. 安装 MuJoCo。从 GitHub 下载 最新二进制版本(在 macOS 上,下载对应一个 DMG 文件,你可以通过双击或运行 hdiutil attach <dmg_file> 来挂载它),或者按照 从源代码构建 中的说明进行构建和安装。

  4. 进入克隆的 MuJoCo 代码库的 python 目录。

    cd mujoco/python
    
  5. 创建虚拟环境。

    python3 -m venv /tmp/mujoco
    source /tmp/mujoco/bin/activate
    
  6. 使用 make_sdist.sh 脚本生成 源代码分发 压缩包。

    bash make_sdist.sh
    

    make_sdist.sh 脚本会生成构建绑定所需的额外 C++ 头文件,并将存储库中 python 目录之外所需的文件拉入到 sdist 中。完成后,脚本将创建一个包含 mujoco-x.y.z.tar.gz 文件(其中 x.y.z 是版本号)的 dist 目录。

  7. 使用生成的源代码分发版来构建和安装绑定。你需要将之前下载或构建并安装的 MuJoCo 库的路径指定在 MUJOCO_PATH 环境变量中,并将 MuJoCo 插件目录的路径指定在 MUJOCO_PLUGIN_PATH 环境变量中。你可以将 MUJOCO_PLUGIN_PATH 环境变量指向你克隆的 MuJoCo 代码库的 plugin 文件夹。

    注意

    对于 macOS,需要从 DMG 中提取文件。按照第 2 步挂载它后,mujoco.framework 目录可以在 /Volumes/MuJoCo 中找到,插件目录可以在 /Volumes/MuJoCo/MuJoCo.app/Contents/MacOS/mujoco_plugin 中找到。这两个目录可以复制到方便的地方,或者你可以使用 MUJOCO_PATH=/Volumes/MuJoCo MUJOCO_PLUGIN_PATH=/Volumes/MuJoCo/MuJoCo.app/Contents/MacOS/mujoco_plugin

    cd dist
    MUJOCO_PATH=/PATH/TO/MUJOCO \
    MUJOCO_PLUGIN_PATH=/PATH/TO/MUJOCO/PLUGIN \
    pip install mujoco-x.y.z.tar.gz
    

Python 绑定现在应该安装好了!要检查它们是否已成功安装,请退出 mujoco 目录并运行 python -c "import mujoco"

提示

作为参考,一个有效的工作构建配置可以在 MuJoCo 的 GitHub 持续集成设置 中找到。

模块#

mujoco 包包含两个子模块:mujoco.rolloutmujoco.minimize

rollout#

mujoco.rolloutmujoco.rollout.Rollout 展示了如何通过 pybind11 将额外的 C/C++ 功能公开为 Python 模块。它在 rollout.cc 中实现,并封装在 rollout.py 中。该模块解决了在 Python 之外实现紧密循环有益处的常见用例:在给定初始状态和控制序列的情况下,推出轨迹(即在循环中调用 mj_step),并返回后续状态和传感器值。如果传递了多个 MjData 实例(每个线程一个)作为参数,则推出将使用内部管理的线程池并行运行。此笔记本展示了如何使用 rollout rollout_colab,以及一些基准测试。

_images/rollout.png

基本用法形式为:

state, sensordata = rollout.rollout(model, data, initial_state, control)
  • model 是 MjModel 的单个实例,或者长度为 nbatch 的同构 MjModel 序列。同构模型具有相同的整数大小,但浮点值可以不同。

  • data 是 MjData 的单个实例,或者长度为 nthread 的兼容 MjData 序列。

  • initial_state 是一个 nbatch x nstate 数组,具有 nbatch 个大小为 nstate 的初始状态,其中 nstate = mj_stateSize(model, mjtState.mjSTATE_FULLPHYSICS)完整物理状态 的大小。

  • control 是一个 nbatch x nstep x ncontrol 的控制数组。控制默认是 mjModel.nu 标准执行器,但可以通过传递可选的 control_spec 位标志来指定任何 用户输入 数组组合。

如果推出发散,当前状态和传感器值将用于填充轨迹的剩余部分。因此,非递增的时间值可用于检测发散的推出。

rollout 函数被设计为计算上无状态的,因此步进流程的所有输入都会被设置,且给定 MjData 实例中已存在的任何值都不会对输出产生影响。

默认情况下,如果 len(data) > 1rollout.rollout 每次调用都会创建一个新的线程池。要跨多次调用重用线程池,请使用 persistent_pool 参数。rollout.rollout 在使用持久池时不是线程安全的。

state, sensordata = rollout.rollout(model, data, initial_state, persistent_pool=True)

池在解释器关闭时或通过调用 rollout.shutdown_persistent_pool 时关闭。

要从多个线程使用多个线程池,请使用 Rollout 对象。

# Pool shutdown upon exiting block.
with rollout.Rollout(nthread=nthread) as rollout_:
 rollout_.rollout(model, data, initial_state)

# Pool shutdown on object deletion or call to rollout_.close().
# To ensure clean shutdown of threads, call close() before interpreter exit.
rollout_ = rollout.Rollout(nthread=nthread)
rollout_.rollout(model, data, initial_state)
rollout_.close()

由于释放了全局解释器锁,此函数也可以使用 Python 线程进行线程化。但是,这不如使用原生线程有效。有关线程化操作的示例,请参阅 rollout_test.py 中的 test_threading 函数。

minimize#

此模块包含与优化相关的实用程序。

minimize.least_squares() 函数实现了一个非线性最小二乘优化器,使用 mju_boxQP 求解顺序二次规划。它在相关笔记本中有记录:lscolab

USD 导出器#

USD 导出器 模块允许用户以 USD 格式 保存场景和轨迹,以便在 NVIDIA Omniverse 或 Blender 等外部渲染器中进行渲染。这些渲染器提供了默认渲染器不具备的更高质量的渲染能力。此外,导出为 USD 允许用户包含不同类型的纹理贴图,使场景中的对象看起来更逼真。

安装#

推荐的安装 USD 导出器所需依赖项的方法是通过 PyPI

pip install mujoco[usd]

这会安装 USD 导出器所需的可选依赖项 usd-corepillow

如果你是从源代码构建,请确保 构建 Python 绑定。然后使用 pip 安装所需的 usd-corepillow 包。

USDExporter#

mujoco.usd.exporter 模块中的 USDExporter 类允许保存完整轨迹,并定义自定义相机和灯光。USDExporter 实例的构造函数参数为:

  • model:MjModel 实例。USD 导出器从模型中读取相关信息,包括有关相机、灯光、纹理和对象几何体的详细信息。

  • max_geom:场景中的最大几何体数量,在实例化内部 mjvScene 时需要。

  • output_directory:存储导出的 USD 文件和所有相关资产的目录名称。将场景/轨迹保存为 USD 文件时,导出器会创建特定的目录结构。

    output_directory_root/
    └-output_directory/
      ├-assets/
      | ├-texture_0.png
      | ├-texture_1.png
      | └-...
      └─frames/
        └-frame_301.usd
    

    使用此文件结构允许用户轻松归档 output_directory。USD 文件中资产的所有路径都是相对的,从而方便在另一台机器上使用 USD 归档。

  • output_directory_root:添加 USD 轨迹的根目录。

  • light_intensity:所有灯光的强度。请注意,强度的单位在不同的渲染器中定义可能不同,因此此值可能需要在特定渲染器基础上进行调整。

  • camera_names:要存储在 USD 文件中的相机列表。在每个时间步,针对定义的每个相机,我们计算其位置和方向,并将该值添加到 USD 中的该帧中。USD 允许存储多个相机。

  • verbose:是否打印来自导出器的日志消息。

如果你希望导出直接从 MJCF 加载的模型,我们提供了一个 演示 脚本,展示了如何操作。此演示文件也可用作 USD 导出功能的示例。

基本用法#

安装可选依赖项后,可以通过 from mujoco.usd import exporter 导入 USD 导出器。

下面演示了使用 USDExporter 的一个简单示例。在初始化过程中,USDExporter 会创建一个空的 USD 阶段,以及资产和帧目录(如果它们尚不存在)。此外,它会为模型中定义的每个纹理生成 .png 文件。每次调用 update_scene 时,导出器都会记录场景中所有几何体、灯光和相机的姿态。

USDExporter 通过维护一个帧计数器在内部跟踪帧。每次调用 update_scene 时,计数器就会递增,并且所有几何体、相机和灯光的姿态都会为相应帧保存。需要注意的是,在调用 update_scene 之前,你可以步进模拟多次。最终的 USD 文件将仅存储上次调用 update_scene 时几何体、灯光和相机的姿态。

import mujoco
from mujoco.usd import exporter

m = mujoco.MjModel.from_xml_path('/path/to/mjcf.xml')
d = mujoco.MjData(m)

# Create the USDExporter
exp = exporter.USDExporter(model=m)

duration = 5
framerate = 60
while d.time < duration:

  # Step the physics
  mujoco.mj_step(m, d)

  if exp.frame_count < d.time * framerate:
    # Update the USD with a new frame
    exp.update_scene(data=d)

# Export the USD file
exp.save_scene(filetype="usd")

USD 导出 API#

  • update_scene(self, data, scene_option):使用用户传入的最新模拟数据更新场景。此函数更新场景中的几何体、相机和灯光。

  • add_light(self, pos, intensity, radius, color, obj_name, light_type):事后向 USD 场景添加具有给定属性的灯光。

  • add_camera(self, pos, rotation_xyz, obj_name):事后向 USD 场景添加具有给定属性的相机。

  • save_scene(self, filetype):使用 USD 文件扩展名 .usd.usda.usdc 导出 USD 场景。

缺失功能#

下面我们列出了 USD 导出器的剩余待办事项。请随时通过在 GitHub 上创建新的 功能请求 来建议其他需求。

  • 添加对额外纹理贴图的支持,包括金属度、遮挡、粗糙度、凹凸等。

  • 添加对 Isaac 在线渲染的支持。

  • 添加对自定义相机的支持。

实用工具#

python/mujoco 目录还包含实用脚本。

msh2obj.py#

msh2obj.py 脚本将表面网格的 遗留 .msh 格式(与同样使用 .msh 的可能包含体积的 gmsh 格式 不同)转换为 OBJ 文件。遗留格式已被弃用,并将在未来的版本中删除。请将所有遗留文件转换为 OBJ。

mujoco-py 迁移#

在 mujoco-py 中,主要入口点是 MjSim 类。用户根据 MJCF 模型(类似于 dm_control.Physics)构造有状态的 MjSim 实例,该实例持有对 mjModel 实例及其关联 mjData 的引用。相比之下,MuJoCo Python 绑定 (mujoco) 采用了上述更低级别的方法:遵循 C 库的设计原则,mujoco 模块本身是无状态的,仅封装了底层的原生结构体和函数。

虽然对 mujoco-py 的完整概述超出了本文档的范围,但我们在此为特定 mujoco-py 功能的非详尽列表提供实现说明:

mujoco_py.load_model_from_xml(bstring)

此工厂函数构造一个有状态的 MjSim 实例。当使用 mujoco 时,用户应按照 上述 说明调用 mujoco.MjModel.from_xml_* 工厂函数。然后,用户负责持有产生的 MjModel 结构体实例,并通过调用 mujoco.MjData(model) 显式生成相应的 MjData

sim.reset(), sim.forward(), sim.step()

如上所述,mujoco 用户需要调用底层库函数,并传递 MjModelMjData 的实例:mujoco.mj_resetData(model, data)mujoco.mj_forward(model, data)mujoco.mj_step(model, data)

sim.get_state(), sim.set_state(state), sim.get_flattened_state(), sim.set_state_from_flattened(state)

正如编程章节中所述,MuJoCo 库的计算在给定特定输入时是确定性的。mujoco-py 实现了获取和设置某些相关字段的方法(同样,dm_control.Physics 也提供了对应于扁平化情况的方法)。此功能在状态与控制一节中有详细描述。

sim.model.get_joint_qvel_addr(joint_name)

这是 mujoco-py 中的一个便利方法,它返回对应于该关节的连续索引列表。该列表从 model.jnt_qposadr[joint_index] 开始,其长度取决于关节类型。mujoco 库本身不提供此功能,但可以使用 model.jnt_qposadr[joint_index]xrange 轻松构建此列表。

sim.model.*_name2id(name)

mujoco-py 在 MjSim 中创建了字典,允许高效查找不同类型对象的索引:site_name2idbody_name2id 等。这些函数替代了函数 mujoco.mj_name2id(model, type_enum, name)mujoco 提供了另一种使用实体名称的方法——命名访问,以及对原生 mj_name2id 的访问。

sim.save(fstream, format_name)

这是 MuJoCo 库(因此也包括 mujoco)具有状态的一个上下文:它在内存中保留了最后一次编译的 XML 副本,该副本用于 mujoco.mj_saveLastXML(fname)。请注意,mujoco-py 的实现有一个方便的额外功能,即在保存之前,位姿(由 sim.data 的状态决定)会被转换为添加到模型中的关键帧。此额外功能目前在 mujoco 中不可用。