DEEP 02 · 第一辑 · 运动控制核心 · LEARN NOTES

收进学习手册 · 2026-09-11 · duck.whatled.com

解读 02 · obs.rs:61 维观测是怎么拼出来的

解读对象:duck-control/src/obs.rs(399 行)
需要的前置:《什么鸭公开课》第 04 课(61→14 契约)

模块注释自称"crate 里风险最高的代码"(obs.rs:3-6):一个 61 维浮点数组,每个下标都必须和训练时一致。偏移错了不会报错,只会得到一只"看起来正常但老摔跤"的鸭子,症状像调参问题而不是索引问题。

1. 一张写进注释的布局表

OBS_LEN = 61(obs.rs:51),布局是:陀螺仪 0..3、投影重力 3..6、关节位置(减 home 位)6..20、关节速度 20..34、上一次动作 34..48、命令块 48..61(obs.rs:13-20)。所有 alpha 策略都是 obs[1,61] → actions[1,14],所以只有一种布局——原型里的 51/54/49/85 维旧契约全被砍掉了。

Observation::build(obs.rs:167)用 split_first_chunk_mut::<N>() 把定长数组逐块"切"出来(obs.rs:182-188):每块是 [f32; N],宽度写进类型。填块用泛型函数 fill<const N>(block, values)(obs.rs:134),两边宽度不同就编译不过。注释点破了这个要害:zip 会在短的一侧静默停下,留下一段全零尾巴——而观测里的零意味着"关节在 home 位",策略会认真对待一台不存在的机器人。

2. 15 个关节、14 个槽位:一个映射管两个方向

机器人有 15 个关节,嘴(MOUTH_INDEX = 9,model.rs:31)不在任何策略里。joint_of(slot)(obs.rs:123)一行解决:if slot < MOUTH_INDEX { slot } else { slot + 1 }——嘴之前的槽位直通,之后的整体上移一格。妙处在复用:policy_joints(obs.rs:109)读的时候过这个映射把 15 压成 14,scatter_action(obs.rs:230)写的时候用同一个映射把 14 摊回 15。注释明说:两个方向走同一份代码,"就不可能对不上"。错一位的后果是第 9 号之后的关节全部错位,"既灾难性又完全无声"(obs.rs:228-229)。

还有一道编译期保险:const 块断言 OBS_LEN == duck_ipc_proto::POLICY_OBS_LEN(obs.rs:60-63)。同一个数在策略清单协议里还有一份副本,测试让两份副本漂移时直接编译失败。

3. 命令块:三个"确认过而不是假设"的细节

命令块 48..61 是唯一没有第二真相来源的部分,有三个坑(obs.rs:34-45):

  1. body x、y、yaw 硬编码为零(obs.rs:213-218)。它们在训练环境里未被绑定,全零就是"标准编码",不是占位符。
  2. 身体块内部是 z, roll, pitch 顺序,不是 z, pitch, roll(obs.rs:45)。写反的症状是"让它前倾它侧倾"。
  3. 头部目标是命令,不是输出叠加。原型在有的模式里把头目标再加到策略输出上,两头一起弯;这里只走观测通道。

每条都有测试钉住:unbound_body_axes_are_always_zero、the_body_block_is_z_roll_pitch(obs.rs:313、325)。测试里的值都是刻意可区分的,让"块挪了位置"表现为一个具体下标错了,而不是"机器人走路变差"。

你带走的收获

延伸

什么鸭 WHATDUCK · 复刻学习笔记 ← 全部手册 · 首页