解读 25 · mdp.py(上):observation 函数库
解读对象:
microduck-replica/upstream/microduck_rl/src/mjlab_microduck/tasks/mdp.py(7188 行,220 个 def + 10 个 class),抽读 observation 相关
需要的前置:解读 23(观测 term 如何被装配);obs term = "函数+参数"组成的观测槽
mdp.py 是全仓库最大的单文件,但每个观测函数的形状高度一致:
def f(env, ..., asset_cfg: SceneEntityCfg = _DEFAULT_ASSET_CFG) -> torch.Tensor,
返回 (num_envs, dim) 张量(119 行定义了默认 _DEFAULT_ASSET_CFG = SceneEntityCfg("robot"))。
挑六组代表读。
最小样本:projected_gravity
3601–3615 行,三行正文:env.scene[asset_cfg.name] 拿实体,返回
asset.data.projected_gravity_b(重力向量投影到机体系)。这就是观测函数的"原子形态":
从仿真状态读一个量,不做任何花活。
每环境恒定的 IMU 错装:缓存四元数
_imu_misalignment_quat(3618–3638 行)首次调用时给每个环境采一条随机轴 + [0, max]
角度的四元数,缓存在 env._imu_misalign_quat——恒定偏置,不是每步噪声,像一台装歪了
的 IMU。projected_gravity_imu_misaligned(3641–3649 行)与 base_ang_vel_imu_misaligned
(3652–3660 行)用 quat_apply 把同一个四元数分别作用到重力向量和角速度上,保证两者
"装在同一块板子上"。配置里传 max_angle_deg=6.0(velocity_env_cfg.py:601/605)。
传感器级读取:raw_accelerometer
3663–3704 行不走 asset.data 的高级封装,而是从 MuJoCo sensordata 里按
sensor_id = 4 的地址切片取加速度计读数,取负、归一化;模长 ≤ 0.1 时回退到
projected_gravity_b(3698–3702 行)——静止/数值异常时退化到纯姿态,防止除零。
编码器穿过齿隙:joint_pos_rel_backlash
齿隙模型在每个舵机关节后串一个无驱动的 passive_<joint>_backlash 铰,而真实编码器装在
输出侧,读的是两者之和。joint_pos_rel_backlash(6184–6201 行)返回
(qpos[servo] + qpos[backlash]) - default[servo];joint_vel_rel_backlash(6204–6218 行)
同理。_backlash_encoder_ids(6145–6181 行)按 (entity, joint selection) 缓存索引和掩码:
模型没有齿隙关节时 mask 为 0,同一函数在普通模型上自动退化为标准读数——用掩码而不是
分支,让一套观测代码服务所有模型变体。
NaN 安全包装与占位
_finite(5210–5211 行)就是 torch.nan_to_num;foot_contact_forces_safe /
foot_height_safe / foot_air_time_safe(5214–5226 行)包住 mjlab 的三个传感器观测,
只给 critic 用:射线/接触传感器可能在机体状态还干净时返回非有限值,单个 NaN 会经
rsl_rl 的 check_nan 杀死整个训练。zero_command_padding(5115–5125 行)返回常数零,
让不追踪头/躯干命令的环境也保持统一的 61 维观测布局,运行时就能用同一个缓冲喂所有策略。
你带走的收获
- 观测函数 = 纯状态到张量的映射,统一签名
(env, …, asset_cfg)让 manager 能无差别装配。 - 随机化分两类:每步噪声(noise 配置)与每环境恒定偏置(缓存四元数),后者才像真实错装。
- 用掩码写"兼容多种模型"的函数,比写两个函数再各配一套 cfg 更省心。
- 观测布局(61 维)是训练与部署的契约,占位零向量也是维护契约的手段。