建模#

简介#

MuJoCo 可以加载其原生 MJCF 格式的 XML 模型文件,以及流行但功能更受限的 URDF 格式。本章为 MJCF 建模指南。参考手册可在 XML 参考 一章中找到。URDF 文档可在其他地方查阅;此处仅描述 MuJoCo 特有的 URDF 扩展

MJCF 模型能够通过广泛的功能和模型元素来表示复杂的动力学系统。访问所有这些功能需要一种丰富的建模格式,如果不以易用性为设计前提,这种格式可能会变得繁琐。因此,我们致力于将 MJCF 设计为一种可扩展的格式,允许用户从简单的模型开始,后续再构建更详细的模型。在这方面特别有帮助的是受 HTML 中内联层叠样式表 (CSS) 启发而设计的广泛的 默认设置 机制。它使用户能够快速创建新模型并进行实验。通过无数的 选项(可用于重新配置仿真管线)以及使模型编辑成为交互式过程的快速重新加载功能,实验过程得到了进一步的辅助。

可以将 MJCF 看作建模格式与编程语言的混合体。它内置了一个编译器,这通常是与编程语言相关的概念。虽然 MJCF 不具备通用编程语言的功能,但根据模型的设计方式,它会自动调用许多复杂的编译时计算。

加载模型#

正如概述章节中 模型实例 所解释的那样,MuJoCo 模型可以从 MJCF 或 URDF 格式的纯文本 XML 文件中加载,然后编译成低级的 mjModel。或者,可以从二进制 MJB 文件(其格式未公开,本质上是 mjModel 内存缓冲区的副本)直接加载先前保存的 mjModel。MJCF 和 URDF 文件使用 mj_loadXML 加载,而 MJB 文件使用 mj_loadModel 加载。

当加载 XML 文件时,它首先会使用内部的 TinyXML 解析器解析为文档对象模型 (DOM)。然后,该 DOM 会被处理并转换为高级 mjSpec 对象。转换取决于模型格式——该格式是从 XML 文件的顶级元素推断出来的,而不是从文件扩展名推断出来的。回想一下,一个有效的 XML 文件具有唯一的顶级元素。对于 MJCF,此元素必须是 mujoco;对于 URDF,必须是 robot

编译模型#

一旦创建了高级 mjSpec(通过加载 MJCF 或 URDF 文件,或以 编程方式),它就会被编译成 mjModel。编译与加载是独立的,这意味着无论 mjSpec 是如何创建的,编译器的工作方式都相同。解析器和编译器都执行详尽的错误检查,并在遇到第一个错误时中止。生成的错误消息包含 XML 文件中的行号和列号,且不言自明,因此我们在此不作记录。解析器使用自定义模式来确保文件结构、元素和属性有效。随后,编译器会应用许多额外的语义检查。最后,执行已编译模型的一个仿真步骤,并拦截任何运行时错误。后者是通过(暂时)将 mju_user_error 设置为指向抛出 C++ 异常的函数来完成的;如果需要,用户可以在运行时实现类似的错误拦截功能。

整个解析和编译过程非常快——如果模型不包含大型网格或需要通过仿真计算的执行器长度范围,则不到一秒。这使得通过频繁重新加载和可视化更改来交互式设计模型成为可能。请注意,simulate.cc 代码示例有一个重新加载当前模型的键盘快捷键 (Ctrl+L)。

保存模型#

MJCF 模型可以由多个(包含的)XML 文件以及 XML 中引用的网格、高度场和纹理组成。编译后,所有这些文件的内容被组装到 mjModel 中,并可以使用 mj_saveModel 保存为二进制 MJB 文件。MJB 是一个独立文件,不引用任何其他文件。它加载速度也更快。因此,我们建议将常用模型保存为 MJB,并在需要仿真时加载它们。

也可以使用 mj_saveLastXML 将已编译的 mjSpec 保存为 MJCF。如果在编译后修改了相应 mjModel 中的任何实值字段(这虽然不常见,但在系统辨识应用中可能会发生),这些修改会在保存前自动复制回 mjSpec 中。注意,结构性更改无法在编译后的模型中进行。XML 写入器会尝试生成最小的 MJCF 文件,该文件保证能编译成相同的模型,除了由实数的纯文本表示引起的微不足道的数值差异。生成的文件结构可能与原始文件不同,因为 MJCF 有许多用户便利功能,允许以不同方式指定同一个模型。XML 写入器使用 MJCF 的一个“规范”子集,其中所有坐标都是局部的,并且所有物体的姿态、方向和惯性属性都已明确指定。在“计算”一章中,我们展示了一个 示例 MJCF 文件和相应的 保存的示例

编辑模型#

自 MuJoCo 3.2 起,可以使用 mjSpec 结构体及相关 API 创建和修改模型。有关进一步的文档,请参阅 模型编辑 一章。

MJCF 机制#

MJCF 使用几种跨越多个模型元素的模型创建机制。为避免重复,我们仅在本节详细描述它们一次。这些机制并不对应于“计算”一章中介绍之外的新的仿真概念。它们的作用是简化 MJCF 模型的创建,并允许使用不同的数据格式,而无需手动转换为规范格式。

运动树#

MJCF 文件的主要部分是由嵌套的 body(物体)元素创建的 XML 树。顶级物体是特殊的,称为 worldbody。这种树状组织与 URDF 形成对比,在 URDF 中,人们创建一系列链接,然后用指定子链接和父链接的关节连接它们。在 MJCF 中,就 XML 而言,子物体字面上就是父物体的子元素。

当在一个物体内部定义 关节 时,其功能不是连接父物体和子物体,而是创建它们之间的运动自由度。如果给定物体内没有定义关节,则该物体会焊接到其父物体上。MJCF 中的一个物体可以包含多个关节,因此无需为了创建复合关节而引入虚拟物体。只需在同一个物体内定义构成所需复合关节的所有原始关节即可。例如,两个滑块和一个铰链可以用来模拟在平面上移动的物体。

其他 MJCF 元素可以在由嵌套物体元素创建的树中定义,特别是 jointgeomsitecameralight。当一个元素在物体内定义时,它被固定在该物体的局部坐标系中,并始终随其移动。引用多个物体或完全不引用物体的元素,则在运动树之外的单独部分中定义。

默认设置#

MJCF 拥有用于设置默认属性值的复杂机制。这使我们能够拥有大量的元素和属性,以展现软件的丰富功能,同时编写简洁且可读的模型文件。该机制进一步使用户能够在一个地方进行更改,并使其传播到整个模型中。我们先从一个示例开始。

<mujoco>
  <default class="main">
    <geom rgba="1 0 0 1"/>
    <default class="sub">
      <geom rgba="0 1 0 1"/>
    </default>
  </default>

  <worldbody>
    <geom type="box"/>
    <body childclass="sub">
      <geom type="ellipsoid"/>
      <geom type="sphere" rgba="0 0 1 1"/>
      <geom type="cylinder" class="main"/>
    </body>
  </worldbody>
</mujoco>

这个示例实际上无法编译,因为缺少一些必要信息,但在这里我们只关心 geom rgba 值的设置。由于默认设置机制,上面创建的四个 geom 最终将具有以下 rgba 值:

geom 类型

geom rgba

长方体 (box)

1 0 0 1

椭球体 (ellipsoid)

0 1 0 1

球体 (sphere)

0 0 1 1

圆柱体 (cylinder)

1 0 0 1

盒子使用顶级默认类“main”来设置其未定义属性的值,因为没有指定其他类。物体指定了 childclass(子类)为“sub”,导致该物体的所有子项(及其所有子项等)都使用“sub”类,除非另有说明。因此,椭圆体使用“sub”类。球体明确定义了 rgba,覆盖了默认设置。圆柱体指定了默认类“main”,因此它使用“main”而不是“sub”,即使后者是在包含该 geom 的物体的 childclass 属性中指定的。

现在我们描述一般规则。MuJoCo 支持无限数量的默认类,由 XML 中可能嵌套的 default 元素创建。每个类都有一个唯一的名称——除了顶级类之外,这是一个必需属性,如果未定义,其名称为“main”。每个类还拥有完整的一套虚拟模型元素,其属性设置如下。当一个默认类在另一个默认类中定义时,子类会自动继承父类的所有属性值。然后,它可以通过定义相应的属性来覆盖其中的部分或全部。顶级默认类没有父类,因此其属性初始化为 参考章节 中显示的内部默认值。

默认类中包含的虚拟元素不是模型的一部分;它们仅用于初始化实际模型元素的属性值。当一个实际元素首次创建时,它的所有属性都会从当前处于活动状态的默认类中相应的虚拟元素复制过来。始终存在一个活动的默认类,可以通过以下三种方式之一确定。如果当前元素或其任何祖先物体中没有指定类,则使用顶级类(无论其是否被命名为“main”或其他名称)。如果当前元素中未指定类,但其一个或多个祖先物体指定了 childclass,则使用来自最近祖先物体的 childclass。如果当前元素指定了一个类,则使用该类,无论其祖先物体中是否存在 childclass 属性。

某些属性(例如物体惯性)可能处于特殊的未定义状态。这指示编译器根据其他信息(在这种情况下为附着在物体上的 geom 的惯性)推断相应的值。未定义状态不能在 XML 文件中输入。因此,一旦属性在一个类中定义,它就不能在该类或其任何子类中变成未定义。因此,如果目标是在给定的模型元素中将某个属性保留为未定义,则必须在活动的默认类中将其保持为未定义。

这里的最后一个复杂之处是执行器(actuators)。它们之所以不同,是因为某些与执行器相关的元素实际上是快捷方式,而快捷方式与默认设置机制的交互方式并不显而易见。这将在下面的 执行器快捷方式 部分中进行解释。

坐标系#

运动树中定义的所有元素的位置和方向均以局部坐标表示,对于物体而言是相对于父物体的,对于 geom、关节、位置、相机和灯光而言是相对于包含该元素的物体的。

一个相关的属性是 compiler/angle。它指定 MJCF 文件中的角度是以度还是弧度表示(编译后,角度始终以弧度表示)。

位置使用以下属性指定:

pos: real(3), “0 0 0”

相对于父物体的位置。

坐标系方向#

几个模型元素与右手空间坐标系相关联。这些都是运动树中定义的元素(关节除外)。空间坐标系由其位置和方向定义。指定 3D 位置很直观,但指定 3D 方向可能具有挑战性。这就是 MJCF 提供几种替代机制的原因。无论用户选择哪种机制,坐标系方向在内部总是转换为单位四元数。回想一下,绕由单位向量 \((x, y, z)\) 给出的轴旋转 \(a\) 角度的 3D 旋转对应于四元数 \((\cos(a/2), \: \sin(a/2) \cdot (x, y, z))\)。还要回想一下,每个 3D 方向都可以通过绕某个轴旋转某个角度来唯一指定。

所有具有空间坐标系的 MJCF 元素都允许使用下面列出的五个属性。坐标系方向使用这些属性中的最多一个来指定。quat 属性具有对应于零旋转的默认值,而其他属性则被初始化为特殊的未定义状态。因此,如果用户未指定这些属性中的任何一个,则坐标系不会旋转。

quat: real(4), “1 0 0 0”

如果已知四元数,这是指定坐标系方向的首选方式,因为它不涉及转换。相反,它会被归一化为单位长度,并在编译期间复制到 mjModel 中。当模型保存为 MJCF 时,所有坐标系方向都会使用此属性表示为四元数。

axisangle: real(4), optional

这些是上述提到的量 \((x, y, z, a)\)。最后一个数字是旋转角度,单位为度或弧度,由 compilerangle 属性指定。前三个数字确定一个 3D 向量,即旋转轴。此向量在编译期间被归一化为单位长度,因此用户可以指定任何非零长度的向量。请记住,旋转是右手的;如果向量 \((x, y, z)\) 的方向相反,这将导致相反的旋转。更改 \(a\) 的符号也可以用来指定相反的旋转。

euler: real(3), optional

绕三个坐标轴的旋转角度。应用这些旋转的轴序列由 compilereulerseq 属性确定,并且对于整个模型是相同的。

xyaxes: real(6), optional

前 3 个数字是坐标系的 X 轴。接下来的 3 个数字是坐标系的 Y 轴,它会自动与 X 轴正交。Z 轴随后被定义为 X 轴和 Y 轴的叉积。

zaxis: real(3), optional

坐标系的 Z 轴。编译器会找到将向量 \((0, 0, 1)\) 映射到此处指定的向量的最小旋转。这隐含地确定了坐标系的 X 轴和 Y 轴。这对于绕 Z 轴具有旋转对称性的 geom 以及灯光(其方向沿着其坐标系的 Z 轴)非常有用。

求解器参数#

约束求解器通过三个量来寻找满足软约束的力:阻抗 \(d\)(强制约束的强度)、刚度 \(k\)阻尼 \(b\)(如何响应违反情况)。这些在“计算”一章的 参数 部分中有数学描述。在这里,我们解释如何设置它们。设置是通过所有涉及约束的 MJCF 元素中都可用的属性 solrefsolimp 间接完成的。这些参数可以按约束调整,或按默认类调整,或者保留未定义——在这种情况下,MuJoCo 使用下面显示的内部默认值。还要注意 option 中可用的覆盖机制;它可用于在运行时更改所有与接触相关的求解器参数,以便交互式地试验参数设置或为数值优化实现延拓法。

在这里,我们专注于单个标量约束。使用与“计算”一章略有不同的符号,令 \(\ac\) 表示加速度,\(v\) 表示速度,\(r\) 表示位置或残差(在摩擦维度中定义为 0),\(k\)\(b\) 表示用于定义参考加速度 \(\ar = -b v - k r\) 的虚拟弹簧的刚度和阻尼(参见 (12))。令 \(d\) 为约束阻抗,\(\au\) 为无约束力时的加速度。我们早期的分析表明,约束空间中的动力学近似为:

(1)#\[\ac + d \cdot (b v + k r) = (1 - d)\cdot \au \]

同样,受用户控制的参数是 \(d, b, k\)。其余量是系统状态的函数,并在每个时间步自动计算。

阻抗#

我们首先解释约束阻抗 \(d\)

阻抗的直观描述

阻抗 \(d \in (0, 1)\) 对应于约束产生力的能力\(d\) 的小值对应弱约束,而 \(d\) 的大值对应强约束。阻抗在任何时候都会影响约束,尤其是在系统静止时。阻抗使用 solimp 属性进行设置。

回想一下 \(d\) 必须介于 0 和 1 之间;MuJoCo 在内部将其钳制在范围 [mjMINIMP mjMAXIMP] 内,该范围目前设置为 [0.0001 0.9999]。它导致求解器在无约束加速度 \(\au\) 和参考加速度 \(\ar\) 之间进行插值。用户可以将 \(d\) 设置为常数,或者利用其插值属性使其与位置相关,即作为约束违反量 \(r\) 的函数。位置相关阻抗可用于模拟物体周围的软接触层,或定义随着违反程度增大而变强的等式约束(例如,以近似反冲)。函数 \(d(r)\) 的形状由元素特定的参数向量 solimp 决定。

solimp : real(5), “0.9 0.95 0.001 0.5 2”

这五个数字 (\(d_0\), \(d_\text{width}\), \(\text{width}\), \(\text{midpoint}\), \(\text{power}\)) 参数化了 \(d(r)\) ——作为约束违反量 \(r\) 函数的阻抗 \(d\)

前 3 个值表明阻抗将随着 \(r\)\(0\) 变化到 \(\text{width}\) 而平滑变化:

\[d(0) = d_0, \quad d(\text{width}) = d_\text{width} \]

第 4 个和第 5 个值,\(\text{midpoint}\)\(\text{power}\),控制在 \(d_0\)\(d_\text{width}\) 之间进行插值的 S 型函数的形状,如下图所示。这些图显示了两个反射的 S 型曲线,因为阻抗 \(d(r)\) 取决于 \(r\) 的绝对值。\(\text{power}\)(用于生成函数的样条多项式的幂)必须为 1 或更大。\(\text{midpoint}\)(指定拐点)必须介于 0 和 1 之间,并以 \(\text{width}\) 为单位表示。请注意,当 \(\text{power}\) 为 1 时,无论 \(\text{midpoint}\) 如何,函数都是线性的。

_images/impedance.png _images/impedance_dark.png

这些图在垂直轴上显示阻抗 \(d(r)\),在水平轴上显示约束违反量 \(r\)

对于等式约束,\(r\) 是约束违反量。对于限制、椭圆锥的法线方向和棱锥锥的所有方向,\(r\) 是(限制或接触)距离减去约束变为活跃状态的边距;对于接触,此边距为 margin。限制和接触约束在 \(r < 0\)(穿透)时处于活跃状态。

对于摩擦约束,请参阅 摩擦

平滑度和可微性

对于完全平滑(可微)的动力学,限制和接触应具有 \(d_0=0\) (solimp[0]=0)。特别对于接触,应牢记与 geom 相关的求解器参数的 混合规则。另请参阅“计算”一章中关于导数的讨论以及 mjd_transitionFD 文档。

参考#

接下来我们解释刚度 \(k\) 和阻尼 \(b\) 的设置,它们控制参考加速度 \(\ar\)

参考加速度的直观描述

参考加速度 \(\ar\) 决定了约束为了纠正违反情况而试图达到的运动。想象一个物体掉落到平面上。撞击时,约束将产生一个法向力,试图利用特定的运动来纠正穿透;这个运动就是参考加速度。

理解参考加速度的另一种方法是考虑“计算”一章中描述的未建模变形变量 Computation chapter。想象两个物体压在一起,导致接触处发生变形。现在非常快地将两个物体拉开;变形在恢复到未变形状态时的运动就是参考加速度。

这种加速度由两个数字定义:刚度 \(k\) 和阻尼 \(b\),它们可以直接设置,或者重新参数化为质量-弹簧-阻尼系统(谐振子)的时间常数和阻尼比。参考加速度由 solref 属性控制。

此属性有两种格式,由数字的符号决定。如果两个数字均为正,则规格被视为 \((\text{timeconst}, \text{dampratio})\) 格式。如果为负,则为“直接” \((-\text{stiffness}, -\text{damping})\) 格式。

对于摩擦约束,下面的质量-弹簧-阻尼分析不直接适用;请参阅 摩擦

solref : real(2), “0.02 1”

我们首先描述默认的正值格式,其中两个数字是 \((\text{timeconst}, \text{dampratio})\)

这里的思路是根据质量-弹簧-阻尼系统的时间常数和阻尼比来重新参数化模型。通过“时间常数”,我们指的是自然频率的倒数乘以阻尼比。现在回想一下,(1) 中的乘积 \(d \cdot k\)\(d \cdot b\) 是约束空间中的有效刚度和阻尼。由于阻抗 \(d(r)\) 随位置残差 \(r\) 变化,我们无法实现恒定的质量-弹簧-阻尼特性;完全消除 \(d\) 的缩放是不可取的,因为 \(d = 0\) 的限制将不再禁用约束。相反,我们将 \(d(r)\) 的一个因子吸收到 \(k\) 中(但不吸收到 \(b\) 中),这样阻尼比保持恒定,而时间常数随 \(d(r)\) 缩放。公式为:

(2)#\[\begin{aligned} b &= 2 / (d_\text{width}\cdot \text{timeconst}) \\ k &= d(r) / (d_\text{width}^2 \cdot \text{timeconst}^2 \cdot \text{dampratio}^2) \\ \end{aligned}\]

timeconst 参数应至少比仿真时间步长大两倍,否则系统相对于数值积分器可能变得太刚(尤其是使用欧拉积分时),并且仿真可能会变得不稳定。除非将 flagrefsafe 属性设置为 false,否则在内部会强制执行此操作。\(\text{dampratio}\) 参数通常应设置为 1,对应于临界阻尼。较小的值会导致欠阻尼或弹跳的约束,而较大的值会导致过阻尼的约束。将 (2)(1) 相结合,我们可以推导出以下结论:如果参考加速度使用正数格式给出,并且阻抗是恒定的 \(d = d_0 = d_\text{width}\),则静止时的穿透深度为:

\[r = \au \cdot (1 - d) \cdot \text{timeconst}^2 \cdot \text{dampratio}^2 \]

接下来我们描述直接格式,其中两个数字是 \((-\text{stiffness}, -\text{damping})\)。这特别允许直接控制恢复系数。我们仍然应用一些缩放,以便相同的数字可以用于不同的阻抗,但缩放不再取决于 \(r\),且两个数字不再相互作用。缩放公式为:

(3)#\[\begin{aligned} b &= \text{damping} / d_\text{width} \\ k &= \text{stiffness} \cdot d(r) / d_\text{width}^2 \\ \end{aligned}\]

类似于 (2) 之后的推导,如果参考加速度是给定的,且阻抗是恒定的,则静止时的穿透深度为:

\[r = \frac{\au (1 - d)}{\text{stiffness}} \]

提示

在正值默认格式中,\(\text{timeconst}\) 参数控制约束的柔软度。它以时间为单位指定,意味着“约束试图解决违反情况的速度”。较大的值对应于较软的约束。

负值“直接”格式更灵活,例如允许完全弹性碰撞 (\(\text{damping} = 0\))。它是系统辨识推荐的格式。

正值格式中的 \(\text{dampratio}\) 为 1 等效于直接格式中的 \(\text{damping} = 2 \sqrt{ \text{stiffness} }\)

摩擦#

摩擦损耗约束(在关节和肌腱中)和椭圆接触锥的摩擦维度具有零位置违反:\(r \equiv 0\)。这简化了约束模型(另请参阅 参数)。

  • 阻抗始终为 \(d_0\) (solimp[0]),因为 \(d(r)\) 是在 \(r=0\) 处评估的。S 型形状参数(\(\text{width}\), \(\text{midpoint}\), \(\text{power}\))无效。

  • 动力学是一阶的(约束速度呈指数衰减,没有弹簧):刚度 \(k\) 始终为 0。

  • 在标准 solref 格式中,时间常数控制指数速度衰减。阻尼比被忽略(它仅出现在 \(k\) 公式中)。

  • 在直接 solref 格式中,使用阻尼(第二个值),但忽略刚度(第一个值)。

  • \(d_\text{width}\) (solimp[1]) 仍然作为缩放分母影响阻尼 \(b\) ((2), (3)),即使它不影响阻抗。

接触参数#

每个接触的参数已在“计算”一章的 接触 部分中进行了描述。在这里,我们解释如何设置这些参数。如果 geom 对使用 XML 元素 pair 明确定义,它具有直接指定所有接触参数的属性。在这种情况下,单个 geom 的参数将被忽略。如果另一方面接触是由动态机制生成的,则需要从接触对中的两个 geom 推断其参数。如果两个 geom 具有相同的参数,则不需要做任何事,但如果它们的参数不同呢?在这种情况下,我们使用 geom 属性 solmixpriority 来决定如何组合它们。每个接触参数的组合规则如下:

condim

如果两个 geom 中的一个具有更高的优先级,则使用它的 condim。如果两个 geom 具有相同的优先级,则使用两个 condim 中的最大值。通过这种方式,无摩擦 geom 和摩擦 geom 形成摩擦接触,除非无摩擦 geom 具有更高的优先级。在粒子系统中,这通常是可取的,我们可能不希望粒子粘在任何物体上。

friction

回想一下,接触最多可以有 5 个摩擦系数:两个切向,一个扭转,两个滚动。mjData.contact 中的每个接触实际上都有所有这 5 个系数,即使 condim 小于 6 且并非所有系数都被使用。相比之下,geom 只有 3 个摩擦系数:切向(两个轴相同)、扭转、滚动(两个轴相同)。这些 3D 摩擦系数向量中的每一个都会通过复制切向和滚动分量扩展为 5D 摩擦系数向量。关于切向、扭转和滚动系数语义的直观描述,请参阅“计算”一章中的 接触 部分。

接触摩擦系数随后按照以下规则计算:如果两个 geom 中的一个具有更高的优先级,则使用其摩擦系数。否则,使用两个 geom 每个摩擦系数的逐元素最大值

每个接触有 5 个系数而每个 geom 只有 3 个的原因如下。对于一个接触对,我们希望允许我们的求解器能处理的最灵活的模型。如前所述,各向异性摩擦可用于模拟如滑行等效果。然而,这需要知道接触切平面的两个轴是如何定向的。对于预定义的接触对,我们预先知道两个 geom 类型,相应的碰撞函数总是生成定向相同——我们不在这里描述,但在可视化器中可以看到——的接触坐标系。然而,对于单个 geom,我们不知道它们可能会与哪些其他 geom 发生碰撞,也不知道它们的 geom 类型可能是什么,因此在指定单个 geom 时无法知道接触切平面将如何定向。这就是为什么 MuJoCo 不允许在单个 geom 规格中使用各向异性摩擦,而只允许在明确的接触对规格中使用。

margin(边距), gap(间隙)

使用两个 geom 边距(或间隙)之和。此处忽略 geom 优先级,因为边距和间隙是距离属性,单边指定几乎没有意义。请参阅 边距和间隙

solref, solimp

如果两个 geom 中的一个具有更高的 优先级,则使用其 solref 和 solimp 参数。如果两个 geom 具有相同的优先级,则使用加权平均值。权重与 solmix 属性成正比,即 weight1 = solmix1 / (solmix1 + solmix2),weight2 同理。对于此加权平均规则,有一个重要的例外。如果任一 geom 的 solref 为非正,即它依赖于直接格式,则无论 solmix 如何,都使用逐元素最小值。这是因为对不同格式的 solref 参数进行平均将毫无意义。

接触覆盖#

MuJoCo 使用“计算”一章中描述的复杂且新颖的 约束模型。要获得该模型如何工作的直觉,需要进行一些实验。为了促进这一过程,我们提供了一种覆盖某些求解器参数的机制,而无需对实际模型进行更改。一旦禁用覆盖,仿真就会恢复为模型中指定的参数。这种机制还可以用于在数值优化(如最优控制或状态估计)的背景下实现延拓法。这可以通过允许接触在优化的早期阶段从远处作用——从而帮助优化器找到梯度并接近良好的解决方案——然后在后期减少这种影响以使最终解决方案在物理上真实来实现。

这里相关的设置是 flagoverride 属性(它启用和禁用此机制),以及 optiono_margin, o_solref, o_solimp 属性(它们指定新的求解器参数)。请注意,覆盖仅适用于接触,不适用于其他类型的约束。原则上,MuJoCo 模型中有许多实值参数可以从类似的覆盖机制中受益。然而,我们不得不划定界限,而接触是自然的选择,因为它们产生了最丰富但最难调整的行为。此外,接触动力学在数值优化方面往往是一个挑战,经验表明,对接触参数进行延拓有助于避免局部极小值。

用户参数#

许多 MJCF 元素具有可选属性 user,它定义了一个自定义元素特定的参数数组。这与 size 元素的相应 “nuser_XXX” 属性进行交互。例如,如果我们将 nuser_geom 设置为 5,那么 mjModel 中的每个 geom 都将拥有一个包含 5 个实值参数的自定义数组。这些 geom 特定的参数要么在 MJCF 文件中通过 geomuser 属性定义,要么如果省略此属性,则由编译器设置为 0。所有 “nuser_XXX” 属性的默认值为 -1,它指示编译器自动将此值设置为模型中定义的关联 user 属性的最大长度。MuJoCo 在任何内部计算中都不会使用这些参数;相反,它们可用于自定义计算。解析器允许 XML 中出现任意长度的数组,编译器随后会将它们调整为 nuser_XXX 的长度。

一些通常在内部计算中使用的元素特定参数也可以用于自定义计算。这是通过安装覆盖仿真管线部分的“用户回调”来完成的。例如,general 执行器元素具有属性 dyntypedynprm。如果 dyntype 设置为 “user”,那么 MuJoCo 将调用 mjcb_act_dyn 来计算执行器动力学,而不是调用其内部函数。由 mjcb_act_dyn 指向的用户函数可以按照它希望的任何方式解释在 dynprm 中定义的参数。但是,此参数数组的长度不能更改(与前面描述的长度在 MJCF 文件中定义的自定义数组不同)。这同样适用于其他回调。

除了上面描述的元素特定用户参数外,还可以通过 custom 元素将全局数据包含在模型中。对于在仿真过程中更改的数据,还有 mjData.userdata 数组,其大小由 size 元素的 nuserdata 属性决定。

求解器设置#

约束力和约束加速度的计算涉及数值求解优化问题。MuJoCo 有三种算法来求解此优化问题:Newton(牛顿法)、CG(共轭梯度法)、PGS(投影高斯-赛德尔法)。它们中的每一个都可以应用于摩擦锥的棱锥模型或椭圆模型,并且可以使用稠密或稀疏约束雅可比矩阵。此外,用户可以指定最大迭代次数和控制提前终止的容差水平。还有一个 NoSlip(无滑移)求解器,这是一个通过指定正数的 NoSlip 迭代次数启用的后处理步骤。所有这些算法设置都可以在 option 元素中指定。

默认设置适用于大多数模型,但在某些情况下,有必要对算法进行调整。最好的方法是使用相关设置进行试验,并使用 simulate.cc 中的可视化性能分析器,它显示了不同计算的耗时以及每次迭代的求解器统计信息。我们提供以下一般准则和观察结果:

  • 对于小型模型,约束雅可比矩阵应为稠密,对于大型模型则应为稀疏。默认设置为 ‘auto’;当自由度数量最多为 60 时,它解析为稠密,超过 60 时则为稀疏。但请注意,阈值用激活的约束数量来定义更好,这取决于模型和行为。

  • 在棱锥摩擦锥和椭圆摩擦锥之间进行选择是一个建模选择,而不是一个算法选择,即它会导致用相同的算法解决不同的优化问题。椭圆锥更接近物理现实。然而,棱锥锥可以提高算法的性能——但不一定。虽然默认为棱锥锥,但我们建议尝试椭圆锥。当接触滑动是一个问题时,抑制它的最好方法是使用椭圆锥、大的 impratio(阻抗比)以及具有非常小容差的 Newton 算法。如果这还不够,请启用 Noslip 求解器。

  • 对于大多数模型,Newton 算法是最佳选择。它在全局最小值附近具有二次收敛性,并且在出奇少的迭代次数内就能达到那里——通常在 5 次左右,很少超过 20 次。它应该与积极的容差值(例如 1e-10)一起使用,因为它能够在不增加延迟的情况下实现高精度(由于最后的二次收敛)。我们看到它减慢的唯一情况是带有椭圆锥和许多滑动接触的大型模型。在这种状态下,Hessian 分解需要大量的更新。它也可能在某些具有不幸的模型元素排序的大型模型中减慢,导致高填充(计算最优消除顺序是 NP-困难的,因此我们依赖于启发式方法)。请注意,可以在性能分析器中监控分解后 Hessian 中的非零值数量。

  • 在上述 Newton 算法减慢的情况下,CG 算法表现良好。通常,CG 显示出良好的线性收敛率,但在迭代次数方面无法与 Newton 竞争,尤其是在需要高精度时。然而,它的迭代速度要快得多,并且不受填充或由于椭圆锥带来的复杂度增加的影响。如果 Newton 被证明太慢,请接着尝试 CG。

  • 当自由度数量多于约束数量时,PGS 求解器是最好的。根据我们的经验,PGS 求解一个约束优化问题,并且具有次线性收敛性,但它通常在前几次迭代中取得快速进展。因此,当可以容忍不精确解时,它是一个不错的选择。对于具有大质量比或其他导致条件不良的模型属性的系统,PGS 收敛趋于相当缓慢。请记住,PGS 执行顺序更新,因此在物理学应该是对称的系统中会打破对称性。相比之下,CG 和 Newton 执行并行更新并保持对称性。

  • NoSlip 求解器是一个改进的 PGS 求解器。它作为主求解器(可以是 Newton、CG 或 PGS)之后的后处理步骤执行。主求解器更新所有未知数。相反,NoSlip 求解器仅更新摩擦维度中的约束力,并忽略约束正则化。这具有抑制由软约束模型引起的漂移或滑移的效果。然而,这种级联的优化步骤不再求解一个定义明确的优化问题(或任何其他问题);相反,它只是一个临时的机制。虽然它通常能完成工作,但我们在具有多个接触之间更复杂交互的模型中看到过一些不稳定性。

  • PGS 在计算约束空间中的逆惯性方面有设置成本(就 CPU 时间而言)。同样,Newton 在 Hessian 的初始分解方面有设置成本,并且根据稍后需要多少分解更新而产生额外的分解成本。CG 没有任何设置成本。由于 NoSlip 求解器也是一个 PGS 求解器,只要启用 NoSlip,就会支付 PGS 设置成本,即使主求解器是 CG 或 Newton。主 PGS 和 NoSlip PGS 的设置操作是相同的,因此当两者都启用时,设置成本仅支付一次。

执行器#

本节描述在 MuJoCo 中使用执行器的各个方面。请参阅关于计算模型的 执行模型

组禁用#

可以通过设置 mjOption.disableactuator 整数位字段在运行时更改的 actuatorgroupdisable 属性,允许用户根据它们的 组 (group) 禁用执行器集。当人们希望为同一运动树使用多种类型的执行器时,此功能很方便。例如,考虑一个具有支持多种控制模式(例如力矩控制和位置控制)的固件的机器人。在这种情况下,可以在同一个 MJCF 模型中定义两种类型的执行器,将一种类型的执行器分配给组 0,另一种分配给组 1。

MJCF 的 actuatorgroupdisable 属性选择默认禁用的组,并且可以在运行时设置 mjOption.disableactuator 以切换活动集。请注意,执行器的总数 mjModel.nu 保持不变,执行器索引也保持不变,因此用户必须知道已禁用执行器的相应 mjData.ctrl 值将被忽略且不会产生任何力。此示例模型 具有三个可以在 simulate 交互式查看器中实时切换的执行器组。请参阅右侧的 示例模型 和相关截屏。

快捷方式#

正如“计算”一章中 执行模型 部分所解释的那样,MuJoCo 提供了一个灵活的执行器模型,具有可以独立指定的传动、激活动力学和力生成组件。全部功能可以通过 XML 元素 general 访问,它允许用户创建各种自定义执行器。此外,MJCF 为配置常用执行器提供了快捷方式。这是通过 XML 元素 motorpositionvelocityintvelocitydampercylindermuscleadhesiondcmotor 完成的。这些不是单独的模型元素。在内部,MuJoCo 仅支持一种执行器类型——这就是为什么当 MJCF 模型被保存时,所有执行器都被写为 general。快捷方式隐式创建通用执行器,将其属性设置为合适的值,并公开可能具有不同名称的属性子集。例如,position 创建一个具有属性 kp(伺服增益)的位置伺服。但是 general 没有 kp 属性。相反,解析器会以协调的方式调整通用执行器的增益和偏置参数,以模拟位置伺服。通过直接使用 general 并将其属性设置为如下所述的某些值,可以达到相同的效果。

执行器快捷方式也与默认设置交互。回想一下,默认设置 机制涉及类,每个类都有一整套用于初始化实际模型元素属性的虚拟元素(每种元素类型一个)。特别是,每个默认类只有一个通用执行器元素。如果我们在同一个默认类中先指定 position 然后指定 velocity,会发生什么?XML 元素按顺序处理,并且通用执行器的属性在每次遇到执行器相关元素时都会被设置。因此 velocity 具有优先权。但是,如果我们在默认类中指定 general,它将仅设置明确给出的属性,而保持其余属性不变。在创建实际模型元素时也会出现类似的复杂情况。假设活动的默认类指定了 position,现在我们使用 general 创建一个执行器并省略其某些属性。丢失的属性将被设置为用于模拟位置伺服的任何值,即使此执行器可能并非旨在作为位置伺服。

鉴于这些潜在的复杂性,我们建议采用一种简单的方法:在默认类中和实际模型元素的创建中都使用相同的执行器快捷方式。如果给定模型需要不同的执行器,要么创建多个默认类,要么避免对执行器使用默认值,而是明确指定它们的所有属性。

力限制#

执行器力通常限制在上下界之间。这些限制可以通过三种方式强制执行:

使用 ctrlrange 进行控制钳制

如果设置了此执行器属性,输入控制值将被钳制。对于简单的 电机,钳制控制输入等同于钳制力输出。

使用 forcerange 在执行器输出端进行力钳制

如果设置了此执行器属性,执行器的输出力将被钳制。此属性对例如 位置执行器 很有用,可以将力保持在界限内。请注意,位置执行器通常还需要控制范围钳制,以避免触及关节限制。

使用 joint/actuatorfrcrange 在关节输入端进行力钳制

此关节属性在穿过 传动 后,钳制作用于关节的所有执行器的输入力。如果传动是平凡的(执行器和关节之间存在一一对应关系),在关节处钳制执行器力等同于在执行器处钳制它们。然而,在多个执行器作用于一个关节或一个执行器作用于多个关节的情况下——尽管实际扭矩由关节处的单个物理执行器施加——在关节本身钳制力是可取的。以下是三个在关节处而非执行器处钳制执行器力是可取的示例:

  • 此示例模型 中,两个执行器,一个 电机 和一个 阻尼器,作用于同一个关节。

  • 此示例模型 (类似于“杜宾车”)中,两个执行器通过 固定肌腱 传动作用于两个轮子,以施加对称(向前/向后滚动)和反对称(向右/向左转弯)扭矩。

  • 此示例模型 中,一个 位置点传动 实现了臂末端执行器的笛卡尔控制器。为了使计算出的扭矩能够由单个、受力限制的关节电机实现,它们需要在关节处进行钳制。

请注意,在这种力/扭矩由传动组合的情况下,应该使用 jointactuatorfrc 传感器来报告作用于关节的总执行器力。标准的 actuatorfrc 传感器将继续报告钳制前的执行器力。

使用 tendon/actuatorfrcrange 在肌腱输入端进行力钳制

此肌腱属性钳制作用于肌腱的所有执行器的输入力。

上面的钳制选项是非排他的,可以根据需要进行组合。

长度范围#

字段 mjModel.actuator_lengthrange 包含可行执行器长度(或者更准确地说,执行器传动的长度)的范围。这是模拟 肌肉执行器 所必需的。在这里,我们专注于 actuator_lengthrange 的含义以及如何设置它。

与 mjModel 的所有其他字段(精确的物理或几何量)不同,actuator_lengthrange 是一个近似值。直观上,它对应于执行器传动在模型的所有“可行”配置中可以达到的最小和最大长度。然而,MuJoCo 约束是软的,因此原则上任何配置都是可行的。但我们需要为肌肉建模定义一个明确的范围。有三种方法可以设置此范围:(1) 使用所有执行器中可用的 lengthrange 属性明确提供它;(2) 从执行器所附着的关节或肌腱的限制中复制它;(3) 自动计算它,如本节其余部分所解释的那样。这里有很多选项,由 XML 元素 lengthrange 控制。

自动计算执行器长度范围是在编译时完成的,结果存储在已编译模型的 mjModel.actuator_lengthrange 中。如果随后保存模型(无论是 XML 还是 MJB),则不需要在下次加载时重复此计算。这很重要,因为计算可能会减慢大型肌肉骨骼模型下的模型编译器。事实上,我们甚至使编译器支持多线程,只是为了加快此操作(不同的执行器在不同的线程中并行处理)。

自动计算依赖于改进的物理模拟。对于每个执行器,我们通过其传动装置施加力(计算最小值时为负,计算最大值时为正),在阻尼状态下推进模拟以避免不稳定性,给予其足够的沉降时间并记录结果。这与带有动量的梯度下降法有关,实际上我们也尝试过基于梯度的显式优化,但问题在于不清楚应该优化什么目标(鉴于软约束的混合)。通过使用模拟,我们本质上是让物理本身告诉我们要优化什么。但请记住,这仍然是一个优化过程,因此它具有可能需要调整的参数。我们提供了保守的默认值,这些值适用于大多数模型,如果不适用,请使用 lengthrange 的属性进行微调。

使用此功能时,请务必记住模型的几何结构。这里的隐含假设是可行的执行器长度确实是有限的。此外,我们不考虑将接触作为限制因素(实际上,我们在模拟中禁用了接触,同时也禁用了被动力、重力、摩擦损耗和执行器力)。这是因为具有接触的模型可能会发生缠绕并产生许多局部极小值。因此,执行器应受到限制,要么是因为模型中定义的关节或肌腱限制(在模拟过程中这些限制是启用的),要么是因为几何结构。为了说明后者,考虑一根肌腱,其一端连接到世界,另一端连接到绕着连接到世界的铰链关节旋转的物体上。在这种情况下,肌腱的最小和最大长度是明确定义的,并且取决于连接点在空间中划出的圆的大小,即使关节或肌腱都没有由用户定义的限制。但如果执行器连接到关节,或者连接到等于关节的固定肌腱上,那么它是无限的。在这种情况下,编译器会返回错误,但它无法判断错误是由于缺乏收敛还是因为执行器长度是无限的。所有这些听起来过于复杂,从我们正在考虑所有可能的极端情况的意义上来说,确实如此。在实践中,长度范围几乎总是用于连接到空间肌腱的肌肉执行器,并且模型中会有定义的关节限制,从而有效地限制了肌肉执行器的长度。如果在此类模型中遇到收敛错误,最可能的解释是您忘记包含关节限制。

有状态执行器#

正如“计算”章节的 驱动模型 部分所述,MuJoCo 支持具有内部动力学的执行器,其状态称为“激活值”(activations)。

激活限制#

有状态执行器的一个有用应用是“积分速度”执行器,由 intvelocity 快捷方式实现。与对传动目标速度进行直接反馈的 纯速度 执行器不同,积分速度 执行器将积分器位置反馈执行器耦合在一起。在这种情况下,激活状态的语义是“位置执行器的设定点”,而控制信号的语义是“位置执行器设定点的速度”。请注意,在真实的机器人系统中,这种积分速度执行器是具有速度语义的执行器的最常见实现方式,而不是对速度进行纯反馈(这在现实生活和模拟中通常都非常不稳定)。

对于积分速度执行器,通常需要对激活状态进行钳制(clamp),否则位置目标将持续积分并超出关节限制,导致失去可控性。要查看激活钳制的效果,请加载下面的示例模型

带有激活限制的示例模型
<mujoco>
<default>
   <joint axis="0 0 1" limited="true" range="-90 90" damping="0.3"/>
   <geom size=".1 .1 .1" type="box"/>
</default>

<worldbody>
   <body>
      <joint name="joint1"/>
      <geom/>
   </body>
   <body pos=".3 0 0">
      <joint name="joint2"/>
      <geom/>
   </body>
</worldbody>

<actuator>
   <general name="unclamped" joint="joint1" gainprm="1" biastype="affine"
      biasprm="0 -1" dyntype="integrator"/>
   <intvelocity name="clamped" joint="joint2" actrange="-1.57 1.57"/>
</actuator>
</mujoco>

请注意,actrange 属性始终以原始单位(弧度)指定,即使关节范围可以是度(默认值)或弧度,具体取决于 compiler/angle 属性。

肌肉#

我们提供了一套用于建模生物肌肉的工具。想要以最小工作量添加肌肉的用户可以在执行器部分的 XML 中使用单行代码实现

<actuator>
    <muscle name="mymuscle" tendon="mytendon">
</actuator>

生物肌肉看起来彼此非常不同,但一旦应用了某些缩放比例,其表现却惊人地相似。我们的默认设置应用了此类缩放,这就是为什么无需调整任何参数即可获得合理的肌肉模型。构建更详细的模型当然需要参数调整,本节将对此进行解释。

请记住,即使肌肉模型非常复杂,它仍然是 MuJoCo 执行器的一种,并且遵循与其他所有执行器相同的约定。可以使用 general 定义肌肉,但 muscle 快捷方式更方便。与其他所有执行器一样,力产生机制和传动装置是独立定义的。尽管如此,肌肉只有在连接到肌腱或关节传动装置时才具有(生物)物理意义。为了具体起见,我们在此假设使用肌腱传动装置。

首先,我们讨论长度和长度缩放。传动装置(即 MuJoCo 肌腱)的可行长度范围将发挥重要作用;请参阅上方的 长度范围 部分。在生物力学中,肌肉和肌腱串联连接并形成肌肉-肌腱执行器。我们的约定略有不同:在 MuJoCo 中,具有空间属性(特别是长度和速度)的实体是肌腱,而肌肉是一种拉动肌腱的抽象力产生机制。因此,MuJoCo 中的肌腱长度对应于生物力学中的肌肉+肌腱长度。我们假设生物肌腱是不可伸缩的,具有恒定的长度 \(L_T\),而生物肌肉长度 \(L_M\) 随时间变化。MuJoCo 肌腱长度是生物肌肉和肌腱长度之和

\[\texttt{actuator\_length} = L_T + L_M \]

另一个重要的常数是肌肉的最佳静息长度,记为 \(L_0\)。它等于肌肉在零速度下产生最大主动力的长度 \(L_M\)。我们不要求用户直接指定 \(L_0\)\(L_T\),因为考虑到肌腱路径和包裹的空间复杂性,很难知道它们的数值。相反,我们通过以下方式自动计算 \(L_0\)\(L_T\)。上面描述的长度范围计算已经提供了 \(L_T+L_M\) 的工作范围。此外,我们要求用户指定肌肉长度 \(L_M\) 的工作范围,并按(尚不可知的)常数 \(L_0\) 进行缩放。这是通过 range 属性完成的;默认的缩放范围是 \((0.75, 1.05)\)。现在,利用实际范围和缩放范围必须相互映射的事实,我们可以计算这两个常数

\[\begin{aligned} (\texttt{actuator\_lengthrange[0]} - L_T) / L_0 &= \texttt{range[0]} \\ (\texttt{actuator\_lengthrange[1]} - L_T) / L_0 &= \texttt{range[1]} \\ \end{aligned} \]

在运行时,我们将缩放后的肌肉长度和速度计算为

\[\begin{aligned} L &= (\texttt{actuator\_length} - L_T) / L_0 \\ V &= \texttt{actuator\_velocity} / L_0 \\ \end{aligned} \]

缩放量的优点在于,所有肌肉在这种表示中表现相似。该行为由许多实验论文中测得的力-长度-速度 (\(\text{\small FLV}\)) 函数捕获。我们对该函数进行如下近似

_images/musclemodel.png _images/musclemodel_dark.png

该函数形式为

\[\text{\small FLV}(L, V, \texttt{act}) = F_L(L)\cdot F_V(V)\cdot \texttt{act} + F_P(L) \]

与 MuJoCo 执行器的一般形式相比,我们看到 \(F_L\cdot F_V\) 是执行器增益,\(F_P\) 是执行器偏差。\(F_L\) 是作为长度函数的主动力,而 \(F_V\) 是作为速度函数的主动力。它们相乘得到总主动力(注意乘以 act,即执行器激活值)。\(F_P\) 是无论激活与否始终存在的被动力。\(\text{\small FLV}\) 函数的输出是缩放后的肌肉力。我们将缩放后的力乘以肌肉特有的常数 \(F_0\) 以获得实际力

\[\texttt{actuator\_force} = -\text{\small FLV}(L, V, \texttt{act}) \cdot F_0 \]

负号是因为正向肌肉激活产生拉力。常数 \(F_0\) 是零速度下的峰值主动力。它与肌肉厚度(即生理横截面积或 PCSA)有关。如果已知,可以使用 muscle 元素的 force 属性进行设置。如果未知,我们将其设置为 \(-1\),这是默认值。在这种情况下,我们依赖于这样一个事实:较大的肌肉倾向于作用于承受更多重量的关节。scale 属性定义了这种关系为

\[F_0 = \text{scale} / \texttt{actuator\_acc0} \]

\(\texttt{actuator\_acc0}\) 由模型编译器预先计算。它是作用在执行器传动装置上的单位力引起的关节加速度的范数。直观地说,\(\text{scale}\) 决定了肌肉“平均”有多强,而其实际强度取决于整个模型的几何和惯性属性。

到目前为止,我们遇到了定义单块肌肉属性的三个常数:\(L_T, L_0, F_0\)。此外,\(\text{\small FLV}\) 函数本身具有上图中所示的几个参数:\(l_\text{min}, l_\text{max}, v_\text{max}, f_\text{pmax}, f_\text{vmax}\)。这些参数对于所有肌肉应该是相同的,但是不同的实验论文提出了不同形状的 FLV 函数,因此熟悉该文献的用户可能希望调整它们。我们提供了 MATLAB 函数 FLV.m,该函数用于生成上图并展示了我们如何计算 \(\text{\small FLV}\) 函数。

在开始设计更准确的 \(\text{\small FLV}\) 函数之前,请考虑这样一个事实:肌肉的工作范围比 \(\text{\small FLV}\) 函数的形状影响更大,而在许多情况下,这个参数是未知的。下面是一个图形说明

_images/musclerange.png _images/musclerange_dark.png

这种图形格式在生物力学文献中很常见,显示了叠加在归一化 \(\text{FL}\) 曲线上的每块肌肉的工作范围(忽略垂直位移)。我们的默认范围以黑色显示。蓝色曲线是两条手臂肌肉的实验数据。人们可以发现工作范围小、范围大、跨越 \(\text{FL}\) 曲线升段、降段或两者兼有的肌肉。现在假设你有一个包含 50 块肌肉的模型。你相信有人做了仔细的实验并测量了你模型中每块肌肉的工作范围,同时考虑了该肌肉跨越的所有关节吗?如果不是,那么最好将肌肉骨骼模型视为与生物系统具有相同的一般行为,而在各种细节上有所不同——包括某些研究界非常关注的细节。对于模型制定者认为恒定且已知的多数肌肉属性,都有实验论文表明它们在某些条件下会发生变化。这并不是要阻止人们构建精确的模型,而是要阻止人们过分相信他们的模型。

回到我们的肌肉模型,存在肌肉激活值 act。这是一个一阶非线性滤波器的状态,其输入是控制信号。滤波器动力学为

\[\frac{\partial}{\partial t}\texttt{act} = \frac{\texttt{ctrl} - \texttt{act}}{\tau(\texttt{ctrl}, \texttt{act})} \]

在内部,控制信号被钳制在 [0, 1] 之间,即使执行器没有指定控制范围。属性 timeconst 指定了两个时间常数,即 \(\text{timeconst} = (\tau_\text{act}, \tau_\text{deact})\),默认值为 \((0.01, 0.04)\)。根据 Millard et al. (2013),有效时间常数 \(\tau\) 在运行时计算为

\[\tau(\texttt{ctrl}, \texttt{act}) = \begin{cases} \tau_\text{act} \cdot (0.5 + 1.5\cdot\texttt{act}) & \texttt{ctrl}-\texttt{act} \gt 0 \\ \tau_\text{deact} / (0.5 + 1.5\cdot\texttt{act}) & \texttt{ctrl} - \texttt{act} \leq 0 \end{cases} \]

由于上述方程描述了不连续的切换,这在使用基于导数的优化时可能是不理想的,因此我们引入了可选的平滑参数 tausmooth。当大于 0 时,切换将被 mju_sigmoid 取代,它将在 \((\texttt{ctrl}-\texttt{act}) \pm \text{tausmooth}/2\) 的范围内平滑地在两个值之间插值。

现在我们总结一下 muscle 元素的属性,用户可能希望根据他们对生物力学文献的熟悉程度以及针对特定模型的详细测量数据的可用性来调整这些属性

默认值

在任何地方都使用内置的默认值。你需要做的就是将肌肉连接到肌腱,如本节开头所示。这会产生一个通用但合理的模型。

scale

如果你不知道每块肌肉的强度,但想让所有肌肉更强或更弱,请调整 scale。这可以为每块肌肉分别调整,但在 default 元素中设置一次更有意义。

force

如果你知道单块肌肉的峰值主动力 \(F_0\),请在此输入。许多实验论文包含此数据。

range

某些论文中也提供了以缩放长度表示的肌肉工作范围。目前尚不清楚此类测量有多可靠(考虑到肌肉作用于多个关节),但它们确实存在。请注意,肌肉之间的范围差异很大。

timeconst

肌肉由慢肌纤维和快肌纤维组成。典型的肌肉是混合型的,但有些肌肉中一种纤维类型的比例较高,使其更快或更慢。这可以通过调整时间常数来建模。\(\text{\small FLV}\) 函数的 vmax 参数也应进行相应调整。

tausmooth

当为正值时,平滑激活和去激活时间常数之间的过渡。虽然单个 运动单元 要么处于激活状态,要么处于去激活状态,但整块肌肉将包含许多单元的混合,从而导致相应的时间尺度混合。

lmin, lmax, vmax, fpmax, fvmax

这些是控制 \(\text{\small FLV}\) 函数形状的参数。高级用户可以对其进行试验;请参阅 MATLAB 函数 FLV.m。与 scale 设置类似,如果你想更改所有肌肉的 \(\text{\small FLV}\) 参数,请在 default 元素中进行。

自定义模型

除了调整我们肌肉模型的参数外,用户还可以通过将 general 执行器的 gaintype、biastype 和 dyntype 设置为“user”并在运行时提供回调来实施不同的模型。或者,将其中一些类型保留为“muscle”并使用我们的模型,同时替换其他组件。请注意,肌腱几何计算仍由标准 MuJoCo 流水线处理,为用户的肌肉模型提供 actuator_length、actuator_velocity 和 actuator_lengthrange 作为输入。自定义回调随后可以模拟弹性肌腱或我们选择省略的任何其他细节。

与 OpenSim 的关系

生物力学研究人员使用的标准软件是 OpenSim。我们设计肌肉模型的初衷是在可能的情况下类似于 OpenSim 模型,同时进行简化,从而实现显著更快、更稳定的模拟。为了帮助 MuJoCo 用户转换 OpenSim 模型,我们在此总结其相似之处和不同之处。

激活动力学模型与 OpenSim 相同,包括默认的时间常数。

\(\text{\small FLV}\) 函数并不完全相同,但 MuJoCo 和 OpenSim 都近似于相同的实验数据,因此它们非常接近。有关 OpenSim 模型和相关实验数据的总结,请参阅 Millard et al. (2013)

我们假设肌腱不可伸缩,而 OpenSim 可以对肌腱弹性进行建模。我们决定不这样做,因为肌腱弹性需要快速平衡假设,这反过来又需要各种调整并且容易导致模拟不稳定。在实践中,肌腱相当僵硬,它们的影响可以通过拉伸对应于不可伸缩情况的 \(\text{FL}\) 曲线来近似(Zajac (1989))。这可以在 MuJoCo 中通过缩短肌肉工作范围来实现。

羽状角(即肌肉与力线之间的角度)在 MuJoCo 中未建模,假设为 0。此效果可以通过缩小肌肉力并调整工作范围来近似。

肌腱包裹在 MuJoCo 中也受到更多限制。我们允许球体和无限圆柱体作为包裹对象,并要求两个包裹对象由肌腱路径中的固定位点分隔。这是为了避免对肌腱路径进行迭代计算的需要。我们还允许在球体或圆柱体内部放置“侧向位点”,这会导致反向包裹:肌腱路径被限制穿过对象而不是绕过它。这可以取代 OpenSim 中用于将肌腱路径保持在给定区域内的环面包裹对象。总的来说,肌腱包裹是将 OpenSim 模型转换为 MuJoCo 模型中最具挑战性的部分,需要一些手动工作。好的一面是,目前使用的高质量 OpenSim 模型数量很少,因此一旦转换完毕,我们就完成了。

下面我们说明四种可用的肌腱包裹类型。请注意,包裹肌腱的弯曲部分被渲染为直线,但几何流水线使用实际曲线并以解析方式计算它们的长度和力矩

image3

传感器#

MuJoCo 可以模拟各种各样的传感器,如下面的 sensor 元素所述。也可以定义用户传感器类型,并由回调 mjcb_sensor 进行评估。传感器不影响模拟。相反,它们的输出被复制到 mjData.sensordata 数组中,供用户处理。

在这里,我们描述所有传感器类型通用的 XML 属性,以避免稍后重复。

name: string, optional

传感器的名称。

noise: real, “0”

此传感器噪声模型的标准差。此属性不影响模拟;它用作存储标准差信息以供后续使用的便捷位置。

cutoff: real, “0”

当此值为正时,它会限制传感器输出的绝对值。它也用于规范化 simulate.cc 中的传感器数据图。请注意,cutoff 对于 碰撞传感器 具有不同的含义。

nsample: int, “0”

如果 nsample 大于 0,则创建一个带有 nsample 个传感器数据槽的时间索引环形缓冲区。在状态推进期间,当前传感器数据以时间戳 time 追加到缓冲区,并移除最旧的样本。历史缓冲区中的值可以通过 mj_readSensor 读取。正的 nsampledelayinterval 功能所必需的。

有关详细信息,请参阅 延迟

interp: [zoh, linear, cubic], “zoh”

从历史缓冲区读取时使用的插值方法。对应于 mj_readSensor 中的 interp 参数。

  • zoh: 零阶保持(分段常数)。

  • linear: 分段线性插值。

  • cubic: 三次样条插值(Catmull-Rom)。

interp 值用于高级用例,有关详细信息,请参阅 延迟

delay: real, “0”

如果大于 0,mjData.sensordata 中的传感器值将从历史缓冲区在 time - delay 时读取,而不是直接计算。需要正的 nsample,不能为负。

在最常见的情况下,delay = nsample * timestep,有关详细信息,请参阅 延迟

interval: real, “0 0”

此属性控制传感器值重新计算的频率。它对于模拟具有比模拟时间步长更长采样周期的传感器非常有用。需要历史缓冲区(nsample > 0)。

此属性由两个实数定义,均以时间单位表示,称为 interval = “period phase”。也可以只指定 period,在这种情况下假定 phase 为 0。

period 指定重新计算之间的间隔周期。默认值 0 具有特殊含义“每个模拟时间步”。请注意,周期不需要是时间步长的整数倍。例如,如果模拟时间步长为 1.0,而 period 为 2.5,则传感器将在时间 0.0, 3.0, 5.0, 8.0, 10.0, 13.0, … 计算,实际间隔在 2 到 3 个时间步长之间交替。period 不能为负。请注意,只有 period > timestep 的值才有意义;小于或等于时间步长的值不会导致错误,只会导致传感器在每个时间步长都被重新计算。

phase 仅在 mj_resetData 中的历史缓冲区初始化期间生效。它指定了传感器在“模拟开始前”最后一次在连续时间内计算的时间(即忽略时间步长的量化)。当使用间隔时,它对于精确控制传感器计算和模拟时间的相对相位非常有用。默认值 0 具有特殊含义“-period”,即指定传感器应在模拟的第一个时间步计算。继续我们之前的示例,如果时间步长为 1.0,间隔为 “2.5 -1.5”,则传感器将在时间 1.0, 4.0, 6.0, 9.0, 11.0, 14.0 等时刻计算。phase 必须在范围 \((-\text{period}, 0]\) 内。

user: real(nuser_sensor), “0 0 …”

请参阅 用户参数

延迟#

执行器和传感器都通过存储带时间戳样本的环形缓冲区支持时间延迟。当整数属性 nsample (执行器, 传感器) 为正时,物理状态 组件 mjData.history 中包含一个具有 nsample 个槽的缓冲区,样本和当前时间戳在状态推进时被写入缓冲区。

如果实值 delay 属性 (执行器, 传感器) 也是正数,则在正向动力学期间,控制值或传感器值将从历史缓冲区读取(而不是分别从 ctrl 读取或重新计算)。正的 delay 需要正的 nsample

请注意,由于读取发生在写入之前,因此最小正延迟实际上是一个时间步长,尽管 delay 是实值。

引擎中的延迟读取由正的 delay 触发,并由 API 函数 mj_readCtrlmj_readSensor 执行,它们在 time - delay 时读取缓冲区,从而有效地实现所请求的延迟。这些函数以 time 作为参数,并且只要 nsample 为正数,就可以使用,从而允许用户随时检查历史缓冲区的内容,包括在“仅历史记录”模式(nsample > 0, delay = 0)下,过去的值可以通过 API 访问,但模拟不受影响。

传感器模式

传感器同时支持 delayinterval/period 属性。组合决定了行为

delay

period

写入/读取行为

= 0

= 0

仅历史记录:每步计算,写入 sensordata,推入历史缓冲区

> 0

= 0

延迟:每步计算,sensordata 包含延迟读取(从缓冲区读取)

= 0

> 0

周期性:按周期计算,sensordata 包含最后计算的值(无延迟)

> 0

> 0

周期性 + 延迟:按周期计算,sensordata 包含延迟读取(从缓冲区读取)

初始化

历史缓冲区由 mj_resetData 初始化如下

  • :始终初始化为零。对于重置后的自定义初始化,调用 mj_initCtrlHistorymj_initSensorHistory

  • 执行器时间戳[..., -2*dt, -dt]

  • 传感器时间戳(无 interval):[..., -2*dt, -dt]

  • 传感器时间戳(有 interval):样本以 period 间隔排列,而不是 dt。连续时间时间戳 [..., phase-2*period, phase-period, phase] 向上舍入到 dt 的最近倍数,因为那是样本可能被计算的时间。如果 phase = 0(默认值),它被解释为 -period,意味着第一个样本将在 t = 0 时计算。

因果关系和插值

最常见的正延迟值是 delay = timestep * nsample,它实现了一个简单的历史缓冲区,没有因果关系问题。

警告

如果 delay > timestep * nsample,则将在最早的缓冲区边界之前读取数据,导致非因果外推:使用它被实际记录之前的值。此场景不会导致运行时错误,因此避免它是用户的责任。

设置 delay < timestep * nsample 并不成问题,对于系统识别和随机延迟很有用。在这些用例中,应该选择一个最大可能的 delay_max 并设置 nsample = ceil(delay_max / timestep)。然后,在运行时或系统识别时间,mjModel 字段 actuator_delaysensor_delay 可以安全地修改,只要不超过 delay_max 即可。

_images/delay_buffer_light.svg _images/delay_buffer_dark.svg

这两个用例是包含 interp 属性(执行器, 传感器)的原因。虽然现实世界的外源延迟通常是零阶保持现象,但这暗示了不连续性:延迟的微小变化没有任何影响,直到越过时间步长阈值。例如,当 dt = 0.1nsample = 2 时,delay = 0.2delay = 0.101 之间没有功能上的区别(两者都从最旧的样本读取),但从 delay = 0.101 步进到 delay = 0.1 会越过阈值并改变行为。通过允许更高阶的插值,延迟的影响变得连续(interp = linear)且可微(interp = cubic)。

请注意,插值对于某些类型的传感器没有意义,例如报告整数值的传感器(例如 insidesite)。

摄像机#

除了默认的、用户可控的自由摄像机外,还可以将“固定”摄像机连接到运动学树上。

外参

默认情况下,摄像机帧连接到所在的物体上。可选的 modetarget 属性可用于指定跟踪(随其移动)或瞄准(看向)物体或子树的摄像机。摄像机看向摄像机帧的负 Z 轴,而正 X 和 Y 分别对应于图像平面中的

投影

摄像机默认使用 透视 投影。将 projection 设置为 orthographic 将切换到正交投影,其中 fovy 属性被解释为长度单位的垂直范围,而不是度数。

内参

摄像机内参使用 ipd(瞳距,立体渲染和 VR 所需)和 fovy(垂直视场角,以度为单位)指定。

上述规范隐含了一个完美的点摄像机,没有像差。但是,当校准真实摄像机时,可以使用标准渲染流水线表达两种类型的线性像差。第一种是垂直和水平方向上不同的焦距(轴向散光)。第二种是非居中的主点。可以使用 focalprincipal 属性指定这些。当使用这些与校准相关的属性时,还必须指定物理 传感器尺寸 和摄像机 分辨率。在这种情况下,可以可视化渲染视锥体。

复合对象#

复合对象是最初设计用于模拟粒子系统、绳索、布料和软体的现有元素的集合。随着时间的推移,这些类型中的大多数已被 replicate(用于重复对象)和 flexcomp(用于软对象)所取代。因此,现在唯一支持的复合类型是 cable,它产生一连串通过球关节连接的不可伸缩物体链。

复合对象由常规 MuJoCo 物体组成,在此上下文中我们称之为“元素体”(element bodies)。元素体的集合由模型编译器自动生成。用户使用 XML 元素 composite 及其属性和子元素,按照 XML 参考章节的说明,对自动生成器进行高级配置。如果随后保存已编译的模型,则 composite 不再存在,取而代之的是自动生成的常规模型元素集合。因此,可以将其视为由模型编译器展开的宏。元素体作为出现 composite 的物体的子项创建;因此,复合对象出现在 XML 中可能定义了常规子物体的地方。每个自动生成的元素体都附带一个几何体。我们将复合对象生成器设计为尽可能具备直观的高级控制,但同时它也公开了大量相互作用并可能深刻影响最终物理效果的选项。因此,用户在某些时候应仔细阅读 参考文档

除了设置物理外,复合对象生成器还创建合适的渲染。对象可以渲染为 皮肤 (skins)。皮肤是自动生成的,并且可以使用双三次插值进行纹理处理和细分。实际的物理,特别是碰撞检测,基于元素体及其几何体,而皮肤纯粹是一个可视化对象。但在某些情况下,我们更喜欢观察皮肤表示,例如 此模型,其皮肤是一个连续的柔性表面,而不是一系列不连续的薄盒子。然而,在微调模型并试图理解其背后的物理原理时,能够渲染几何体非常有用。要切换渲染样式,请禁用皮肤的渲染并为几何体和肌腱启用组 3。

线缆.

作为快速入门,MuJoCo 附带了一个复合线缆示例。在所有示例中,我们都有一个包含在模型中的静态场景,后跟一个复合对象。下面的 XML 代码片段只是复合对象的定义;请参阅发行版中的 XML 模型文件以获取完整示例。

coil

<extension>
   <plugin plugin="mujoco.elasticity.cable"/>
</extension>

<worldbody>
   <composite prefix="actuated" type="cable" curve="cos(s) sin(s) s" count="41 1 1"
              size="0.25 .1 4" offset="0.25 0 .05" initial="none">
      <plugin plugin="mujoco.elasticity.cable">
         <!--Units are in Pa (SI)-->
         <config key="twist" value="5e8"/>
         <config key="bend" value="15e8"/>
         <config key="vmax" value="0"/>
      </plugin>
      <joint kind="main" damping="0.15" armature="0.01"/>
      <geom type="capsule" size=".005" rgba=".8 .2 .1 1"/>
   </composite>
</worldbody>

线缆模拟了一个具有扭转和弯曲刚度的不可伸缩弹性一维对象。它使用一系列胶囊体或盒子进行离散化。其刚度和惯性属性直接根据给定的参数和横截面的形状计算,这允许各向异性行为,例如可以在皮带或计算机线缆中找到。它是一个单一的运动学树,因此无需使用额外的约束即可精确地保持不可伸缩,从而能够使用较大的时间步长。弹性模型在几何上是精确的,并基于计算中心线的 Bishop 或无扭曲框架,即穿过横截面中心的线。几何体的方向相对于此框架表示,然后分解为扭转和弯曲分量,因此可以独立设置不同的刚度。此外,可以指定无应力配置是平坦的还是弯曲的,例如在螺旋弹簧的情况下。该线缆需要使用第一方 引擎插件,将来可能会直接集成到引擎中。

已弃用的类型.

cable 之外的所有复合类型均已弃用或删除。请使用 replicate 处理重复对象(例如粒子系统),使用 flex 可变形对象处理软体(绳索、布料、体积实体)。

可变形对象#

之前描述的 复合对象 旨在在实际上是刚体模拟器的环境中模拟软体。这是可能的,因为 MuJoCo 约束是软的,但尽管如此,它在功能和建模能力上仍受到限制。在 MuJoCo 3.0 中,我们引入了涉及新模型元素的真正可变形对象。前面描述的 皮肤 实际上就是这样一个元素,但它仅用于可视化。我们现在有一个相关的元素 flex,它会根据需要生成接触力、约束力和被动力,以建模各种可变形实体。皮肤和 flex 现在都在 XML 中一个名为 deformable 的新分组元素中定义。flex 是一个低级元素,它指定了运行时所需的一切,但在建模时很难设计。为了辅助建模,我们进一步引入了元素 flexcomp,它自动创建低级 flex,类似于 composite 自动创建模拟软体所需的(MuJoCo 对象集合)的方式。flex 最终可能会取代复合对象,但目前两者都有用,用于不同的目的。

flex 是 MuJoCo 物体的集合,这些物体通过无质量的可拉伸元素连接。这些元素可以是胶囊体(一维 flex)、三角形(二维 flex)或四面体(三维 flex)。在所有情况下,我们都允许设置半径,这使得元素在 1D 和 2D 中变得平滑且具有体积。原始元素说明如下

_images/flexelem.png

到目前为止,这些看起来像几何体。但关键区别在于它们会变形:随着物体(顶点)独立运动,元素的形状会实时改变。碰撞和接触力现在经过概括,以处理这些可变形几何元素。请注意,当两个此类元素发生碰撞时,接触不再仅仅涉及两个物体,而是最多可以涉及 8 个物体(如果两个元素都是四面体)。接触力像以前一样计算,给定接触框架和在该框架中表示的相关量。然后接触力被分配给所有相互作用的物体。接触雅可比的概念很复杂,因为接触点不能被认为在任何物体框架中是固定的。相反,我们使用加权方案将每个接触点“分配”给多个物体。也可以通过将所有顶点分配给同一个物体来创建刚性 flex。这是一种重新利用新的 flex 碰撞机制来实现刚性非凸网格碰撞的方法(不像网格几何体那样为了碰撞目的而凸化)。

变形模型.

为了保持 flex 的形状(在软的意义上),我们需要产生被动力或约束力。在 MuJoCo 3.0 之前,这将涉及大量的肌腱以及肌腱和关节上的约束。在这里仍然可以这样做,但在 flex 较大时,在建模和模拟方面效率都很低。相反,设计理念是使用单一参数集并提供两种建模选择:一种适用于给定 flex 所有边的新型(软)等式约束类型,它允许较大的时间步长;或者一种离散化的连续体表示,其中每个元素都处于恒定应力状态,这等同于分段线性有限元并实现了更高的真实性和准确性。基于边的模型可以看作是一个“集中式”刚度模型,其中变形模式(例如剪切和体积)的正确耦合是在单一量中平均的。相反,连续体模型能够使用材料的 泊松比 分别指定剪切和体积刚度。有关更多详细信息,请参阅 Saint Venant-Kirchhoff 超弹性模型。

参数化类型.

虽然 flexcomp 的默认行为会产生一个“完整”的 flex,其中每个节点对应一个 MuJoCo 物体,但它也为体积对象支持专门的 参数化三线性 (trilinear) 和 二次 (quadratic)。这些选项没有直接模拟所有节点,而是定义了一个背景单元网格。内部顶点的位置通过插值单元角点的位置来计算。三线性 flex 使用 8 节点六面体单元,并在每个轴上进行线性插值,而二次 flex 使用 27 节点单元,并进行二次插值,允许曲线变形模式。这些基于网格的参数化比完整 flex 需要更少的自由度,并且可以显著缩短模拟时间,特别是对于大型体积软体。

创建和可视化.

<option timestep=".001"/>

<worldbody>
   <flexcomp type="grid" count="24 4 4" spacing=".1 .1 .1" pos=".1 0 1.5"
             radius=".0" rgba="0 .7 .7 1" name="softbody" dim="3" mass="7">
      <contact condim="3" solref="0.01 1" solimp=".95 .99 .0001" selfcollide="none"/>
      <edge damping="1"/>
      <elasticity poisson="0.2" young="5e4">
   </flexcomp>
</worldbody>

使用 flexcomp 元素,我们可以从网格(包括四面体网格)创建 flex,并自动生成所有物体/顶点并将它们与合适的元素连接起来。我们还可以自动创建网格和其他拓扑。这种机制使得创建非常大的 flex 变得容易,涉及成千上万个物体、元素和边。显然,此类模拟不会很快。即使对于中等大小的 flex,剪枝碰撞对也是必不可少的。这就是为什么我们开发了精细的自我碰撞剪枝方法;请参阅 XML 参考。

在由四面体组成的 3D flex 的情况下,检查 flex 在内部是如何“三角化”的可能很有用。我们有一种特殊的渲染模式可以剥离外层。下面是斯坦福兔子 (Stanford Bunny) 的一个示例。注意它是如何在外面有更小的四面体,在里面有更大的四面体。这种网格设计是有道理的,因为我们希望碰撞表面是准确的,但在内部我们只需要软材料属性——这需要较少的空间分辨率。为了将表面网格转换为四面体网格,我们推荐使用开放工具,如 fTetWild 库

bunny1 bunny2

包含文件#

MJCF 文件可以使用 include 元素包含其他 XML 文件。从机制上讲,解析器会将主文件中对应于 include 元素的 DOM 节点替换为所包含文件中作为顶级元素子项的 XML 元素列表。顶级元素本身被丢弃,因为出于 XML 目的它是一个分组元素,如果包含,将违反 MJCF 格式。

此功能支持模块化 MJCF 模型;请参阅模型库中的 MPL 系列模型。模块化的一个例子是构建一个机器人模型(通常很复杂),然后将其包含在多个“场景”中,即定义机器人环境中物体的 MJCF 模型。另一个例子是创建一个包含常用资产的文件(例如材质,具有仔细调整的 rgba 值),并将其包含在引用这些资产的多个模型中。

所包含的文件不需要单独成为有效的 MJCF 文件,但它们通常是。事实上,我们设计这种机制是为了允许 MJCF 模型包含在其他 MJCF 模型中。为了使这成为可能,即使在单个模型的上下文中语义上没有意义,也允许重复的 MJCF 部分。例如,我们允许运动学树具有多个根(即多个 worldbody 元素),解析器会自动合并这些根。否则,将机器人包含到场景中将是不可能的。

重复 MJCF 部分的灵活性是有代价的:应用于整个模型的全局设置,例如 compilerangle 属性,可以定义多次。MuJoCo 允许这样做,并使用在处理完所有 include 元素后在复合模型中遇到的最后一个定义。因此,如果模型 A 是以度定义的,模型 B 是以弧度定义的,并且 A 在 B 的 compiler 元素之后包含在 B 中,则整个复合模型将被视为以度定义——在这种情况下会导致不良后果。用户必须确保相互包含的模型在这种意义上是兼容的;局部坐标与全局坐标是另一个兼容性要求。

最后,如下所述,元素名称在所有相同类型的元素中必须是唯一的。因此,例如,如果在两个模型中使用相同的几何体名称,并且一个模型包含在另一个模型中,这将导致编译错误。包含同一个 XML 文件多次是一个解析错误。此限制的原因是我们希望避免重复的元素名称以及由包含引起的无限递归。

命名元素#

MJCF 中的大多数模型元素都可以有名词。它们是用相应 XML 元素的 name 属性定义的。当给定的模型元素被命名时,它的名称在所有相同类型的元素中必须是唯一的。名称区分大小写。它们在编译时用于引用相应的元素,并为了用户在运行时的方便而保存在 mjModel 中。

名称通常是一个可选属性。我们建议将其保持未定义(以便保持模型文件更短),除非有特定的理由定义它。可能有几个这样的原因

  • 一些模型元素需要将其他元素作为其创建的一部分来引用。例如,空间肌腱需要引用位点以指定它通过的途经点。引用只能通过名称完成。请注意,资产存在是为了被引用,因此它们必须有名词,但是它可以省略并从相应的文件名中隐式设置。

  • 可视化工具提供了标记给定类型的所有模型元素的选项。当有名词时,它会打印在 3D 视图中对象旁边;否则会打印“body 7”格式的通用标签。

  • 函数 mj_name2id 返回具有给定类型和名称的模型元素的索引。相反,函数 mj_id2name 根据索引返回名称。这对于涉及模型元素的自定义计算很有用,该模型元素在 XML 中通过其名称标识(而不是依赖于编辑模型时可能会改变的固定索引)。

  • 模型文件原则上可以通过命名某些元素变得更易读。但是请记住,XML 本身具有注释机制,该机制更适合实现可读性——特别是因为大多数文本编辑器都提供检测 XML 注释的语法高亮显示。

URDF 扩展#

统一机器人描述格式 (URDF) 是一种流行的 XML 文件格式,许多现有机器人都以这种格式建模。这就是为什么我们实现了对 URDF 的支持,即使它只能表示 MuJoCo 中可用模型元素的一个子集。除了标准的 URDF 文件外,MuJoCo 还可以加载具有自定义(从 URDF 的角度来看)mujoco 元素作为顶级 robot 元素子项的文件。此自定义元素可以具有与 MJCF 中功能相同的子元素 compiler, option, size,只是默认的编译器设置被修改以适应 URDF 建模约定。特别是 compiler 扩展非常有用,事实上引入它的几个属性是因为许多现有的 URDF 模型具有非物理动力学参数,如果保持不变,MuJoCo 的内置编译器将拒绝这些参数。此扩展还需要指定网格目录。还要注意,编译器属性 strippath, angle, fusestaticdiscardvisual 对于 URDF 和 MJCF 具有不同的默认值。

请注意,虽然 MJCF 模型由解析器根据自定义 XML 模式进行检查,但 URDF 模型不会。即使嵌入在 URDF 文件中的 MuJoCo 特定元素也不会被检查。结果,拼写错误的属性名称被默默忽略,如果拼写错误未被发现,可能会导致重大混乱。

这是一个 URDF 模型的扩展部分示例

<robot name="darwin">
  <mujoco>
    <compiler meshdir="../mesh/darwin/" balanceinertia="true" discardvisual="false"/>
  </mujoco>
  <link name="MP_BODY">
    ...
</robot>

上述扩展使 URDF 更易用,但仍然有限。如果用户希望在充分利用 MuJoCo 的同时保持 URDF 兼容性,我们建议遵循以下程序。根据需要引入 URDF 中的扩展,加载它并将其保存为 MJCF。然后尽可能使用 include 元素向 MJCF 添加信息。这样,如果 URDF 被修改,相应的 MJCF 可以很容易地重新创建。不过根据我们的经验,URDF 文件倾向于静态,而 MJCF 文件经常被编辑。因此在实践中,通常只需将 URDF 转换为 MJCF 一次,之后只使用 MJCF 即可。

运动捕捉 (MoCap) 物体#

mocap 物体是世界的静态子项(即没有关节),它们的 mocap 属性设置为“true”。它们可用于将来自运动捕捉设备的数据流输入到 MuJoCo 模拟中。假设你拿着一个 VR 控制器,或者一个用运动捕捉标记(例如 Vicon)进行仪器化处理的物体,并希望有一个以同样方式移动的模拟对象,同时也与其他模拟对象交互。这里有一个困境:虚拟对象无法推动你的物理手,因此你的手(从而你正在控制的对象)会违反模拟物理规律。但与此同时,我们希望最终的模拟是合理的。我们该怎么做?

第一步是在 MJCF 模型中定义一个动作捕捉(mocap)刚体,并实现一段在运行时读取数据流的代码,将 mjData.mocap_posmjData.mocap_quat 设置为从动作捕捉系统接收到的位置和方向。代码示例 simulate.cc 使用鼠标作为动作捕捉设备,允许用户移动动作捕捉刚体。

particle

关于动作捕捉刚体,需要理解的关键点是模拟器将其视为固定的。我们通过直接更新它们的位置和方向来使它们在模拟时间步之间产生位移,但就物理模型而言,它们的位置和方向是恒定的。那么,如果我们像 MuJoCo 发行版中提供的粒子示例(回想一下,在这些示例中,我们有一个作为动作捕捉刚体的胶囊体探针,可以通过鼠标移动)那样与常规动力学刚体接触会发生什么?两个常规刚体之间的接触会经历穿透以及相对速度,而与动作捕捉刚体的接触则缺失了相对速度分量,因为模拟器不知道动作捕捉刚体本身正在运动。因此,产生的接触力较小,动力学对象被推开所需的时间更长。此外,在更复杂的模拟中,这种物理上的不一致性可能会导致不稳定性。

然而,有一种表现更好的替代方案。除了动作捕捉刚体外,我们还添加第二个常规刚体,并通过焊接等式约束(weld equality constraint)将其连接到动作捕捉刚体上。在下方的图中,粉色盒子是动作捕捉刚体,它连接到手部的基座。在没有其他约束的情况下,手部几乎能完美地追踪动作捕捉刚体(比弹簧阻尼器效果好得多),因为约束是隐式处理的,可以在不破坏模拟稳定性的情况下产生巨大的力。但是,如果强迫手部与桌面接触(右图),它就无法同时满足接触约束并追踪动作捕捉刚体。这是因为动作捕捉刚体可以自由地穿过桌面。那么哪个约束会获胜?这取决于焊接约束相对于接触约束的柔度。需要调整相应的 solrefsolimp 参数,以实现所需的权衡。关于此示例,请参阅 MuJoCo 论坛上提供的模块化假肢(MPL)手部模型;下方的图就是使用该模型生成的。

image18 image19

内存分配#

MuJoCo 在 mjData 中预分配了运行时所需的所有内存,且在模型创建后不会访问堆分配器。mjData 中的内存由 mj_makeData 分配为两个连续的块。

  • mjData.buffer 包含固定大小的数组。

  • mjData.arena 包含动态大小的数组。

arena 内存空间中分配了两种类型的动态数组。

  • 接触(contacts)和约束相关的数组从 arena 的起始处开始排列。

  • 堆栈(stack) 数组从 arena 的末尾处开始排列。

通过从 arena 空间的两侧分配动态量,可变大小的内存分配由单个数值控制:即 size MJCF 元素的 memory 属性。与 buffer 中的固定大小数组不同,arena 中的可变大小数组可以为 NULL,例如在调用 mj_resetData 之后。当 arena 内存耗尽时,根据请求的内存类型,会发生以下三种情况之一。

  • 如果内存耗尽发生在接触分配期间,将发出警告,且该步骤中不会添加后续的接触,但模拟会照常继续。

  • 如果内存耗尽发生在约束相关的分配期间,将发出警告,且该步骤中约束求解器将被禁用,但模拟会照常继续。请注意,没有约束求解器的物理过程通常会大不相同,但在某些情况下(如场景初始化时多个物体暂时重叠)允许模拟继续运行可能仍然有用。

  • 如果内存耗尽发生在堆栈数组分配期间,则会发生严重错误。

buffer 的大小不同,arena 的大小无法预先计算,因为接触数量和堆栈使用量事先未知。那么该如何选择它呢?目前使用以下简单的启发式方法,尽管未来可能会改进:在最坏情况下,为 100 个接触和 500 个标量约束分配足够的内存。如果此启发式方法不足,我们建议按照以下步骤操作:使用 memory 属性大幅增加 arena 内存,并检查运行时实际使用的内存量。mjData.maxuse_arena 会跟踪自上次重置以来 arena 内存的最大使用量。simulate 查看器会将此数值显示为 arena 总空间的一部分(在左下角的信息窗口中)。因此,可以先设置一个较大的数值,模拟一段时间,如果比例很小,再回到 XML 文件中减小分配大小。但请记住,根据活跃约束的数量和所使用的约束求解器,内存使用情况可能会在模拟过程中发生剧烈变化。CG 求解器内存效率最高,其次是 Newton 求解器,PGS 求解器内存占用最大。在设计模型时,我们通常在探索模型时遇到的最坏情况下争取达到 50% 的利用率。如果您只打算使用 CG 求解器,则可以使用较小的 arena 分配。

提示与技巧#

在此我们提供关于如何完成一些常见建模任务的指导。此处没有新材料,因为本节中的所有内容都可以从文档的其他部分推断出来。尽管如此,推断过程并不总是显而易见的,因此将其详细说明可能会有所帮助。

性能调优#

以下是可以采取以最大化模拟吞吐量的步骤列表。所有的建议都涉及参数调整。建议在查看 simulate 工具内置的分析器时,以交互方式进行这些操作。testspeed 工具也提供了一份详细且有时更有用的分析报告。在进行下述更详尽的步骤时,应以分析器报告的最昂贵的流水线组件为目标。请注意,其中一些在 MJX 中略有不同,请参阅相关章节 therein

  1. 时间步长:尝试增加模拟时间步长。正如在 数值积分 部分末尾所解释的那样,时间步长是任何模型中最重要的参数。默认值是为了稳定性而非效率而选择的,通常可以增加。在某个点上,进一步增加它会导致发散,因此最佳时间步长是永远不会发生发散或发散极罕见的最大时间步长。具体数值取决于模型。

  2. 积分器:根据 数值积分 部分末尾的建议选择您的积分器。默认推荐的选择是 implicitfast 积分器。

  3. 约束雅可比矩阵:尝试在“dense”(稠密)和“sparse”(稀疏)之间切换雅可比矩阵设置。这两个选项使用不同的代码路径分别使用稠密或稀疏代数,但在计算上是等效的,因此总是优先选择较快的一个。默认的“auto”启发式方法并不总是做出正确的选择。

  4. 约束求解器:如果分析器报告大量时间花费在求解器上,请考虑以下建议:

    • solver:默认的 Newton 求解器通常是最快的,因为它收敛所需的迭代次数最少。对于大型模型,CG 求解器可能更快;对于自由度多于约束的模型,PGS 求解器将是最快的,尽管这种情况并不常见。

    • iterationstolerance:尝试减少迭代次数,或者等效地增加求解器的终止容差。特别是对于通常在 2-3 次(昂贵的)迭代中达到数值收敛的 Newton 求解器,最后一次迭代提高的精度水平通常没有明显效果,可以跳过。

  5. 碰撞:如果分析器报告碰撞检测占用了大量计算时间,请考虑以下步骤:

    • 使用 碰撞检测 部分所述的 contype / conaffinity 机制减少检查的碰撞次数。

    • 修改碰撞几何体,用更便宜的原始体-原始体碰撞代替昂贵的碰撞测试(例如网格-网格)。作为一个经验法则,在 engine_collision_driver.c 顶部碰撞表中具有自定义成对函数的碰撞,比使用通用凸-凸碰撞器 mjc_Convex 的碰撞显著更便宜。最昂贵的碰撞是涉及 SDF 几何体的碰撞。

    • 如果将碰撞网格替换为原始体不可行,请尽可能简化网格。像 trimesh、Blender、MeshLab 和 CoACD 这样的开源工具在这方面非常有用。

  6. 摩擦锥:椭圆锥更准确,在高 impratio 下防滑效果更好,但计算成本更高。如果精确摩擦力不重要,请尝试切换到金字塔锥。

  7. 对于自定义构建,MuJoCo 可以使用 32 位浮点精度(而非默认的 64 位)编译。对于内存带宽是瓶颈的大型模型,这可以提高性能。有关更多信息,请参阅 mjtNum。注意,float32 在典型模型中很少产生可测量的加速。

防止打滑#

以下是一系列可以用来诊断和解决接触打滑的步骤,这在操作任务中尤为棘手。为了诊断打滑,建议使用 simulate 工具的内置可视化选项来检查接触和接触力。调整接触和力的视觉尺寸(使用全局 meansize 或特定的 contactwidthcontactheightforcewidth 属性)以及 力缩放 属性,通常有助于更好地可视化和理解接触配置及产生的力。

防止打滑的接触力超出了摩擦锥。

这意味着物理上甚至在原则上也无法防止打滑。这种情况发生在:

  1. 法向力太小。 确保抓取器能够施加的最大力乘以滑动摩擦系数远大于物体的重量。

  2. 滑动摩擦系数太低。 增加滑动 friction 系数。

  3. 扭转摩擦力不足以施加所需的扭矩。condim 增加到 4 或 6,并选择合适的摩擦系数。condim 4 启用扭转摩擦,防止绕法线旋转。condim 6 还启用滚动摩擦,防止绕切线方向旋转。详细信息及这些系数的具体语义请参阅 接触 部分。

几何形状不支持所需的力或扭矩。

这是一个常见的现实世界问题,通过改进抓取器和手柄的设计来解决。

  1. 改进接触几何体的几何形状以增加更多的接触点,可能采用非平面几何(如凸起),从而通过法向力而非仅仅通过摩擦分量来防止打滑。

  2. 如果接触发生在平面之间,确保标记 multiccd 没有被禁用(默认启用),因为它允许检测器找到比凸-凸碰撞器返回的单个接触更多的接触点。

  3. 确保标记 nativeccd 没有被禁用(默认启用),因为 NativeCCD 是一种更准确、高效的凸碰撞检测算法。

高频振动

高频、低幅度的振动在许多工业环境中也是一个现实问题,但与模拟不同,在现实世界中它们是可以听到的。此类振动通常由高增益控制器引起,有时由来自接触或关节的粘滑(stick-slip)反馈引起,并与机构的本征模态产生共振。诊断此类振动最简单的方法是在 simulate 中可视化接触力。解决方法通常是减小 timestep 和/或向相关关节添加一些 armature(电枢惯量)。振动的另一个原因是显式阻尼的反馈。请使用隐式或隐式快速积分器,如 数值积分 部分所述。

缓慢打滑

与上述导致快速打滑的问题不同,缓慢、渐进的打滑是 MuJoCo 接触模型设计的固有属性,因为没有它,逆动力学就没有定义。这在 柔度和打滑 的澄清说明中进行了详细讨论。这种类型的打滑可以通过两种方式解决。

  1. 增加 impratio 参数。这将减少(但不能完全防止)缓慢打滑。注意,高 impratio 值仅与 椭圆锥 配合使用效果良好。

  2. 通过将 noslip_iterations 增加到一个正整数来启用 NoSlip 求解器。一个小数字(1、2 或 3)通常就足够了。NoSlip 后处理求解器将完全防止打滑,代价是使得逆动力学定义不明确并增加了计算成本。

背隙(Backlash)#

背隙存在于许多机器人关节中。它通常是由齿轮箱中齿轮之间的小间隙引起的,也可能是由关节机构中的松动引起的。其影响是电机可以在关节转动前转动一个小角度,反之亦然(当外力作用于关节时)。背隙可以在 MuJoCo 中建模如下:不要在刚体内部定义单个铰链关节,而是定义两个位置和方向相同的铰链关节。

<body>
  <joint name="J1" type="hinge" pos="0 0 0" axis="0 0 1" armature="0.01"/>
  <joint name="J2" type="hinge" pos="0 0 0" axis="0 0 1" limited="true" range="-1 1"/>
</body>

因此,刚体相对于其父级的总旋转量为 J1+J2。现在定义一个仅作用于 J1 的执行器。J2 上较小的关节范围使其保持在 0 附近,但也允许它在作用于其上的力方向上移动一点,从而产生背隙效果。注意 J1 中的 armature 属性。没有它,关节空间的惯性矩阵将是奇异的,因为这两个关节可能会向相反方向加速而不会遇到任何惯性。产生背隙的物理齿轮实际上具有旋转惯性(我们称之为电枢惯量),因此这是一种现实的建模方法。该示例中的数字应根据需要进行调整,以获得预期的行为。关节限制约束的 solrefsolimp 参数也可以进行调整,使背隙旋转在更柔和或更硬的限制处结束。

除了在 J2 中指定关节限制外,还可以指定一个弱等式约束,保持 J2=0。约束阻抗函数应进行调整,使约束在 J2=0 附近较弱,在远离 0 时变强。在 求解器参数 中显示的新阻抗函数参数化实现了这一点。与关节限制相比,等式约束方法将在背隙区间和极限区间之间产生更柔和的过渡。它也将始终处于活跃状态,这对于需要在用户代码中将约束违规或约束力作为输入的场景非常方便。

恢复系数(Restitution)#

存在另一种指定 solref 的机制,如 求解器参数 中所述。当两个数字均为非正数时,它们被解释为 (-刚度, -阻尼) 并由约束阻抗进行缩放。为了实现接触和其他约束的完美恢复,将刚度设置为一个合理的大值,并将阻尼设置为零。下面是一个球在平面上反弹的例子,恢复系数为 1,这样接触前后的能量大致得以保留。它不能被完全保留,因为接触本身是柔性的,需要几个时间步长,且这些时间步长中的(隐式)变形并非精确的能量守恒。但总体效果是球反弹了很长时间而其峰值高度没有可见的变化,能量围绕初始值波动而不是漂移。

<worldbody>
  <geom type="plane" size="1 1 .1"/>

  <body pos="0 0 1">
    <freejoint/>
    <geom type="sphere" size="0.1" solref="-1000 0"/>
  </body>
</worldbody>

约束精度#

MuJoCo 的 约束阻抗 计算依赖于约束空间惯性矩阵的近似对角线,该对角线在编译时从初始配置 qpos0 计算得出。

在绝大多数模型中,这种近似完全足够。然而,在某些情况下——例如具有高度各向异性惯性的模型、复杂的运动链或远离 qpos0 运行的刚体——近似可能会变得不准确。这有时表现为无法解释的求解器发散(badqacc 警告)、过度穿透、不切实际的打滑或求解器收敛不良。一个有用的诊断工具是 fwdinv 标记:如果正逆向差异较大,不准确的约束缩放可能是一个促成因素。

如果您怀疑编译时的近似对于您的模型不足,您可以启用 diagexact 标记以在运行时计算精确的对角线。有关底层机制和性能影响的详细信息,请参阅 对角线近似