解读 123 · mdp.py(二):观测函数精选
解读对象:
microduck-replica/upstream/microduck_rl/src/mjlab_microduck/tasks/mdp.py,抽读四个代表性观测/读数函数(全文件 7188 行)
需要的前置:解读 25(观测函数的统一签名与 IMU 错装家族)、解读 23(obs term 怎么装配)
观测函数是策略的眼睛。25 篇看了"原子形态",本篇挑四个更有戏的:给策略时间感的、故意只给 critic 的、替编码器穿过齿隙的、从命令里解相位的。
时间观测:standing_phase
standing_phase(3774-3796 行)正文四行:(episode_length_buf * step_dt) % 2.0 / 2.0,返回 (num_envs, 1) 的相位——每回合从 0 起每 2 秒循环一次的锯齿波。站着不动的任务里关节状态几乎不变,这条时间信号让策略能区分"刚站定"和"站了很久",是给无事件任务的最小时间感。
非对称 actor-critic:ball_pos_in_base 与 ball_vel_in_base
ball_pos_in_base(5827-5841 行)返回球相对机器人根体的位置,经 matrix_from_quat 旋转到机体系;ball_vel_in_base(5844-5853 行)同理给速度。两者的 docstring 都写着 CRITIC-ONLY:非对称 actor-critic(演员-评论家看不同观测的训练法)——部署时机器没有球传感器,所以演员(真正上机的策略网络)必须对球全盲;评论家(只在训练里估计回报的网络)可以偷看,用来更准地预测"这一脚值多少"。同样一维观测,谁看什么是 sim-to-real 的契约的一部分。
编码器穿过齿隙:joint_pos_rel_backlash 深读
25 篇概述过这对函数,这里深一层。_backlash_encoder_ids(6145-6181 行)是公共底座:按 (实体名, 关节选择) 缓存三元组 (main_ids, backlash_ids, mask)——对每个舵机关节找同名 passive_<joint>_backlash 齿隙铰,找不到就 mask=0。joint_pos_rel_backlash(6184-6201 行)返回 qpos[伺服] + qpos[齿隙] * mask − default[伺服]:真实编码器装在减速器输出侧,读的正是两者之和。还有个 biased=True 开关(6186、6197 行)改读 joint_pos_biased,给每环境一条编码器标定偏置——齿隙加数保持原始值,偏置只属于伺服侧。joint_vel_rel_backlash(6204-6218 行)的理由更妙:注释说固件的速度就是从编码器位置差分出来的,所以速度也必须含齿隙项,否则训练与真机的读数口径不一致。
从命令解相位:spin_phase_from_command
spin_phase_from_command(5910-5912 行)一行:(atan2(cmd[:,1], cmd[:,0]) / 2π) % 1.0。spin 任务的命令槽不装速度装相位(编码成 [cos 2πφ, sin 2πφ, 0],见 4966 行的 GroundPickPhaseCommand),这个函数就是反向解码:两个数还原出一个 [0,1) 的周期相位。命令当"乐谱"、观测/奖励当"读谱人",_gp_phase(2982-2984 行)是 ground_pick 任务的同款。
你带走的收获
- 观测不限于物理量:时间相位也是观测,静态任务靠它获得时间感。
- 非对称 actor-critic 的原则:部署时看不到的量,演员必须全盲;评论家偷看只影响训练效率,不影响部署契约。
- "编码器读到什么"是个建模决定:齿隙模型里和=伺服+齿隙,速度也要同样口径, biases 只加在伺服侧。
- 把周期信号编码成 (cos, sin) 对塞进命令槽,一个槽就能传相位,atan2 即解码。