环境准备
# 导入必要的库
import mujoco
import numpy as np
import matplotlib.pyplot as plt
# mujoco的jax加速版本,提供GPU加速和并行仿真
from mujoco import mjx
# 媒体文件处理
import mediapy as media
# XML模型处理
import xml.etree.ElementTree as ET
from typing import List, Dict, Tuple
import tempfile
import os
本节静态渲染代码均采用如下形式,因此后文将省略
with mujoco.Renderer(model) as renderer:
# 执行前向动力学计算,更新所有派生量
mujoco.mj_forward(model, data)
# 更新渲染场景
renderer.update_scene(data)
# 渲染并显示
media.show_image(renderer.render())
程序化生成 MJCF
MJCF 格式是 MuJoCo 的原生模型描述格式,使用 XML 语法定义物理仿真环境中的所有元素。
我们目前接触的 MJCF 的主要组成部分如下:
option:仿真选项,如时间步长、重力、求解器等asset:资源定义,如材质、纹理、网格等worldbody:场景层次结构,定义所有物理元素及其关系actuator:执行器定义,用于控制模型中的关节sensor:传感器定义,用于获取仿真数据default:默认参数设置,可以被继承contact:接触参数设置
ElementTree
至此,我们的 MJCF 都通过字符串手动编写,这对于复杂模型会变得繁琐且容易出错。程序化生成 MJCF 使用 ElementTree 库构建 XML 树,然后将其转换为 MJCF 字符串,其优势如下:
- 可扩展性:轻松创建具有重复结构的复杂模型
- 参数化:通过改变参数快速生成不同变体
- 一致性:确保模型的各个部分遵循相同的命名和结构规则
- 可维护性:集中管理模型生成逻辑,便于修改和扩展
下面是一个程序化生成链条模型的函数:
def create_chain_model(num_links: int, link_length: float = 0.5) -> str:
"""
创建一个多连杆链条模型 / Create a multi-link chain model
Args:
num_links: 链条节数 / Number of links
link_length: 每节长度 / Length of each link
"""
# 创建根元素 / Create root element
root = ET.Element("mujoco")
# 添加选项 / Add options
option = ET.SubElement(root, "option")
option.set("timestep", "0.001")
option.set("gravity", "0 0 -9.81")
# 添加默认设置 / Add defaults
default = ET.SubElement(root, "default")
joint_default = ET.SubElement(default, "joint")
joint_default.set("damping", "0.5")
# 创建世界物体 / Create worldbody
worldbody = ET.SubElement(root, "worldbody")
# 添加地面 / Add ground
ground = ET.SubElement(worldbody, "geom")
ground.set("type", "plane")
ground.set("size", "10 10 0.1")
ground.set("rgba", "0.8 0.8 0.8 1")
# 固定基座 / Fixed base
base = ET.SubElement(worldbody, "body")
base.set("name", "base")
base.set("pos", "0 0 1")
base_geom = ET.SubElement(base, "geom")
base_geom.set("type", "box")
base_geom.set("size", "0.1 0.1 0.1")
base_geom.set("rgba", "0 0 0 1")
# 创建链条 / Create chain
parent = base
actuators = ET.SubElement(root, "actuator")
sensors = ET.SubElement(root, "sensor")
for i in range(num_links):
# 创建链节 / Create link
link = ET.SubElement(parent, "body")
link.set("name", f"link_{i}")
link.set("pos", f"{link_length} 0 0")
# 添加关节 / Add joint
joint = ET.SubElement(link, "joint")
joint.set("name", f"joint_{i}")
joint.set("type", "hinge")
joint.set("axis", "0 0 1")
joint.set("range", "-90 90")
# 添加几何体 / Add geometry
geom = ET.SubElement(link, "geom")
geom.set("type", "capsule")
geom.set("fromto", f"0 0 0 {link_length} 0 0")
geom.set("size", "0.05")
# 渐变颜色 / Gradient color
color = i / num_links
geom.set("rgba", f"{1-color} {color} 0 1")
# 添加执行器 / Add actuator
motor = ET.SubElement(actuators, "motor")
motor.set("name", f"motor_{i}")
motor.set("joint", f"joint_{i}")
motor.set("gear", "50")
motor.set("ctrlrange", "-1 1")
# 添加传感器 / Add sensor
pos_sensor = ET.SubElement(sensors, "jointpos")
pos_sensor.set("name", f"pos_{i}")
pos_sensor.set("joint", f"joint_{i}")
parent = link
# 转换为字符串 / Convert to string
return ET.tostring(root, encoding='unicode')
# 创建5节链条 / Create 5-link chain
chain_xml = create_chain_model(5)
chain_model = mujoco.MjModel.from_xml_string(chain_xml)
chain_data = mujoco.MjData(chain_model)
print(f"生成的链条模型 / Generated chain model:")
print(f"- 关节数 / Joints: {chain_model.njnt}")
print(f"- 执行器数 / Actuators: {chain_model.nu}")
print(f"- 传感器数 / Sensors: {chain_model.nsensor}")
生成的链条模型 / Generated chain model:
- 关节数 / Joints: 5
- 执行器数 / Actuators: 5
- 传感器数 / Sensors: 5

代码解析
- 层次结构
- 使用
ET.Element("root_name")创建根元素 - 使用
ET.SubElement(parent_name, "sub_name")在指定的父元素下创建子元素 - 在
for循环中创建子元素,并在循环末尾更新子元素为新的父元素,可以形成链式结构
- 使用
- 参数定义
- 使用
element_name.set("parameter_name", "parameter_value")设置指定元素的指定参数值
- 使用
- 字符串生成
- 使用
ET.tostring(root, encoding='unicode')从根元素生成 XML 字符串
- 使用
mjspec API
除了使用 ElementTree 构建 XML,我们还可以使用MuJoCo 的 mjspec API,它允许在运行时直接修改模型结构,具有如下优势:
- 直接操作:无需 XML 解析和序列化的开销
- 类型安全:API 提供类型检查,减少错误
- 即时反馈:修改后可以立即编译和测试
- 动态适应:可以根据仿真状态动态调整模型
下面使用 mjspec 创建一个球体阵列模型:
# 创建一个简单的基础模型 / Create a simple base model
base_xml = """
<mujoco>
<worldbody>
<geom type="plane" size="10 10 0.1" rgba="0.8 0.8 0.8 1"/>
</worldbody>
</mujoco>
"""
# 使用 spec 动态添加物体 / Dynamically add bodies using spec
spec = mujoco.MjSpec()
spec.from_string(base_xml)
# 获取 worldbody / Get worldbody
worldbody = spec.worldbody
# 动态添加多个球体 / Dynamically add multiple spheres
for i in range(3):
for j in range(3):
# 添加物体 / Add body
body = worldbody.add_body()
body.name = f"sphere_{i}_{j}"
body.pos = [i*0.5 - 0.5, j*0.5 - 0.5, 1.0]
# 添加自由关节 / Add free joint
joint = body.add_joint()
joint.type = mujoco.mjtJoint.mjJNT_FREE
# 添加几何体 / Add geometry
geom = body.add_geom()
geom.type = mujoco.mjtGeom.mjGEOM_SPHERE
geom.size = [0.1, 0, 0]
geom.rgba = [i/2, j/2, 1, 1]
geom.mass = 0.1
# 编译模型 / Compile model
dynamic_model = spec.compile()
dynamic_data = mujoco.MjData(dynamic_model)
print(f"动态创建的模型 / Dynamically created model:")
print(f"- 物体数 / Bodies: {dynamic_model.nbody}")
print(f"- 自由度 / DOFs: {dynamic_model.nv}")
动态创建的模型 / Dynamically created model:
- 物体数 / Bodies: 10
- 自由度 / DOFs: 54

代码解析
- 模型创建
mujoco.MjSpec():创建模型规范对象,其为模型的内存表示,可动态修改spec_name.from_string(xml_name):XML 和 Spec 之间支持相互转换
- 组件访问
mujoco下的一级节点可以作为spec的属性直接访问并修改,例如spec.option.timestep = 0.001- 二级及以下节点需要通过父节点的
add_*方法添加,在显式创建后才能访问并修改 - 组件类型需要通过相应的枚举定义(
mjtJoint,mjtGeom等)
- 模型编译
spec.compile():将spec转换为优化的仿真模型spec编译后无法再修改,但可以通过spec.clone()保存其模板
高级建模功能
MJCF 还有其他强大的建模组件,这里再介绍两种。
composite
composite 组件可以创建软体和复杂几何结构,包括以下 type 类型:
| 类型 | 维度 | 主要应用 | 约束类型 |
|---|---|---|---|
particle | 3D | 颗粒 | 接触 |
grid | 2D | 薄膜、布料 | 边约束 |
cloth | 2D | 布料、薄膜 | 边+对角约束 |
rope | 1D | 绳索、链条 | 拉伸约束 |
cable | 1D | 电缆、肌腱 | 拉伸+弯曲约束 |
box | 3D | 立方软体 | 体约束 |
cylinder | 3D | 圆柱软体 | 体约束 |
| ellipsoid | 3D | 椭球软体 | 体约束 |
composite 是一种复合体,是由 geom 定义的小单元组成的阵列,阵列参数在 composite 中给出:
count:小单元在阵列的三个维度上的数量,对于一维和二维的复合体,不包含的维度配置为 1spacing:相邻小单元之间的间距mass:复合体的总质量,自动分配到每个小单元
以下是一个颗粒系统的例子:
<composite type="particle" count="5 5 5" spacing="0.03" mass="0.1">
<geom type="sphere" size="0.015" rgba="1 0 0 1"/>
</composite>
通常情况下,sphere 用于颗粒状集合,capsule 用于链式链接,box 用于块状结构。
除此之外,子元素 skin 可以为 composite 添加视觉外观、连续表面,例如下面这个布料:
<composite type="cloth" count="10 10 1" spacing="0.04">
<skin material="fabric" thickness="0.001"/>
<geom type="box" size="0.015" mass="0.001"/>
</composite>
equality
equality 组件用于在模型中的任意两个实体之间创建约束,不受层级结构限制,比关节更加灵活。以下是 equality 不同类型的子元素组件:
connect:使两个物体上两个点重合,物体仍可各自绕点旋转distance:使两个物体上两个点保持固定的距离,物体仍可各自绕点旋转weld:固定两个物体之间的相对位姿,使之像同一个物体一样运动joint:建立起两个关节的广义位置之间的多项式映射关系tendon:建立起两个肌腱的标量长度之间的多项式映射关系
每种约束类型都有如下通用参数:
name:约束的标识名称type1/2:约束所作用的两个实体的名称(type对应约束的实体类型)active:布尔值,控制约束是否生效solimp:阻抗参数solref:约束求解器
下面是一些例子:
<equality>
<!-- 假设有两个名为 box1 和 box2 的刚体 -->
<!-- 两个物体将始终保持 0.5 米的距离 -->
<distance name="rod_constraint"
body1="box1" body2="box2"
anchor1="0.1 0 0" anchor2="-0.1 0 0"
distance="0.5"
solref="0.02 1" />
<!-- 调整 solref 可以使它更像硬杆或弹性杆 -->
</equality>
anchor:作用点在物体局部坐标系下的位置distance:作用点之间的距离
<equality>
<!-- 假设有两个名为 hinge1 和 hinge2 的关节 -->
<!-- 此约束强制 hinge2 的角度永远是 hinge1 的两倍 -->
<joint name="couple_hinges"
joint1="hinge1" joint2="hinge2"
polycoef="0 2 0 0 0" />
</equality>
polycoef:关节之间广义位置映射关系的多项式系数,假设关节 1 和 2 的广义位置分别为 q1q_1q1 和 q2q_2q2,polycoef数组中第 n 个元素为 cnc_ncn,则其数学表达式为
q2=∑n=0N−1cn⋅q1 q_2=\sum^{N-1}_{n=0}c_n\cdot q_1 q2=n=0∑N−1cn⋅q1
模型验证与优化
在渲染模型之前,检查模型的物理合理性并优化性能可以取得更好的效果。下面的代码包含了一些基本的验证和优化方面:
def validate_model(model: mujoco.MjModel) -> Dict[str, List[str]]:
"""
验证模型的物理合理性 / Validate physical validity of model
"""
warnings = {
"mass": [],
"inertia": [],
"collision": [],
"joint": []
}
# 检查质量 / Check masses
for i in range(model.nbody):
mass = model.body_mass[i]
if mass <= 0 and i > 0: # 跳过world body
warnings["mass"].append(f"Body {i} has zero or negative mass")
elif mass > 1000:
warnings["mass"].append(f"Body {i} has very large mass: {mass}")
# 检查惯性 / Check inertias
for i in range(model.nbody):
inertia = model.body_inertia[i]
if i > 0 and np.any(inertia <= 0):
warnings["inertia"].append(f"Body {i} has invalid inertia")
# 检查关节限位 / Check joint limits
for i in range(model.njnt):
if model.jnt_limited[i]:
range_val = model.jnt_range[i]
if range_val[0] >= range_val[1]:
warnings["joint"].append(f"Joint {i} has invalid range")
return warnings
# 优化模型设置 / Optimize model settings
def optimize_model_settings(xml_string: str) -> str:
"""
优化模型的仿真设置 / Optimize simulation settings
"""
root = ET.fromstring(xml_string)
# 确保有option标签 / Ensure option tag exists
option = root.find('option')
if option is None:
option = ET.SubElement(root, 'option')
# 优化设置 / Optimize settings
option.set('timestep', '0.002') # 2ms timestep
option.set('iterations', '50')
option.set('solver', 'Newton')
option.set('jacobian', 'auto')
# 添加size标签优化内存 / Add size tag to optimize memory
size = root.find('size')
if size is None:
size = ET.SubElement(root, 'size')
size.set('memory', '1M')
return ET.tostring(root, encoding='unicode')
# 测试验证函数 / Test validation function
warnings = validate_model(chain_model)
print("模型验证结果 / Model validation results:")
for category, issues in warnings.items():
if issues:
print(f"\n{category.upper()} 警告 / warnings:")
for issue in issues:
print(f" - {issue}")
else:
print(f"\n{category.upper()}: ✓ 通过 / Passed")
模型验证结果 / Model validation results:
MASS: ✓ 通过 / Passed
INERTIA: ✓ 通过 / Passed
COLLISION: ✓ 通过 / Passed
JOINT: ✓ 通过 / Passed
- 物理合理性检查
- 质量应为正数且不宜过大
- 惯性矩应为正数
- 关节限位的下限必须严格小于上限
- 性能调优
- 配置合理的仿真参数
- 通过
size组件为仿真预分配内存用于仿真时的临时数据结构
3760

被折叠的 条评论
为什么被折叠?



