Python(Python)#

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

Python 绑定作为 mujoco 包在 PyPI 上分发。这些是低级别的绑定,旨在尽可能直接地访问 MuJoCo 库。然而,为了提供开发人员在典型 Python 库中期望的 API 和语义,绑定在许多地方故意与原始的 MuJoCo API 有所不同,这些差异在本页各处均有记录。

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

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

教程笔记本(Tutorial notebook)#

一个使用 Python 绑定的 MuJoCo 教程可在此处获取: mjcolab

安装(Installation)#

推荐通过 PyPI 安装此包:

pip install mujoco

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

交互式查看器(Interactive viewer)#

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

托管查看器(Managed viewer)#

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

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

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

  • viewer.launch(model, data) 与上面相同,只是可视化器直接在给定的 mjData 实例上操作——退出时 data 对象将被修改。

独立应用程序(Standalone app)#

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

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

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

被动查看器(Passive viewer)#

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 时被拾取,而在 state_only=True 时不会被拾取。

    用户脚本必须调用 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:
    
      # 启用整个场景的线框渲染。
      viewer.user_scn.flags[mujoco.mjtRndFlag.mjRND_WIREFRAME] = 1
      viewer.sync()
    
      while viewer.is_running():
        ...
        # 步进物理。
        mujoco.mj_step(m, d)
    
        # 向场景中间添加一个 3x3x3 的各种颜色球体网格。
        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:
  # 30 秒后自动关闭查看器。
  start = time.time()
  while viewer.is_running() and time.time() - start < 30:
    step_start = time.time()

    # mj_step 可以被也评估策略并在步进物理之前应用控制信号的代码替换。
    mujoco.mj_step(m, d)

    # 查看器选项修改示例:每两秒切换一次接触点。
    with viewer.lock():
      viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(d.time % 2)

    # 拾取物理状态的更改,应用扰动,从 GUI 更新选项。
    viewer.sync()

    # 粗略的时间保持,相对于挂钟会漂移。
    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 来切换这些面板的可见性。

基本用法(Basic usage)#

安装后,可以通过 import mujoco 导入包。结构体、函数、常量和枚举可直接从顶级 mujoco 模块获取。

结构体(Structs)#

绑定包括暴露 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 命名指南,结构体名称以大写字母开头,例如 mjData 在 Python 中变为 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) 进行初始化。当使用这种形式的初始化时,相应的释放函数 mj_freeFoo/mj_deleteFoo 会在 Python 对象被删除时自动调用。用户不需要手动释放资源。

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

函数(Functions)#

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)

枚举和常量(Enums and constants)#

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

最小示例(Minimal example)#

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)

命名访问(Named access)#

大多数设计良好的 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 的字段)与自由度 (DoF) 相关联,而不是与关节相关联。这是因为不同类型的关节具有不同数量的 DoF。然而,我们仍然将这些字段与它们相应的关节相关联,例如通过 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

渲染(Rendering)#

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

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

一旦创建了上下文,用户可以按照 MuJoCo 的标准渲染进行,例如 可视化 部分中记录的那样。

错误处理(Error handling)#

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

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

回调(Callbacks)#

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 获取 C 函数指针的 Python 句柄,并将其传递给 mujoco.set_mjcb_foo。然后绑定将检索底层函数指针并将其直接分配给原始回调指针,并且每次进入回调时**不会**获取 GIL。

模型编辑(Model editing)#

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

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()

构造(Construction)#

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() 方法只能在构造时调用。

资产(Assets)#

所有三种方法都接受一个名为 assets 的可选参数,该参数用于解析 XML 中的资产引用。此参数是一个字典,将资产名称(字符串)映射到资产数据(字节),如下所示:

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

保存为 XML(Save to 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>

附件(Attachment)#

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

  • 将子规范中的身体附加到父规范中的框架: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

# 创建父规范。
parent = mujoco.MjSpec()
body = parent.worldbody.add_body()
frame = parent.worldbody.add_frame()
site = parent.worldbody.add_site()

# 创建子规范。
child = mujoco.MjSpec()
child_body = child.worldbody.add_body()
child_frame = child.worldbody.add_frame()

# 以不同的方式将子规范附加到父规范。
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-')

便利方法(Convenience methods)#

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

命名访问(Named access)#

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

元素列表(Element lists)#

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

元素移除(Element removal)#

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

树遍历(Tree traversal)#

运动学树的遍历由以下方法辅助,这些方法返回与树相关的元素列表:

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

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

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

序列化(Serialization)#

MjSpec 对象可以使用 spec.to_zip(file) 函数及其所有资产进行序列化,其中 file 可以是文件路径或文件对象。为了从 zip 文件加载规范,使用 spec = MjSpec.from_zip(file),其中 file 是 zip 文件的路径或 zip 文件对象。

网格创建(Mesh creation)#

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

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

与 PyMJCF 和 bind 的关系(Relationship to PyMJCF and bind)#

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

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

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

torsos = [data.bind(geom) for geom in spec.geoms if 'torso' in geom.name] # 获取所有名称包含'torso'的几何体的数据绑定对象
pos_x = [torso.xpos[0] for torso in torsos] # 获取所有 torso 几何体的 X 位置
pos_y = [torso.xpos[1] for torso in torsos] # 获取所有 torso 几何体的 Y 位置

使用 bind 方法要求 mjModelmjData 是从 mjSpec 编译的。如果 自上次编译以来向 mjSpec 添加或删除了对象,则会引发错误。

注释(Notes)#

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

从源代码构建(Building from source)#

注意

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

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

  2. 从 GitHub 克隆整个 mujoco 代码库。

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

  2. 进入克隆的 MuJoCo 代码库的 python 目录:

cd mujoco/python
  1. 创建一个虚拟环境:

python3 -m venv /tmp/mujoco
source /tmp/mujoco/bin/activate
  1. 使用 make_sdist.sh 脚本生成一个 源码分发 tarball。

bash make_sdist.sh

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

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

    注意

    对于 macOS,需要从 DMG 中提取文件。 按照步骤 2 挂载后,可以在 /Volumes/MuJoCo 中找到 mujoco.framework 目录, 插件目录可以在 /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 绑定现在应该已经安装好了!要检查它们是否已成功安装,请 cdmujoco 目录之外并运行 python -c "import mujoco"

提示

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

模块(Modules)#

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

rollout#

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

_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 发散,则使用当前状态和传感器值填充轨迹的其余部分。 因此,可以使用非递增的时间值来检测发散的 rollout。

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 对象。基本使用形式是

# 退出块时关闭池。
with rollout.Rollout(nthread=nthread) as rollout_:
 rollout_.rollout(model, data, initial_state)

# 在对象删除或调用 rollout_.close() 时关闭池。
# 为确保线程干净关闭,请在解释器退出前调用 close()。
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 求解顺序 二次规划。在相关的 notebook 中有文档记录:最小二乘 colab notebook

USD 导出器(USD exporter)#

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

安装(Installation)#

安装 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 加载的模型,我们提供了一个 demo 脚本,展示了如何操作。此 演示文件也作为 USD 导出功能的一个示例。

基本用法(Basic usage)#

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

下面,我们演示一个使用 USDExporter 的简单示例。在初始化期间,USDExporter 创建 一个空的 USD 舞台,以及 assets 和 frames 目录(如果它们尚不存在)。此外,它 为模型中定义的每个纹理生成 .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)

# 创建 USDExporter
exp = exporter.USDExporter(model=m)

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

  # 步进物理
  mujoco.mj_step(m, d)

  if exp.frame_count < d.time * framerate:
    # 使用新帧更新 USD
    exp.update_scene(data=d)

# 导出 USD 文件
exp.save_scene(filetype="usd")
USD 导出 API(USD Export 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 之一导出 USD 场景, 或 .usdc

缺失功能(Missing features)#

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

  • 添加对其他纹理贴图的支持,包括金属度、环境光遮蔽、粗糙度、凹凸等。

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

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

实用程序(Utilities)#

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

msh2obj.py#

msh2obj.py 脚本转换 旧版 .msh 格式 的表面网格(不同于也可能使用 .msh 的体积 gmsh 格式)为 OBJ 文件。旧版格式已弃用,将在 未来版本中移除。请将所有旧版文件转换为 OBJ。

mujoco-py 迁移(mujoco-py migration)#

在 mujoco-py 中,主要入口点是 MjSim 类。用户从 MJCF 模型构造一个有状态的 MjSim 实例(类似于 dm_control.Physics),并且这个 实例持有对 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 提供了对应于扁平化情况的方法)。mujoco 不提供这种 抽象,用户需要显式地获取/设置相关字段的值。

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 中不可用。