MuJoCo 教程

本文为 MuJoCo 官方教程的简单翻译.

原文链接如下:

https://colab.research.google.com/github/deepmind/mujoco/blob/main/python/tutorial.ipynb

官方GitHub 链接为 https://github.com/deepmind/mujoco.

安装 MuJoCo

1
!pip install mujoco

检查是否安装成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#@title Check if installation was successful

from google.colab import files

import distutils.util
import subprocess
if subprocess.run('nvidia-smi').returncode:
raise RuntimeError(
'Cannot communicate with GPU. '
'Make sure you are using a GPU Colab runtime. '
'Go to the Runtime menu and select Choose runtime type.')
# Configure MuJoCo to use the EGL rendering backend (requires GPU)
# use WGL for Windows
print('Setting environment variable to use GPU rendering:')
%env MUJOCO_GL=egl

try:
print('Checking that the installation succeeded:')
import mujoco
mujoco.MjModel.from_xml_string('<mujoco/>')
except Exception as e:
raise e from RuntimeError(
'Something went wrong during installation. Check the shell output above '
'for more information.\n'
'If using a hosted Colab runtime, make sure you enable GPU acceleration '
'by going to the Runtime menu and selecting "Choose runtime type".')

print('Installation successful.')

导入相关图形和绘图包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#@title Import packages for plotting and creating graphics
import time
import itertools
import numpy as np
from typing import Callable, NamedTuple, Optional, Union, List

# Graphics and plotting.
print('Installing mediapy:')
!command -v ffmpeg >/dev/null || (apt update && apt install -y ffmpeg)
!pip install -q mediapy
import mediapy as media
import matplotlib.pyplot as plt

# More legible printing from numpy.
np.set_printoptions(precision=3, suppress=True, linewidth=100)

MuJoCo 基础

我们从定义与读入一个简单的模型来开始:

1
2
3
4
5
6
7
8
9
xml = """
<mujoco>
<worldbody>
<geom name="red_box" type="box" size=".2 .2 .2" rgba="1 0 0 1"/>
<geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)

这段 xml 代码依据 MuJoCo 的 MJCF 编写, 这是一种基于 XML 的建模语言.

  • 唯一必需的元素为 <mujoco>. 最小的有效 MJCF 模型为 <mujoco/> , 这是一个完全空模型.
  • 所有的物理元素放在 <worldbody> 中, 这始终为顶层物体并构成笛卡尔坐标系的全局原点.
  • 我们在世界中定义两个几何体 red_boxgreen_sphere.
  • 问题: red_box 没有位置, the green_sphere 没有类型, 为什么?
    • 答: MJCF 属性中有 default values. 默认位置为 0 0 0, 默认几何类型为 sphere. MJCF 语言在文档 XML Reference chapter 中有相关解释.

方法 from_xml_string() 调用了模型编译器, 其创建了一个二进制 mjModel 实例.

mjModel

MuJoCo 中的 mjModel, 包含了模型描述, 比如所有均不随时间变化的量. mjModel 的完整描述可以在头文件 mjmodel.h 的末尾找到. 注意, 头文件包含简短的有用的行内注释, 用于描述每个字段.

可以在模型 mjModel 找到一些量, 比如场景中的几何体数量ngeom, 以及它们各自的颜色 geom_rgba:

1
2
model.ngeom
model.geom_rgba

2

array([[1., 0., 0., 1.],
[0., 1., 0., 1.]], dtype=float32)

Named access

MuJoCo Python 的绑定使用名称提供了便捷的访问器, 在没有名称字符串的情况下调用 model.geom() 访问器会生成一个方便的错误提示, 告诉我们有效的名称是什么.

1
2
3
4
try:
model.geom()
except KeyError as e:
print(e)

"Invalid name". Valid names: ['green_sphere', 'red_box']

调用命名访问器而不指定属性将告诉我们所有有效的属性是什么:

1
model.geom('green_sphere')

<_MjModelGeomViews
bodyid: array([0], dtype=int32)
conaffinity: array([1], dtype=int32)
condim: array([3], dtype=int32)
contype: array([1], dtype=int32)
dataid: array([-1], dtype=int32)
friction: array([1. , 0.005, 0. ])
gap: array([0.])
group: array([0], dtype=int32)
id: 1
margin: array([0.])
matid: array([-1], dtype=int32)
name: 'green_sphere'
pos: array([0.2, 0.2, 0.2])
priority: array([0], dtype=int32)
quat: array([1., 0., 0., 0.])
rbound: array([0.1])
rgba: array([0., 1., 0., 1.], dtype=float32)
sameframe: array([0], dtype=uint8)
size: array([0.1, 0. , 0. ])
solimp: array([0.9 , 0.95 , 0.001, 0.5 , 2. ])
solmix: array([1.])
solref: array([0.02, 1. ])
type: array([2], dtype=int32)
user: array([], dtype=float64)

让我们获取 green_sphere 的 rgba 值:

1
model.geom('green_sphere').rgba

array([0., 1., 0., 1.], dtype=float32)

这个函数是 MuJoCo 的 mj_name2id 函数的一个方便的快捷方式:

1
2
id = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_GEOM, 'green_sphere')
model.geom_rgba[id, :]

array([0., 1., 0., 1.], dtype=float32)

类似地, 只读的 idname 属性可用于从 id 转换到 name 以及反之:

1
2
3
print('id of "green_sphere": ', model.geom('green_sphere').id)
print('name of geom 1: ', model.geom(1).name)
print('name of body 0: ', model.body(0).name)

id of "green_sphere": 1
name of geom 1: green_sphere
name of body 0: world

注意, 第 0 个物体总是 world. 不能重命名.

idname 属性在 Python 推导式中很有用:

1
[model.geom(i).name for i in range(model.ngeom)]

['red_box', 'green_sphere']

mjData

mjData 包含依赖于它的状态和数量. 状态由时间, 广义位置和广义速度组成, 分别为data.time, data.qposdata.qvel. 为了创建一个新的 mjData,我们只需要 mjModel.

1
data = mujoco.MjData(model)

mjData 还包含状态函数, 例如世界坐标系中对象的笛卡尔位置. 两个几何体的 (x, y, z) 位置在 data.geom_xpos 中:

1
print(data.geom_xpos)

[[0. 0. 0.]
[0. 0. 0.]]

等等, 为什么两个几何体都在原点? 绿色球体不是有位移吗? 答案是 mjData 中的派生量需要显式地传播(见下文). 在我们的例子中, 所需的最小函数是 mj_kinematics, 它计算所有对象(不包括相机和灯光)的全局笛卡尔姿态.

1
2
3
4
5
mujoco.mj_kinematics(model, data)
print('raw access:\n', data.geom_xpos)

# MjData also supports named access:
print('\nnamed access:\n', data.geom('green_sphere').xpos)

raw access:
[[0. 0. 0. ]
[0.2 0.2 0.2]]

named access:
[0.2 0.2 0.2]

基本的渲染、模拟和动画

要实现渲染, 我们需要实例化一个 Renderer 对象并调用它的 render 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml = """
<mujoco>
<worldbody>
<geom name="red_box" type="box" size=".2 .2 .2" rgba="1 0 0 1"/>
<geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
</worldbody>
</mujoco>
"""
# Make model and data
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)

# Make renderer, render and show the pixels
renderer = mujoco.Renderer(model)
media.show_image(renderer.render())

嗯… 为什么是黑色像素?

: 出于与上面相同的原因, 我们首先需要传播 mjData 中的值. 这次我们将调用 mj_forward, 它将调用整个管线, 直到计算加速度, 即, 它计算 , 其中 是状态. 这个函数所做的比我们实际需要的要多, 但是除非我们关心节省计算时间, 否则调用 mj_forward 是一个很好的实践, 因为这样我们就知道我们不会遗漏任何东西.

我们还需要更新 mjvScene, 这是一个由渲染器持有的描述可视化场景的对象. 我们稍后将看到场景可以包括不属于物理模型的可视化对象.

1
2
3
4
mujoco.mj_forward(model, data)
renderer.update_scene(data)

media.show_image(renderer.render())

这是有效的, 但这张照片有点暗. 让我们添加一个灯光并重新渲染.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml = """
<mujoco>
<worldbody>
<light name="top" pos="0 0 1"/>
<geom name="red_box" type="box" size=".2 .2 .2" rgba="1 0 0 1"/>
<geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
renderer = mujoco.Renderer(model)

mujoco.mj_forward(model, data)
renderer.update_scene(data)

media.show_image(renderer.render())

好多了!

注意, mjModel 实例中的所有值都是可写入的. 虽然通常不建议这样做, 而是建议更改 XML 中的值, 因为很容易创建无效的模型, 但有些值是可以安全写入的, 例如颜色:

1
2
3
4
# Run this cell multiple times for different colors
model.geom('red_box').rgba[:3] = np.random.rand(3)
renderer.update_scene(data)
media.show_image(renderer.render())

仿真

现在进行仿真并制作一个视频. 我们将使用 MuJoCo 的主要高级函数 mj_step, 该函数每步更新状态 .

注意, 在下面的代码块中, 我们不会在每次调用 mj_step 后进行渲染. 这是因为默认的时间步长是 2ms, 我们想要一个 60fps 的视频, 而不是 500fps.

1
2
3
4
5
6
7
8
9
10
11
12
13
duration = 3.8  # (seconds)
framerate = 60 # (Hz)

# Simulate and display video.
frames = []
mujoco.mj_resetData(model, data) # Reset state and time.
while data.time < duration:
mujoco.mj_step(model, data)
if len(frames) < data.time * framerate:
renderer.update_scene(data)
pixels = renderer.render()
frames.append(pixels)
media.show_video(frames, fps=framerate)

嗯… 视频在播放, 但是没有东西在动, 为什么呢?

这是因为这个模型没有自由度)(DoFs). 运动的(有惯性的)物体叫做 bodies. 我们通过给 bodies 添加 joints 来添加 DoFs, 指定它们如何相对于 parents 移动. 让我们创建一个包含几何体的新 body, 添加一个铰链关节并重新渲染, 同时使用可视化选项对象 MjvOption 来可视化关节轴。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
xml = """
<mujoco>
<worldbody>
<light name="top" pos="0 0 1"/>
<body name="box_and_sphere" euler="0 0 -30">
<joint name="swing" type="hinge" axis="1 -1 0" pos="-.2 -.2 -.2"/>
<geom name="red_box" type="box" size=".2 .2 .2" rgba="1 0 0 1"/>
<geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
renderer = mujoco.Renderer(model)

# enable joint visualization option:
scene_option = mujoco.MjvOption()
scene_option.flags[mujoco.mjtVisFlag.mjVIS_JOINT] = True

duration = 3.8 # (seconds)
framerate = 60 # (Hz)

frames = []
mujoco.mj_resetData(model, data)
while data.time < duration:
mujoco.mj_step(model, data)
if len(frames) < data.time * framerate:
renderer.update_scene(data, scene_option=scene_option)
pixels = renderer.render()
frames.append(pixels)

# Simulate and display video.
media.show_video(frames, fps=framerate)

注意, 我们将 box_and_sphere body 绕 Z (垂直)轴旋转30°, 指令 euler="0 0 -30". 这样做是为了强调运动树中元素的姿态总是相对于它们的父体, 所以我们的两个几何体也通过这个变换旋转.

物理选项位于 mjModel.opt 中, 例如时间步:

1
model.opt.timestep

0.002

让我们翻转重力并重新渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
print('default gravity', model.opt.gravity)
model.opt.gravity = (0, 0, 10)
print('flipped gravity', model.opt.gravity)

frames = []
mujoco.mj_resetData(model, data)
while data.time < duration:
mujoco.mj_step(model, data)
if len(frames) < data.time * framerate:
renderer.update_scene(data, scene_option=scene_option)
pixels = renderer.render()
frames.append(pixels)

media.show_video(frames, fps=60)

default gravity [ 0. 0. -9.81]
flipped gravity [ 0. 0. 10.]

我们也可以在 XML 中使用顶级元素 <option> 来实现它:

1
2
3
4
<mujoco>
<option gravity="0 0 10"/>
...
</mujoco>

理解自由度

在现实世界中, 所有刚体都有 6 个自由度: 3 个平移和 3 个旋转. 现实世界中的关节充当约束, 消除了由关节连接的物体的相对自由度. 一些物理模拟软件使用这种被称为 “Cartesian” 或 “subtractive” 的表示法, 但它的效率很低. MuJoCo 使用一种被称为 “Lagrangian”, “generalized” 或 “additive” 的表示, 其中对象没有自由度, 除非使用关节显式地添加.

我们的模型有一个铰链关节, 有一个自由度, 整个状态由这个关节的角度和角速度定义. 这是系统的广义位置和速度.

1
2
3
print('Total number of DoFs in the model:', model.nv)
print('Generalized positions:', data.qpos)
print('Generalized velocities:', data.qvel)

Total number of DoFs in the model: 1
Generalized positions: [1.392]
Generalized velocities: [-3.79]

MuJoCo 使用广义坐标的原因是在渲染或读取对象的全局位置之前需要调用函数(例如 mj_forward) - 笛卡尔位置是从广义位置派生出来的, 需要显式计算.

示例: 用自反转的“tippe-top”模拟自由体

一个自由物体是一个具有 6 个自由度的自由关节的物体, 即 3 个平移和 3 个旋转. 我们其实可以给 box_and_sphere 体一个自由关节, 然后看着它下落,但是让我们看一些更有趣的东西. 一个 “tippe top” 是一个旋转的玩具, 它可以自己翻转(视频维基百科). 我们将其建模如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
tippe_top = """
<mujoco model="tippe top">
<option integrator="RK4"/>

<asset>
<texture name="grid" type="2d" builtin="checker" rgb1=".1 .2 .3"
rgb2=".2 .3 .4" width="300" height="300"/>
<material name="grid" texture="grid" texrepeat="8 8" reflectance=".2"/>
</asset>

<worldbody>
<geom size=".2 .2 .01" type="plane" material="grid"/>
<light pos="0 0 .6"/>
<camera name="closeup" pos="0 -.1 .07" xyaxes="1 0 0 0 1 2"/>
<body name="top" pos="0 0 .02">
<freejoint/>
<geom name="ball" type="sphere" size=".02" />
<geom name="stem" type="cylinder" pos="0 0 .02" size="0.004 .008"/>
<geom name="ballast" type="box" size=".023 .023 0.005" pos="0 0 -.015"
contype="0" conaffinity="0" group="3"/>
</body>
</worldbody>

<keyframe>
<key name="spinning" qpos="0 0 0.02 1 0 0 0" qvel="0 0 0 0 1 200" />
</keyframe>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(tippe_top)
renderer = mujoco.Renderer(model)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
renderer.update_scene(data, camera="closeup")
media.show_image(renderer.render())

注意这个模型定义的几个新特性:

  1. 一个使用 <freejoint/> 标签添加的6自由度自由关节.
  2. 我们使用 <option/> 标签将积分器设置为 4 阶 Runge Kutta. Runge-Kutta 具有比默认的欧拉积分器更高的收敛速率, 在许多情况下, 在给定的时间步长下提高了精度.
  3. 我们在 <asset/> 标签中定义地板的网格材质, 并在 "floor” 几何体中引用它.
  4. 我们使用一种看不见的, 不碰撞的 box 几何体, 称为 ballast , 来降低顶部的质心. 低质心是发生翻转行为所必需的(与直觉相反).
  5. 我们将初始旋转状态保存为关键帧. 它绕Z轴有很高的旋转速度, 但并不完全朝向世界坐标轴, 这引入了翻转所需的对称性破坏.
  6. 我们在模型中定义了一个 <camera>, 然后使用 <camera> 参数对 update_scene() 进行渲染. 让我们来检查一下现在的状态:
1
2
print('positions', data.qpos)
print('velocities', data.qvel)

positions [0. 0. 0.02 1. 0. 0. 0. ]
velocities [0. 0. 0. 0. 0. 0.]

速度很容易解释, 6 个 0, 每个自由度一个. 那么长度为 7 的位置呢? 我们可以看到 body 最初的 2 厘米高度; 后面的 4 个数字是三维朝向, 由单位四元数定义. 三维朝向用 4 个数字表示, 角速度用 3 个数字表示. 有关更多信息, 请参阅维基百科关于四元数和空间旋转的文章.

让我们做一个视频:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
duration = 7    # (seconds)
framerate = 60 # (Hz)

# Simulate and display video.
frames = []
mujoco.mj_resetDataKeyframe(model, data, 0) # Reset the state to keyframe 0
while data.time < duration:
mujoco.mj_step(model, data)
if len(frames) < data.time * framerate:
renderer.update_scene(data, "closeup")
pixels = renderer.render()
frames.append(pixels)

media.show_video(frames, fps=framerate)

测量 mjData 中的值

如上所述, mjData 结构包含由模拟产生的动态变量和中间结果, 这些结果预计会在每个时间步上发生变化. 下面我们模拟了 2000 个时间步长, 并绘制了杆顶的角速度和高度作为时间的函数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
timevals = []
angular_velocity = []
stem_height = []

# Simulate and save data
mujoco.mj_resetDataKeyframe(model, data, 0)
while data.time < duration:
mujoco.mj_step(model, data)
timevals.append(data.time)
angular_velocity.append(data.qvel[3:6].copy())
stem_height.append(data.geom_xpos[2,2]);

dpi = 120
width = 600
height = 800
figsize = (width / dpi, height / dpi)
_, ax = plt.subplots(2, 1, figsize=figsize, dpi=dpi, sharex=True)

ax[0].plot(timevals, angular_velocity)
ax[0].set_title('angular velocity')
ax[0].set_ylabel('radians / second')

ax[1].plot(timevals, stem_height)
ax[1].set_xlabel('time (seconds)')
ax[1].set_ylabel('meters')
_ = ax[1].set_title('stem height')

实例: 一个混沌摆

下面是一个混沌摆模型, 与旧金山探索博物馆的这个模型相似.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
chaotic_pendulum = """
<mujoco>
<option timestep=".001">
<flag energy="enable" contact="disable"/>
</option>

<default>
<joint type="hinge" axis="0 -1 0"/>
<geom type="capsule" size=".02"/>
</default>

<worldbody>
<light pos="0 -.4 1"/>
<camera name="fixed" pos="0 -1 0" xyaxes="1 0 0 0 0 1"/>
<body name="0" pos="0 0 .2">
<joint name="root"/>
<geom fromto="-.2 0 0 .2 0 0" rgba="1 1 0 1"/>
<geom fromto="0 0 0 0 0 -.25" rgba="1 1 0 1"/>
<body name="1" pos="-.2 0 0">
<joint/>
<geom fromto="0 0 0 0 0 -.2" rgba="1 0 0 1"/>
</body>
<body name="2" pos=".2 0 0">
<joint/>
<geom fromto="0 0 0 0 0 -.2" rgba="0 1 0 1"/>
</body>
<body name="3" pos="0 0 -.25">
<joint/>
<geom fromto="0 0 0 0 0 -.2" rgba="0 0 1 1"/>
</body>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(chaotic_pendulum)
renderer = mujoco.Renderer(model, 480, 640)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
renderer.update_scene(data, camera="fixed")
media.show_image(renderer.render())

Timing

让我们来看看它在运行时的视频, 同时我们对组件进行计时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# setup
n_seconds = 6
framerate = 30 # Hz
n_frames = int(n_seconds * framerate)
frames = []
renderer = mujoco.Renderer(model, 240, 320)

# set initial state
mujoco.mj_resetData(model, data)
data.joint('root').qvel = 10

# simulate and record frames
frame = 0
sim_time = 0
render_time = 0
n_steps = 0
for i in range(n_frames):
while data.time * framerate < i:
tic = time.time()
mujoco.mj_step(model, data)
sim_time += time.time() - tic
n_steps += 1
tic = time.time()
renderer.update_scene(data, "fixed")
frame = renderer.render()
render_time += time.time() - tic
frames.append(frame)

# print timing and play video
step_time = 1e6*sim_time/n_steps
step_fps = n_steps/sim_time
print(f'simulation: {step_time:5.3g} μs/step ({step_fps:5.0f}Hz)')
frame_time = 1e6*render_time/n_frames
frame_fps = n_frames/render_time
print(f'rendering: {frame_time:5.3g} μs/frame ({frame_fps:5.0f}Hz)')
print('\n')

# show video
media.show_video(frames, fps=framerate)

simulation: 3.74 μs/step (267581Hz)
rendering: 3.31e+04 μs/frame ( 30Hz)

注意, 渲染比模拟物理要慢得多.

Chaos

这是一个混沌系统(初始条件下的小扰动会迅速累积):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
PERTURBATION = 1e-7
SIM_DURATION = 10 # seconds
NUM_REPEATS = 8

# preallocate
n_steps = int(SIM_DURATION / model.opt.timestep)
sim_time = np.zeros(n_steps)
angle = np.zeros(n_steps)
energy = np.zeros(n_steps)

# prepare plotting axes
_, ax = plt.subplots(2, 1, figsize=(8, 6), sharex=True)

# simulate NUM_REPEATS times with slightly different initial conditions
for _ in range(NUM_REPEATS):
# initialize
mujoco.mj_resetData(model, data)
data.qvel[0] = 10 # root joint velocity
# perturb initial velocities
data.qvel[:] += PERTURBATION * np.random.randn(model.nv)

# simulate
for i in range(n_steps):
mujoco.mj_step(model, data)
sim_time[i] = data.time
angle[i] = data.joint('root').qpos
energy[i] = data.energy[0] + data.energy[1]

# plot
ax[0].plot(sim_time, angle)
ax[1].plot(sim_time, energy)

# finalize plot
ax[0].set_title('root angle')
ax[0].set_ylabel('radian')
ax[1].set_title('total energy')
ax[1].set_ylabel('Joule')
ax[1].set_xlabel('second')
plt.tight_layout()

时间步长和精度

问题: 为什么能量会发生变化? 没有摩擦和阻尼, 这个系统应该节省能量.

答: 因为时间的离散化.

如果我们减少时间步长, 我们将获得更好的精度和更好的节能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SIM_DURATION = 10 # (seconds)
TIMESTEPS = np.power(10, np.linspace(-2, -4, 5))

# prepare plotting axes
_, ax = plt.subplots(1, 1)

for dt in TIMESTEPS:
# set timestep, print
model.opt.timestep = dt

# allocate
n_steps = int(SIM_DURATION / model.opt.timestep)
sim_time = np.zeros(n_steps)
energy = np.zeros(n_steps)

# initialize
mujoco.mj_resetData(model, data)
data.qvel[0] = 9 # root joint velocity

# simulate
print('{} steps at dt = {:2.2g}ms'.format(n_steps, 1000*dt))
for i in range(n_steps):
mujoco.mj_step(model, data)
sim_time[i] = data.time
energy[i] = data.energy[0] + data.energy[1]

# plot
ax.plot(sim_time, energy, label='timestep = {:2.2g}ms'.format(1000*dt))

# finalize plot
ax.set_title('energy')
ax.set_ylabel('Joule')
ax.set_xlabel('second')
ax.legend(frameon=True);
plt.tight_layout()

1000 steps at dt = 10ms
3162 steps at dt = 3.2ms
10000 steps at dt = 1ms
31622 steps at dt = 0.32ms
100000 steps at dt = 0.1ms

时间步长和敛散性

当我们增加时间步长时, 模拟会迅速发散:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SIM_DURATION = 10 # (seconds)
TIMESTEPS = np.power(10, np.linspace(-2, -1.5, 7))

# get plotting axes
ax = plt.gca()

for dt in TIMESTEPS:
# set timestep
model.opt.timestep = dt

# allocate
n_steps = int(SIM_DURATION / model.opt.timestep)
sim_time = np.zeros(n_steps)
energy = np.zeros(n_steps) * np.nan
speed = np.zeros(n_steps) * np.nan

# initialize
mujoco.mj_resetData(model, data)
data.qvel[0] = 11 # set root joint velocity

# simulate
print('simulating {} steps at dt = {:2.2g}ms'.format(n_steps, 1000*dt))
for i in range(n_steps):
mujoco.mj_step(model, data)
if data.warning.number.any():
warning_index = np.nonzero(data.warning.number)[0]
warning = mujoco.mjtWarning(warning_index).name
print(f'stopped due to divergence ({warning}) at timestep {i}.\n')
break
sim_time[i] = data.time
energy[i] = sum(abs(data.qvel))
speed[i] = np.linalg.norm(data.qvel)

# plot
ax.plot(sim_time, energy, label='timestep = {:2.2g}ms'.format(1000*dt))
ax.set_yscale('log')

# finalize plot
ax.set_ybound(1, 1e3)
ax.set_title('energy')
ax.set_ylabel('Joule')
ax.set_xlabel('second')
ax.legend(frameon=True, loc='lower right');
plt.tight_layout()

simulating 1000 steps at dt = 10ms
stopped due to divergence (mjWARN_BADQACC) at timestep 385.

simulating 825 steps at dt = 12ms
stopped due to divergence (mjWARN_BADQACC) at timestep 322.

simulating 681 steps at dt = 15ms
stopped due to divergence (mjWARN_BADQACC) at timestep 166.

simulating 562 steps at dt = 18ms
stopped due to divergence (mjWARN_BADQACC) at timestep 105.

simulating 464 steps at dt = 22ms
stopped due to divergence (mjWARN_BADQACC) at timestep 84.

simulating 383 steps at dt = 26ms
stopped due to divergence (mjWARN_BADQACC) at timestep 61.

simulating 316 steps at dt = 32ms
stopped due to divergence (mjWARN_BADQACC) at timestep 45.

接触力

让我们回到我们的盒子和球体的例子, 给它一个自由关节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
free_body_MJCF = """
<mujoco>
<asset>
<texture name="grid" type="2d" builtin="checker" rgb1=".1 .2 .3"
rgb2=".2 .3 .4" width="300" height="300" mark="edge" markrgb=".2 .3 .4"/>
<material name="grid" texture="grid" texrepeat="2 2" texuniform="true"
reflectance=".2"/>
</asset>

<worldbody>
<light pos="0 0 1" mode="trackcom"/>
<geom name="ground" type="plane" pos="0 0 -.5" size="2 2 .1" material="grid" solimp=".99 .99 .01" solref=".001 1"/>
<body name="box_and_sphere" pos="0 0 0">
<freejoint/>
<geom name="red_box" type="box" size=".1 .1 .1" rgba="1 0 0 1" solimp=".99 .99 .01" solref=".001 1"/>
<geom name="green_sphere" size=".06" pos=".1 .1 .1" rgba="0 1 0 1"/>
<camera name="fixed" pos="0 -.6 .3" xyaxes="1 0 0 0 1 2"/>
<camera name="track" pos="0 -.6 .3" xyaxes="1 0 0 0 1 2" mode="track"/>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(free_body_MJCF)
renderer = mujoco.Renderer(model, 400, 600)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
renderer.update_scene(data, "fixed")
media.show_image(renderer.render())

让我们渲染这个 body 在地板上滚动, 慢动作, 同时可视化接触点和力:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
n_frames = 200
height = 240
width = 320
frames = []
renderer = mujoco.Renderer(model, height, width)

# visualize contact frames and forces, make body transparent
options = mujoco.MjvOption()
mujoco.mjv_defaultOption(options)
options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = True
options.flags[mujoco.mjtVisFlag.mjVIS_CONTACTFORCE] = True
options.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] = True

# tweak scales of contact visualization elements
model.vis.scale.contactwidth = 0.1
model.vis.scale.contactheight = 0.03
model.vis.scale.forcewidth = 0.05
model.vis.map.force = 0.3

# random initial rotational velocity:
mujoco.mj_resetData(model, data)
data.qvel[3:6] = 5*np.random.randn(3)

# simulate and render
for i in range(n_frames):
while data.time < i/120.0: #1/4x real time
mujoco.mj_step(model, data)
renderer.update_scene(data, "track", options)
frame = renderer.render()
frames.append(frame)

# show video
media.show_video(frames, fps=30)

分析接触力

让我们重新运行上述模拟(使用不同的随机初始条件)并绘制与接触相关的一些值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
n_steps = 499

# allocate
sim_time = np.zeros(n_steps)
ncon = np.zeros(n_steps)
force = np.zeros((n_steps,3))
velocity = np.zeros((n_steps, model.nv))
penetration = np.zeros(n_steps)
acceleration = np.zeros((n_steps, model.nv))
forcetorque = np.zeros(6)

# random initial rotational velocity:
mujoco.mj_resetData(model, data)
data.qvel[3:6] = 2*np.random.randn(3)

# simulate and save data
for i in range(n_steps):
mujoco.mj_step(model, data)
sim_time[i] = data.time
ncon[i] = data.ncon
velocity[i] = data.qvel[:]
acceleration[i] = data.qacc[:]
# iterate over active contacts, save force and distance
for j,c in enumerate(data.contact):
mujoco.mj_contactForce(model, data, j, forcetorque)
force[i] += forcetorque[0:3]
penetration[i] = min(penetration[i], c.dist)
# we could also do
# force[i] += data.qfrc_constraint[0:3]
# do you see why?

# plot
_, ax = plt.subplots(3, 2, sharex=True, figsize=(10, 10))

lines = ax[0,0].plot(sim_time, force)
ax[0,0].set_title('contact force')
ax[0,0].set_ylabel('Newton')
ax[0,0].legend(iter(lines), ('normal z', 'friction x', 'friction y'));

ax[1,0].plot(sim_time, acceleration)
ax[1,0].set_title('acceleration')
ax[1,0].set_ylabel('(meter,radian)/s/s')

ax[2,0].plot(sim_time, velocity)
ax[2,0].set_title('velocity')
ax[2,0].set_ylabel('(meter,radian)/s')
ax[2,0].set_xlabel('second')

ax[0,1].plot(sim_time, ncon)
ax[0,1].set_title('number of contacts')
ax[0,1].set_yticks(range(6))

ax[1,1].plot(sim_time, force[:,0])
ax[1,1].set_yscale('log')
ax[1,1].set_title('normal (z) force - log scale')
ax[1,1].set_ylabel('Newton')
z_gravity = -model.opt.gravity[2]
mg = model.body("box_and_sphere").mass[0] * z_gravity
mg_line = ax[1,1].plot(sim_time, np.ones(n_steps)*mg, label='m*g', linewidth=1)
ax[1,1].legend()

ax[2,1].plot(sim_time, 1000*penetration)
ax[2,1].set_title('penetration depth')
ax[2,1].set_ylabel('millimeter')
ax[2,1].set_xlabel('second')

plt.tight_layout()

摩擦力

让我们看看改变摩擦值的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
MJCF = """
<mujoco>
<asset>
<texture name="grid" type="2d" builtin="checker" rgb1=".1 .2 .3"
rgb2=".2 .3 .4" width="300" height="300" mark="none"/>
<material name="grid" texture="grid" texrepeat="6 6"
texuniform="true" reflectance=".2"/>
<material name="wall" rgba='.5 .5 .5 1'/>
</asset>

<default>
<geom type="box" size=".05 .05 .05" />
<joint type="free"/>
</default>

<worldbody>
<light name="light" pos="-.2 0 1"/>
<geom name="ground" type="plane" size=".5 .5 10" material="grid"
zaxis="-.3 0 1" friction=".1"/>
<camera name="y" pos="-.1 -.6 .3" xyaxes="1 0 0 0 1 2"/>
<body pos="0 0 .1">
<joint/>
<geom/>
</body>
<body pos="0 .2 .1">
<joint/>
<geom friction=".33"/>
</body>
</worldbody>

</mujoco>
"""
n_frames = 60
height = 300
width = 300
frames = []

# load
model = mujoco.MjModel.from_xml_string(MJCF)
data = mujoco.MjData(model)
renderer = mujoco.Renderer(model, height, width)

# simulate and render
mujoco.mj_resetData(model, data)
for i in range(n_frames):
while data.time < i/30.0:
mujoco.mj_step(model, data)
renderer.update_scene(data, "y")
frame = renderer.render()
frames.append(frame)
media.show_video(frames, fps=30)

肌腱, 执行器和传感器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
MJCF = """
<mujoco>
<asset>
<texture name="grid" type="2d" builtin="checker" rgb1=".1 .2 .3"
rgb2=".2 .3 .4" width="300" height="300" mark="none"/>
<material name="grid" texture="grid" texrepeat="1 1"
texuniform="true" reflectance=".2"/>
</asset>

<worldbody>
<light name="light" pos="0 0 1"/>
<geom name="floor" type="plane" pos="0 0 -.5" size="2 2 .1" material="grid"/>
<site name="anchor" pos="0 0 .3" size=".01"/>
<camera name="fixed" pos="0 -1.3 .5" xyaxes="1 0 0 0 1 2"/>

<geom name="pole" type="cylinder" fromto=".3 0 -.5 .3 0 -.1" size=".04"/>
<body name="bat" pos=".3 0 -.1">
<joint name="swing" type="hinge" damping="1" axis="0 0 1"/>
<geom name="bat" type="capsule" fromto="0 0 .04 0 -.3 .04"
size=".04" rgba="0 0 1 1"/>
</body>

<body name="box_and_sphere" pos="0 0 0">
<joint name="free" type="free"/>
<geom name="red_box" type="box" size=".1 .1 .1" rgba="1 0 0 1"/>
<geom name="green_sphere" size=".06" pos=".1 .1 .1" rgba="0 1 0 1"/>
<site name="hook" pos="-.1 -.1 -.1" size=".01"/>
<site name="IMU"/>
</body>
</worldbody>

<tendon>
<spatial name="wire" limited="true" range="0 0.35" width="0.003">
<site site="anchor"/>
<site site="hook"/>
</spatial>
</tendon>

<actuator>
<motor name="my_motor" joint="swing" gear="1"/>
</actuator>

<sensor>
<accelerometer name="accelerometer" site="IMU"/>
</sensor>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(MJCF)
renderer = mujoco.Renderer(model, 480, 480)
data = mujoco.MjData(model)
mujoco.mj_forward(model, data)
renderer.update_scene(data, "fixed")
media.show_image(renderer.render())

驱动球棒和被动的皮纳塔.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
n_frames = 180
height = 240
width = 320
frames = []
fps = 60.0
times = []
sensordata = []

renderer = mujoco.Renderer(model, height, width)

# constant actuator signal
mujoco.mj_resetData(model, data)
data.ctrl = 20

# simulate and render
for i in range(n_frames):
while data.time < i/fps:
mujoco.mj_step(model, data)
times.append(data.time)
sensordata.append(data.sensor('accelerometer').data.copy())
renderer.update_scene(data, "fixed")
frame = renderer.render()
frames.append(frame)

media.show_video(frames, fps=fps)

让我们绘制加速度传感器测量的值:

1
2
3
4
5
6
7
8
9
10
ax = plt.gca()

ax.plot(np.asarray(times), np.asarray(sensordata), label='timestep = {:2.2g}ms'.format(1000*dt))

# finalize plot
ax.set_title('Accelerometer values')
ax.set_ylabel('meter/second^2')
ax.set_xlabel('second')
ax.legend(frameon=True, loc='lower right');
plt.tight_layout()

注意 body 被球棒击中的瞬间是如何在加速度测量中清晰可见的.

高级渲染

与关节可视化一样, 附加的渲染选项作为参数公开给 render 方法.

让我们回到第一个模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml = """
<mujoco>
<worldbody>
<light name="top" pos="0 0 1"/>
<body name="box_and_sphere" euler="0 0 -30">
<joint name="swing" type="hinge" axis="1 -1 0" pos="-.2 -.2 -.2"/>
<geom name="red_box" type="box" size=".2 .2 .2" rgba="1 0 0 1"/>
<geom name="green_sphere" pos=".2 .2 .2" size=".1" rgba="0 1 0 1"/>
</body>
</worldbody>
</mujoco>
"""
model = mujoco.MjModel.from_xml_string(xml)
renderer = mujoco.Renderer(model)
data = mujoco.MjData(model)

mujoco.mj_forward(model, data)
renderer.update_scene(data)
media.show_image(renderer.render())

启用透明度和坐标轴可视化

1
2
3
4
5
6
7
#@title Enable transparency and frame visualization

scene_option.frame = mujoco.mjtFrame.mjFRAME_GEOM
scene_option.flags[mujoco.mjtVisFlag.mjVIS_TRANSPARENT] = True
renderer.update_scene(data, scene_option=scene_option)
frame = renderer.render()
media.show_image(frame)

深度渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#@title Depth rendering

# update renderer to render depth
renderer.enable_depth_rendering()

# reset the scene
renderer.update_scene(data)

# depth is a float array, in meters.
depth = renderer.render()

# Shift nearest values to the origin.
depth -= depth.min()
# Scale by 2 mean distances of near rays.
depth /= 2*depth[depth <= 1].mean()
# Scale to [0, 255]
pixels = 255*np.clip(depth, 0, 1)

media.show_image(pixels.astype(np.uint8))

renderer.disable_depth_rendering()

分割渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#@title Segmentation rendering

# update renderer to render segmentation
renderer.enable_segmentation_rendering()

# reset the scene
renderer.update_scene(data)

seg = renderer.render()

# Display the contents of the first channel, which contains object
# IDs. The second channel, seg[:, :, 1], contains object types.
geom_ids = seg[:, :, 0]
# Infinity is mapped to -1
geom_ids = geom_ids.astype(np.float64) + 1
# Scale to [0, 1]
geom_ids = geom_ids / geom_ids.max()
pixels = 255*geom_ids
media.show_image(pixels.astype(np.uint8))

renderer.disable_segmentation_rendering()

相机矩阵

有关相机矩阵的描述, 请参阅维基百科上关于相机矩阵的文章.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def compute_camera_matrix(renderer, data):
"""Returns the 3x4 camera matrix."""
# If the camera is a 'free' camera, we get its position and orientation
# from the scene data structure. It is a stereo camera, so we average over
# the left and right channels. Note: we call `self.update()` in order to
# ensure that the contents of `scene.camera` are correct.
renderer.update_scene(data)
pos = np.mean([camera.pos for camera in renderer.scene.camera], axis=0)
z = -np.mean([camera.forward for camera in renderer.scene.camera], axis=0)
y = np.mean([camera.up for camera in renderer.scene.camera], axis=0)
rot = np.vstack((np.cross(y, z), y, z))
fov = model.vis.global_.fovy

# Translation matrix (4x4).
translation = np.eye(4)
translation[0:3, 3] = -pos

# Rotation matrix (4x4).
rotation = np.eye(4)
rotation[0:3, 0:3] = rot

# Focal transformation matrix (3x4).
focal_scaling = (1./np.tan(np.deg2rad(fov)/2)) * renderer.height / 2.0
focal = np.diag([-focal_scaling, focal_scaling, 1.0, 0])[0:3, :]

# Image matrix (3x3).
image = np.eye(3)
image[0, 2] = (renderer.width - 1) / 2.0
image[1, 2] = (renderer.height - 1) / 2.0
return image @ focal @ rotation @ translation

让我们使用相机矩阵从世界坐标投影到相机坐标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# reset the scene
renderer.update_scene(data)

# Get the world coordinates of the box corners
box_pos = data.geom_xpos[model.geom('red_box').id]
box_mat = data.geom_xmat[model.geom('red_box').id].reshape(3, 3)
box_size = model.geom_size[model.geom('red_box').id]
offsets = np.array([-1, 1]) * box_size[:, None]
xyz_local = np.stack(list(itertools.product(*offsets))).T
xyz_global = box_pos[:, None] + box_mat @ xyz_local

# Camera matrices multiply homogenous [x, y, z, 1] vectors.
corners_homogeneous = np.ones((4, xyz_global.shape[1]), dtype=float)
corners_homogeneous[:3, :] = xyz_global

# Get the camera matrix.
m = compute_camera_matrix(renderer, data)

# Project world coordinates into pixel space. See:
# https://en.wikipedia.org/wiki/3D_projection#Mathematical_formula
xs, ys, s = m @ corners_homogeneous
# x and y are in the pixel coordinate system.
x = xs / s
y = ys / s

# Render the camera view and overlay the projected corner coordinates.
pixels = renderer.render()
fig, ax = plt.subplots(1, 1)
ax.imshow(pixels)
ax.plot(x, y, '+', c='w')
ax.set_axis_off()

修改场景

让我们在 mjvScene 中添加一些任意的几何图形.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def get_geom_speed(model, data, geom_name):
"""Returns the speed of a geom."""
geom_vel = np.zeros(6)
geom_type = mujoco.mjtObj.mjOBJ_GEOM
geom_id = data.geom(geom_name).id
mujoco.mj_objectVelocity(model, data, geom_type, geom_id, geom_vel, 0)
return np.linalg.norm(geom_vel)

def add_visual_capsule(scene, point1, point2, radius, rgba):
"""Adds one capsule to an mjvScene."""
if scene.ngeom >= scene.maxgeom:
return
scene.ngeom += 1 # increment ngeom
# initialise a new capsule, add it to the scene using mjv_makeConnector
mujoco.mjv_initGeom(scene.geoms[scene.ngeom-1],
mujoco.mjtGeom.mjGEOM_CAPSULE, np.zeros(3),
np.zeros(3), np.zeros(9), rgba.astype(np.float32))
mujoco.mjv_makeConnector(scene.geoms[scene.ngeom-1],
mujoco.mjtGeom.mjGEOM_CAPSULE, radius,
point1[0], point1[1], point1[2],
point2[0], point2[1], point2[2])

# traces of time, position and speed
times = []
positions = []
speeds = []
offset = model.jnt_axis[0]/8 # offset along the joint axis

def modify_scene(scn):
"""Draw position trace, speed modifies width and colors."""
if len(positions) > 1:
for i in range(len(positions)-1):
rgba=np.array((np.clip(speeds[i]/10, 0, 1),
np.clip(1-speeds[i]/10, 0, 1),
.5, 1.))
radius=.003*(1+speeds[i])
point1 = positions[i] + offset*times[i]
point2 = positions[i+1] + offset*times[i+1]
add_visual_capsule(scn, point1, point2, radius, rgba)

duration = 6 # (seconds)
framerate = 30 # (Hz)

# Simulate and display video.
frames = []

# Reset state and time.
mujoco.mj_resetData(model, data)
mujoco.mj_forward(model, data)

while data.time < duration:
# append data to the traces
positions.append(data.geom_xpos[data.geom("green_sphere").id].copy())
times.append(data.time)
speeds.append(get_geom_speed(model, data, "green_sphere"))
mujoco.mj_step(model, data)
if len(frames) < data.time * framerate:
renderer.update_scene(data)
modify_scene(renderer.scene)
pixels = renderer.render()
frames.append(pixels)
media.show_video(frames, fps=framerate)