【MuJoCo学习笔记】#3 模型程序化创建、验证与优化

环境准备

# 导入必要的库
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_* 方法添加,在显式创建后才能访问并修改
    • 组件类型需要通过相应的枚举定义(mjtJointmjtGeom 等)
  • 模型编译
    • spec.compile():将 spec 转换为优化的仿真模型
    • spec 编译后无法再修改,但可以通过 spec.clone() 保存其模板

高级建模功能

MJCF 还有其他强大的建模组件,这里再介绍两种。

composite

composite 组件可以创建软体和复杂几何结构,包括以下 type 类型:

类型维度主要应用约束类型
particle3D颗粒接触
grid2D薄膜、布料边约束
cloth2D布料、薄膜边+对角约束
rope1D绳索、链条拉伸约束
cable1D电缆、肌腱拉伸+弯曲约束
box3D立方软体体约束
cylinder3D圆柱软体体约束
ellipsoid3D椭球软体体约束

composite 是一种复合体,是由 geom 定义的小单元组成的阵列,阵列参数在 composite 中给出:

  • count:小单元在阵列的三个维度上的数量,对于一维和二维的复合体,不包含的维度配置为 1
  • spacing:相邻小单元之间的间距
  • 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_1q1q2q_2q2polycoef 数组中第 n 个元素为 cnc_ncn,则其数学表达式为

q2=∑n=0N−1cn⋅q1 q_2=\sum^{N-1}_{n=0}c_n\cdot q_1 q2=n=0N1cnq1

模型验证与优化

在渲染模型之前,检查模型的物理合理性并优化性能可以取得更好的效果。下面的代码包含了一些基本的验证和优化方面:

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 组件为仿真预分配内存用于仿真时的临时数据结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值