Motrix Contact Sensor 适配笔记

背景

Sharpa Inhand Rotation 任务的触觉观测依赖 contact sensor 数据(指尖与物体的接触力)。该代码在 MuJoCo 后端下工作正常,但 Motrix 后端的 contact sensor 返回格式不同,读取路径需要同时兼容两种形状。

Contact Sensor 配置

指尖 contact sensor 定义在机器人场景 XML src/unilab/assets/robots/sharpa_wave/right_sharpa_wave.xml 中:

<contact name="contact_right_thumb_elastomer_force" geom1="right_thumb_elastomer" geom2="object" num="1" data="force" reduce="netforce"/>
<!-- 每个指尖一个 sensor,参数相同 -->

参数说明:

  • num="1" — 最多报告 1 个接触点

  • data="force" — 只返回力数据

  • reduce="netforce" — 合成为单个接触点的合力,全局坐标系

两个后端的返回格式差异

MuJoCo

返回 (num_envs, 3) 的力向量,全局坐标系:

[[fx, fy, fz],    # env 0
 [fx, fy, fz],    # env 1
 ...]

Motrix

返回 (num_envs, 1 + num * stride) 的扁平数组,首个元素为实际接触点数

[[count, fx, fy, fz],    # env 0, shape = (4,) 因为 num=1, stride=3
 [count, fx, fy, fz],    # env 1
 ...]

对于 reduce="netforce" + data="force" + num=1

  • shape = (num_envs, 4)

  • [0] = 接触点数(0 或 1)

  • [1:4] = 全局坐标系的合力向量(netforce 模式使用全局坐标)

多接触点的通用格式(Motrix)

num=4, data="force pos normal tangent" 且无 reduce 时:

shape = (num_envs, 1 + 4 * 12) = (num_envs, 49)

[count,
 f1_normal, f1_tangent0, f1_tangent1,   # contact 1 force (contact 坐标系)
 f1_x, f1_y, f1_z,                       # contact 1 position
 f1_nx, f1_ny, f1_nz,                    # contact 1 normal
 f1_tx, f1_ty, f1_tz,                    # contact 1 tangent
 f2_normal, f2_tangent0, f2_tangent1,    # contact 2 ...
 ...
 padding_zeros]                          # 不足 num 个的用 0 填充

注意:无 reduce 时力是 contact 坐标系(法向标量 + 两个切向标量),不是全局 xyz 向量。只有 reduce="netforce" 返回全局坐标系的力向量。

Motrix reduce 模式参考

reduce 模式

坐标系

返回内容

netforce

全局

单个合成的合力向量

maxforce

contact

力最大的那个接触点

mindist

contact

穿透最浅的那个接触点

contact

num 个接触点

其中只有 netforce 返回全局坐标系力向量,其他模式返回 contact 坐标系数据(法向标量 + 两个切向标量)。

单一 norm 分支为何不够

env 通过 src/unilab/envs/manipulation/sharpa_inhand/base.py 中的 _read_tactile_force()_extract_sensor_scalar() 读取触觉力。该 helper 目前对任意 (N, >=3) 数组都用 np.linalg.norm(data[:, :3], axis=1) 折叠。

如果 env 仍把两种后端形状都走这一个分支,MuJoCo 的 (N, 3) 是正确的(对真实力向量取 norm),但 Motrix 的 (N, 4) 会出错:data[:, :3] 取到的是 [count, fx, fy]——把接触点数当成了力分量,并且漏掉了 fz。正确做法不是在 env 里按形状特判,而是把每个后端的布局知识下沉到 backend 方法。

建议的契约:用 backend 方法返回力大小

SimBackend 接口(src/unilab/base/backend/base.py)增加 get_contact_force_magnitude(sensor_name) -> np.ndarray,返回 (num_envs,) 的标量力大小。每个 backend 按自己的数据布局实现:

  • MuJoCosrc/unilab/base/backend/mujoco/backend.py):对 get_sensor_data(name) 返回的 3D 力向量取范数。

  • Motrixsrc/unilab/base/backend/motrix/backend.py):按 reduce 模式解释布局:

    • reduce="netforce":取 [1:4] 后取 norm。

    • 无 reduce 多 contact:对各接触力求和后取 norm。

    • reduce="maxforce":取力最大的接触点。

env 层的 _read_tactile_force() 对 contact sensor 走 get_contact_force_magnitude() 路径,普通标量 sensor 仍走 _extract_sensor_scalar()

这样 env 代码保持 backend 无关,并与高风险区的不变量一致:env 层只能调用 SimBackend 上已声明的方法——不向 env 泄漏后端功能,新增 backend 只需实现该接口方法。get_sensor_data 已是 SimBackend 上声明的方法,因此该提案是在同一边界上扩展,而不是伸手去调用 backend 子类。

相关文件

文件

说明

src/unilab/envs/manipulation/sharpa_inhand/base.py

_extract_sensor_scalar(), _read_tactile_force()

src/unilab/envs/manipulation/sharpa_inhand/rotation.py

reward 计算,virtual torque

src/unilab/assets/robots/sharpa_wave/right_sharpa_wave.xml

contact sensor XML 定义

src/unilab/base/backend/motrix/backend.py

Motrix get_sensor_data()

src/unilab/base/backend/mujoco/backend.py

MuJoCo get_sensor_data()

src/unilab/base/backend/base.py

SimBackend 接口