☰
启元机器人 Q1 MuJoCo 传感器核验:49 项定义与 60 维数据
2026/11/3 19:19:50 网站建设 项目流程

我在前面的 Q1 MuJoCo 实验里已经看到过nq=29、nv=28和nu=22。这次读模型 XML 时,又遇到一组更容易混淆的数字:nsensor=49,但nsensordata=60。

乍一看,49 个传感器为什么不是 49 个数?我没有直接把这两个字段写进文章就结束,而是顺着q1_v3_mc.xml的<sensor>区域查了一遍:每个条目占几维,数据在mjData.sensordata的哪个地址,22 个关节位置和速度是否真的对应qpos、qvel。最后又专门测了一个读取时机问题:mj_step()返回后立即读传感器,和再调用一次mj_forward()后读,结果是不是一样。

这篇记录的是固定版 Q1 模型在 MuJoCo 里的定义和仿真输出。我没有实体 Q1,下面的数字也不代表真机传感器精度。

先把实验对象固定下来

这次使用的是启元机器人公开的 Q1 V3.0 q1-urdf 仓库,提交固定为562b764a18613d459140ed9b255fa30434e346ef,模型文件为xml/q1_v3_mc.xml。我还记录了这个 XML 的 SHA-256:

d9442f0de18071b100d927e310bc58b04e7be305fea8f126893b2700e8c4bd3b

仓库提交标题和模型文件名都指向 Q1 V3.0,不过同一提交里的package.xml仍保留Q1-V2.0-URDF-0601这个旧包名。我按提交、模型文件和哈希固定实验对象,没有用包清单里的旧名称推断模型版本。

实验在我的 Apple Silicon Mac 上完成,版本如下:

项目实际版本
macOS15.4.1,ARM64
Python3.10.17
MuJoCo3.16.0
NumPy2.2.6
Matplotlib3.10.9
模型步长0.001 s

运行脚本没有给 22 个执行器发送输入,只把data.ctrl初始化为零,然后推进 1000 步,也就是 1 秒仿真时间。模型仍会在重力和接触动力学下变化,所以后面出现关节角度变化时,不能把它写成某条控制指令的效果。

49 个条目为什么是 60 维

MuJoCo 会把所有传感器的输出连续放进mjData.sensordata,数组长度由mjModel.nsensordata给出。传感器条目数和输出维度本来就是两个概念:一个姿态四元数传感器算一个条目,却需要四个数;一个三轴陀螺仪也是一个条目,却需要三个数。

这个模型的 49 项传感器分成三组:机身与 IMU 相关传感器 5 项、关节位置传感器 22 项、关节速度传感器 22 项。5 项机身传感器占4+3+3+3+3=16维,两个关节组各占 22 维,合计正好是 60。

这里有两个不同的索引:sensor_id表示 XML 里的第几个传感器,sensor_adr才是它在sensordata中的起始位置,sensor_dim表示从这个位置连续读取多少个数。比如姿态传感器只有一个条目,却从地址 0 开始占 4 个槽位;因此下一个三维 gyro 从地址 4 开始。消费数据时要用地址和维度切片,不能把“第几个传感器”当成数组下标。

图 1:根据固定模型的nsensor、nsensordata与传感器分组生成的布局图。

排布图能核对数量,但还不能确认名字、类型和模型参数来自哪一段定义。传感器在 MuJoCo 模型中的序号与sensordata地址也不是一回事:四维四元数已经占去前四个槽位,所以接下来我把 XML 原文和编译后地址表分开核对。

我先回到固定提交里的 XML 原文,而不是从最后生成的 CSV 反推传感器名称。<sensor>区段先写imu_0相关的五项,再写 22 个jointpos,最后写 22 个jointvel;其中 gyro 和 accelerometer 明确写了noise="0.001",关节传感器写的是零噪声。下面这张卡片只展开了原文件中的关键行,省略号代表同组剩余条目。

图 2:根据启元公开 Q1 V3.0 模型 XML 排版,保留原文件行号、字段和值。这是官方源文件的摘录整理图,不是启元发布的宣传配图。

图中的两个合计分别回答了两个问题:传感器列表有 49 行,真正写入sensordata的连续槽位有 60 个。布局也能看出顺序不是我在脚本里重新规定的,而是跟 XML 中<sensor>的出现顺序一致:前 16 个槽位是机身/IMU 输出,之后是 22 个关节位置,最后是 22 个关节速度。

我用脚本把每个条目的名称、类型、地址、维度、挂载对象和 XML 中的noise属性写进sensor_inventory.csv。这一步很有必要,因为只打印nsensor和nsensordata,后面接 SDK 或写消息转换时仍然不知道某一维到底是什么。

图 3:条目顺序按固定版 XML,地址和维度从 MuJoCo 3.16.0 编译后的MjModel读取。它把五项机身/IMU 输出和两个 22 项关节组放在一张表里;这是本次核验整理图,不是启元官方配图。

22 个关节,分别对应哪里

Q1 模型有一个 floating base。它在qpos中占 7 个槽位,在qvel中占 6 个槽位,因此模型整体是nq=29、nv=28:

qpos: floating base 7 维 + 22 个一维 hinge joint = 29 qvel: floating base 6 维 + 22 个一维 hinge joint = 28

这也是为什么不能拿qpos和qvel的同一个下标硬套。第一个关节left_hip_pitch_joint的位置在qpos[7],速度在qvel[6]。传感器端则更规整:位置传感器从sensordata[16]开始连续排到sensordata[37],速度传感器从sensordata[38]排到sensordata[59]。

图 4:从编译后的MjModel读取的 22 个关节传感器与qpos/qvel索引映射。

这张索引表是从编译后的MjModel读出来的,不是根据关节名称手写一张静态表。脚本对每个位置传感器取sensor_objid,再用jnt_qposadr找到对应的qpos下标;速度传感器则用jnt_dofadr找到qvel下标。当前固定模型的 22 个关节都是一维 hinge joint,因此每个传感器都占一个槽位。浮动基座的自由关节则不同:位置由三维平移和四元数表示,占 7 个qpos;速度由线速度和角速度组成,占 6 个qvel。所以关节位置传感器首地址 16,与第一个关节的qpos[7]是两套互不相同的索引。

MuJoCo 的 XML 参考文档对jointpos和jointvel的说明也很直接:这两类传感器分别把关节位置和速度输出到sensordata。我还是做了逐项数值核对,而不是只引用文档。1000 步结束后,22 个位置读数和 22 个速度读数都能与对应状态数组对上,前提是先把当前派生量刷新到同一个状态。脚本同时记录传感器序号、数据地址和关节状态下标;这样模型结构一旦变化,就能看出错在传感器排列还是关节映射,而不只是发现“两个数对不上”。

读数不一致,问题出在调用时机

真正让我停下来查的是下面这个现象:如果每一步只调用mj_step(),然后马上拿data.sensordata去和刚更新的data.qpos、data.qvel比较,结果并不是零。

我让两份MjData使用相同的模型和全零控制输入。一份每步执行:

mujoco.mj_step(model,data)# 立即读取 data.sensordata、data.qpos、data.qvel

另一份每步执行:

mujoco.mj_step(model,data)mujoco.mj_forward(model,data)# 再读取 data.sensordata、data.qpos、data.qvel

在 1000 步、1 秒仿真中,第一种读取方式的 22 个关节最大位置差是0.018420513 rad,最大速度差是1.886024685 rad/s。位置误差峰值出现在left_shoulder_yaw_joint的第 749 步附近,速度误差峰值出现在第 750 步。这个数字不是模型精度,也不是传感器噪声指标,只是“步进后直接读取”与“当前状态数组”之间的调用时机差异。

图 5:两种读取时机下,22 个关节位置和速度传感器的误差曲线。

追加mj_forward()后,22 个位置传感器和 22 个速度传感器的最大绝对误差都变成了0。两份MjData的动力学轨迹也没有被这次刷新改变:整个 1000 步过程中,两个读取路径的最大qpos差为0,最大qvel差为0。

在这个脚本里,mj_forward()用当前qpos、qvel和控制输入重新计算派生量及传感器输出,不推进仿真时间。两条路径的qpos/qvel完全相同,说明差异出在读取阶段的刷新时机。代码如果在mj_step()后立即打包sensordata,需要明确消费者拿到的是哪一时刻的状态;数组长度吻合并不能证明采样时刻一致。

5 项机身与 IMU 输出分别是什么

XML 的前 5 项传感器都挂在imu_0site 或以它为参考:

名称类型维度单位与本次核对的含义
body-orientationframequat4无量纲,imu_0相对世界坐标系的姿态四元数(w、x、y、z)
body-angular-velocitygyro3rad/s,site 局部坐标的角速度
body-linear-posframepos3m,site 在全局坐标中的位置
body-linear-velvelocimeter3m/s,site 局部坐标的线速度
body-linear-accelerationaccelerometer3m/s²,site 局部坐标的线加速度,包含重力

MuJoCo 文档对gyro、velocimeter和accelerometer都强调了 site 局部坐标;framepos和framequat则按空间 frame 返回全局位置或姿态。这个坐标约定不能靠传感器名字猜,所以我又调用mj_objectVelocity()、mj_objectAcceleration()和 site 的位置/旋转矩阵,把它们与sensordata做了交叉核验。

这两种坐标表达在后处理时不能直接混在一列里。位置和姿态描述imu_0相对世界的位置与朝向;gyro、velocimeter、accelerometer 是沿 site 局部轴表达的向量。机器人倾斜后,局部 z 轴相对世界坐标已经转动,若要把不同来源的向量放进同一坐标系,需要依据姿态旋转矩阵转换后再比较。传感器名字里都带有body-,不代表它们使用同一套坐标轴。表格里的单位也按传感器类型记录:四元数无量纲,角速度用rad/s,位置用m,线速度用m/s,加速度用m/s²。如果把这些量发布为 ROS 2 消息,坐标系和时间戳也应一起定义,字段名和单位仍不足以说明向量含义。

结果是:body-linear-pos与data.site_xpos[imu_0]完全一致,角速度、线速度、加速度也与imu_0的局部参考量一致;四元数只留下约1.33e-15的浮点舍入误差。仿真初始时加速度读到约9.95 m/s²的 z 分量,是因为 MuJoCo 的加速度传感器定义包含重力,这个数不能直接拿来当成“机器人受到了某个外部冲击”。

图 6:1 秒无控制输入仿真中,执行mj_forward()后采样的 5 个机身/IMU 传感器输出。

曲线是 1 秒无控制输入仿真中,执行mj_forward()后的sensordata采样。值得单独解释的是,约0.7 s时加速度曲线出现明显尖峰:CSV 中 site 局部 x 分量为59.94 m/s²,z 分量为-96.69 m/s²;同一时刻imu_0高度约0.268 m,姿态四元数的 w/y 分量约为0.489/-0.872。这时模型已明显倾斜并发生位移,和前面的全零控制、受重力运行是一组条件。我把这个尖峰视为当前模型动态过程中的瞬态,不能从这段仿真里单独归因到传感器、接触参数或某一种硬件效应。

因此这张曲线能用于核对传感器地址、坐标系和状态刷新时机,却不适合作为静止 IMU 的基线。XML 里 gyro 和 accelerometer 的noise="0.001"是模型配置字段;这次运行没有做噪声统计,因此不把它解释成真实 IMU 的噪声、偏置或安装误差测量值。

把这次核验放回开发流程

我把复现实验整理成了一个独立的 AtomGit 项目:q1-mujoco-sensor-audit。运行后会得到:

  • sensor_inventory.csv:49 个传感器的名称、类型、地址、维度和挂载对象;
  • joint_sensor_comparison.csv:采样时刻、传感器读数、qpos/qvel、两种读取时机下的误差;
  • body_sensor_timeseries.csv:5 个机身/IMU 传感器的时间序列;
  • summary.json:模型提交、XML 哈希、版本、维度、峰值误差和有限值检查;
  • figures/:脚本生成的四张实验图表。文章另有两张依据固定版 XML 与编译模型整理的说明图。

在自己的模型目录和项目目录中可以这样运行:

python3.10-mvenv .venvsource.venv/bin/activate python-mpipinstall-rrequirements-lock.txt python scripts/audit_q1_sensors.py\--model~/q1-mujoco/official-q1-urdf/xml/q1_v3_mc.xml\--steps1000\--sample-every10\--output-dir results

脚本会先检查模型提交和 XML 哈希,再检查49/60维度;模型换成其他版本、传感器数量变化或关节不再是当前的 22 个一维 hinge joint 时,会停止并提示复核。

我还会把模型提交和 XML 哈希与 CSV、summary.json放在同一份实验结果中。即使新模型仍然有 49 项传感器,XML 的排列或某个传感器维度改变,也可能让后续地址发生偏移;单看总数无法发现这种变化。版本升级后应重新导出映射表,再更新消费端索引,不能把这一版的sensordata[16:60]规则当成跨版本约定。

这次实验的结论

固定提交的 Q1 V3.0 XML 定义了 49 项传感器,编译后占用 60 个连续数据槽位;用mj_forward()刷新后,22 个关节位置与 22 个关节速度输出能分别对上相应的qpos/qvel。直接在mj_step()后读取出现的差异,则来自本次代码的状态与传感器刷新顺序。

后续把数据送进 ROS 2 消息或日志时,我会在采样函数里明确派生量的刷新时机。真机 IMU 的噪声、偏置和安装误差要用设备数据另行核对。

参考资料

  • 启元机器人 Q1 V3.0 q1-urdf 仓库(固定提交)。
  • MuJoCo XML Reference:sensor。
  • MuJoCo Python bindings。
  • MuJoCo Computation。

Q1 模型、SDK 与开发资料可从启元机器人开发者社区获取。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询