系列文章目录
前言
重要:如果您在使用 PyMJCF 时发现自己卡住了,请查看本页上的各个重要方框和底部的常见问题部分,看看其中是否有相关内容。
该库为 MuJoCo 基于 XML 的 MJCF 物理建模语言提供了一个 Python 对象模型。该库的目标是让用户能够在 Python 中轻松地与 MJCF 模型交互并对其进行修改,就像 JavaScript DOM 对 HTML 所做的那样。
该库的一个主要特点是能够轻松地将多个独立的 MJCF 模型组成一个更大的模型。不同模型或同一模型的多个实例中的重复名称会自动进行消歧处理。
下面的代码段提供了该库典型用例的一个快速示例。在这里,UpperBody 类可以简单地实例化 Arm 的两个副本,从而减少代码重复。每个 Arm 的身体、关节或 geoms 的名称都自动以其父名称为前缀,因此不会发生名称碰撞。
一、基本操作
1.1 创建 MJCF 模型
在 PyMJCF 中,模型的基本构件是 mjcf.Element。它对应于生成的 XML 中的一个元素。然而,用户代码不能直接实例化一个通用的 mjcf.Element 对象。
一个有效的模型总是由一个根 <mujoco> 元素组成。这在 PyMJCF 中被表示为特殊的 mjcf.RootElement 类型,它可以在用户代码中被实例化以创建一个空模型。
from dm_control import mjcfmjcf_model = mjcf.RootElement()
print(mjcf_model) # MJCF Element: <mujoco/>
1.2 添加新元素
新元素的属性可以作为 kwargs 传递:
my_box = mjcf_model.worldbody.add('geom', name='my_box',type='box', pos=[0, .1, 0])
print(my_box) # MJCF Element: <geom name="my_box" type="box" pos="0. 0.1 0."/>
1.3 解析现有的 XML 文档
另外,如果已有一个 XML 文件,PyMJCF 也可以对其进行解析,以创建一个 Python 对象:
from dm_control import mjcf# Parse from path
mjcf_model = mjcf.from_path(filename)# Parse from file
with open(filename) as f:mjcf_model = mjcf.from_file(f)# Parse from string
with open(filename) as f:xml_string = f.read()
mjcf_model = mjcf.from_xml_string(xml_string)print(type(mjcf_model)) # <type 'mjcf.RootElement'>
1.4 遍历模型
请看下面的 MJCF 模型:
<mujoco model="test"><default><default class="brick"><geom rgba="1 0 0 1"/></default></default><worldbody><body name="foo"><freejoint/><inertial pos="0 0 0" mass="1"/><body name="bar"><joint name="my_hinge" type="hinge"/><geom name="my_geom" pos="0 1 2" class="brick"/></body></body></worldbody>
</mujoco>
Element 对象的子元素和 XML 属性都作为 Python 属性公开。这些属性的名称都与它们的 XML 对应属性相同,只有一个例外:class XML 属性被命名为 dclass,以避免与 Python class 关键字冲突:
my_geom = mjcf_model.worldbody.body['foo'].body['bar'].geom['my_geom']
print(isinstance(mjcf_model, mjcf.Element)) # True
print(my_geom.name) # 'my_geom'
print(my_geom.pos) # np.array([0., 1., 2.], dtype=float)
print(my_geom.class) # SyntaxError
print(my_geom.dclass) # 'brick'
请注意,对象模型中的属性值不受默认值的影响:
print(mjcf_model.default.default['brick'].geom.rgba) # [1, 0, 0, 1]
print(my_geom.rgba) # None
1.5 无需遍历即可查找元素
我们还可以直接查找元素,而无需遍历对象层次结构:
found_geom = mjcf_model.find('geom', 'my_geom')
print(found_geom == my_geom) # True
查找给定类型的所有元素
# Note that <freejoint> is also considered a joint
joints = mjcf_model.find_all('joint')
print(len(joints)) # 2
print(joints[0] == mjcf_model.worldbody.body['foo'].freejoint) # True
print(joints[1] == mjcf_model.worldbody.body['foo'].body['bar'].joint[0]) # True
请注意,find_all 返回元素的顺序与它们在模型中声明的顺序相同。
1.6 修改 XML 属性
属性可以修改、添加或删除:
my_geom.pos = [1, 2, 3]
print(my_geom.pos) # np.array([1., 2., 3.], dtype=float)
my_geom.quat = [0, 1, 0, 0]
print(my_geom.quat) # np.array([0., 1., 0., 0.], dtype=float)
del my_geom.quat
print(my_geom.quat) # None
违反模式会导致错误:
print(my_geom.poss) # raise AttributeError (no child or attribute called poss)
my_geom.pos = 'invalid' # raise ValueError (assigning string to array)
my_geom.pos = [1, 2, 3, 4, 5, 6] # raise ValueError (array length is too long)# raise ValueError (mass is a required attribute of <inertial>)
del mjcf_model.find('body', 'foo').inertial.mass
1.7 标识符的唯一性
PyMJCF 强化了模型中 "标识符 "属性的唯一性。标识符由 <default> 的类属性和所有名称属性组成。它们的唯一性只在特定命名空间内强制执行。例如,<body> 可以与 <geom> 具有相同的名称,而 <position> 和 <velocity> 执行器则不能具有相同的名称。
mjcf_model.worldbody.add('geom', name='my_geom')
foo = mjcf_model.worldbody.find('body', 'foo')
foo.add('my_geom') # Error, duplicated geom name
foo.add('foo') # OK, a geom can have the same name as a body
mjcf_model.find('geom', 'foo').name = 'my_geom' # Error, duplicated geom name
1.8 引用属性
有些属性是对其他元素的引用。例如,执行器的关节属性指向模型中的 <joint> 元素。
mjcf.Element 可以直接分配给这些引用属性:
my_hinge = mjcf_model.find('joint', 'my_hinge')
my_actuator = mjcf_model.actuator.add('velocity', joint=my_hinge)
这是分配引用属性的推荐方式,因为它能保证在被引用元素重命名时引用不会失效。另外,字符串也可以分配给引用属性。在这种情况下,PyMJCF 不会尝试验证命名的元素是否确实存在于模型中。
重要:如果被引用的元素与引用属性在不同的模型中(例如在附加模型中),则必须通过直接将 mjcf.Element 对象而不是字符串赋值给属性来创建引用。分配给引用属性的字符串不能包含"/",因为 PyMJCF 会在附加时自动对它们进行作用域划分。
二、附加模型
在本节中,我们将 mjcf.RootElement 简单地称为 "模型"。模型可以附加到其他模型上,以创建合成场景。
arena = mjcf.RootElement()
arena.worldbody.add('geom', name='ground', type='plane', size=[10, 10, 1])robot = mjcf.from_xml_file('robot.xml')
arena.attach(robot)
我们把 arena 称为父模型,把机器人称为子模型(或附着模型)。
2.1 附加 frames
当一个模型被附加到一个场地时,会在父模型中创建一个空体。这个空体称为附着 frame。
附着 frame 是作为包含附着点的主体的子体创建的,其位置和方向与站点相同。生成 XML 时,附着 frame 的内容会与所附模型的 <worldbody> 内容一致。在生成的 XML 中,附着 frame 的名称是子代的完全/限定/前缀/。尾部的斜线确保附着 frame 的名称不会与用户定义的主体相冲突。
更具体地说,如果我们有以下父模型和子模型:
<mujoco model="parent"><worldbody><body><geom name="foo" type="box" pos="-0.2 0 0.3" size="0.5 0.3 0.1"/><site name="attachment_site" pos="1. 2. 3." quat="1. 0. 0. 1."/></body></worldbody>
</mujoco><mujoco model="child"><worldbody><geom name="bar" type="box" pos="0.5 0.25 1." size="0.1 0.2 0.3"/></worldbody>
</mujoco>
那么最终生成的 XML 将是
<!-- PyMJCF-generated XML, contains implementation details -->
<mujoco model="parent"><worldbody><body><geom name="foo" type="box" pos="-0.2 0 0.3" size="0.5 0.3 0.1"/><site name="attachment_site" pos="1. 2. 3." quat="1. 0. 0. 1."/><body name="child/" pos="1. 2. 3." quat="1. 0. 0. 1."><geom name="child/my_box" type="box" pos="0.5 0.25 1." size="0.1 0.2 0.3"/></body></body></worldbody>
</mujoco>
重要:附着 frame 是以对用户透明的方式创建的。特别是,PyMJCF 不会将其作为常规正文处理。生成的 XML 中的名称应视为实现细节,不应依赖。
尽管如此,有时还是有必要访问附着 frame ,例如在父模型和子模型之间添加一个连接点。最简单的方法是持有对 attach 调用所返回对象的引用:
attachment_frame = parent_model.attach('child')
attachment_frame.add('freejoint')
另外,如果模型已经被附加,则可以使用附着框架命名空间中的 find 函数来检索附着 frame 。mjcf.traversal_utils 中的 get_attachment_frame 方便函数可以查找子模型的附着 frame ,而无需访问父模型。
frame_1 = parent_model.find('attachment_frame', 'child')# Convenience function: get the attachment frame directly from a child model
frame_2 = mjcf.traversal_utils.get_attachment_frame(child_model)
print(frame_1 == frame_2) # True
重要: 为鼓励良好的建模实践,附着 frame 的直接子元素只能是 <joint> 和 <inertial>。其他类型的元素应添加到附加模型的 <worldbody> 中。
2.2 元素所有权
重要:当遍历父模型时,子模型的元素不会出现。
2.3 默认类
PyMJCF 确保父模型的默认类永远不会影响它的任何子模型。这最大限度地减少了两个模型发生微妙的 "不兼容 "的可能性,因为无论模型附属于哪个模型,其行为方式总是相同的。
PyMJCF 在实践中实现这一点的方法是将模型的全局 <default> 上下文中的所有内容移到一个名为 / 的默认类中。换句话说,PyMJCF 生成的模型永远不会在全局默认上下文中出现任何内容。相反,生成的模型总是看起来像这样:
<!-- PyMJCF-generated XML, contains implementation details -->
<mujoco><default><default class="/"><!-- "global defaults" go here --><geom rgba="1. 0. 0. 1."/></default></default>
</mujoco>
重要:这种转换对用户是透明的。在 Python 中,上述 geom rgba 设置被当作全局默认值访问,即 mjcf_model.default.geom.rgba。一般来说,用户不必担心 PyMJCF 对默认值的内部处理。
当模型被附加时,它的 / 默认类会变成 fully/qualified/prefix/。尾部的斜线确保了这种转换不会与用户命名的默认类发生冲突。更具体地说,如果我们有以下父模型和子模型:
<mujoco model="parent"><default><geom rgba="1. 0. 0. 1."/><default class="green"><geom rgba="0. 1. 0. 1."/></default></default>
</mujoco><mujoco model="child"><default><joint range="0. 1."/><default class="stiff"><joint stiffness="0.1"/></default></default>
</mujoco>
那么最终生成的 XML 将是
<!-- PyMJCF-generated XML, contains implementation details -->
<mujoco model="parent"><default><default class="/"><geom rgba="1. 0. 0. 1."/><default class="green"><geom rgba="0. 1. 0. 1."/></default></default><default class="child/"><joint range="0. 1."/><default class="child/stiff"><joint stiffness="0.1"/></default></default></default>
</mujoco>
2.4 全局选项
如果任何全局选项不同,则无法将一个模型附加到另一个模型。全局选项由 <compiler>, <option>, <size> 和 <visual> 属性组成。与处理默认类一样,这是为了确保两个模型不会出现微妙的 "不兼容"。例如
model_1 = mjcf.RootElement()
model_1.compiler.angle = 'radian'model_2 = mjcf.RootElement()
model_2.compiler.angle = 'degree'model_1.attach(model_2) # Error!
只有当两个模型明确赋予一个选项不同的值时,该选项才会被认为是冲突的。下面举例说明冲突选项可能产生的问题:
model_1 = mjcf.RootElement()model_2 = mjcf.RootElement()
model_2.compiler.angle = 'degree'model_1.attach(model_2) # No error, but all angles in model_1 are now wrong!
在这里,model_1 假设 MuJoCo 的默认角度单位为弧度。由于没有显式地给 compiler.angle 赋值,PyMJCF 没有检测到与 model_2 中的 angle=degree 冲突。现在,model_1 中的所有角度都被错误地解释为度。
2.5 <worldbody> 以外的元素
当连接模型时,所有非世界体元素的子元素(如致动器或肌腱)都会自动合并到适当的位置。已命名元素的前缀如前所述。
三、常见问题 {#common-gotchas}
3.1 使用 foo.dclass,而不是 foo.class
class XML 属性与 PyMJCF 中的 dclass Python 属性相对应。这是因为 class 在 Python 中是一个保留关键字。不过,在 getattr 中使用 "class "也是可以的。
<geom name="my_geom" class="red"/>
print(my_geom.class) # SyntaxError
print(my_geom.dclass) # 'red'
print(getattr(my_geom, 'class')) # 'red'
3.2 foo.type 和 foo.range 没问题
type 和 range 属性在 Cider 中会触发语法高亮,但它们在 Python 中不是保留字。
my_geom.type = 'capsule' # OK!
my_joint.range = [-1, 1] # OK!
3.3 一个模型只能附加一次
一个模型不能附加两次。如果在模拟中需要同一模型的多个副本,可以制作深拷贝。最好是定义一个类来构造模型,然后根据需要多次调用构造函数。