DEEP 100 · 第十辑 · duck-control 逐函数精读 · LEARN NOTES

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

解读 100 · obs.rs(一):61 维观测逐位

解读对象:duck-control/src/obs.rs(406 行)
需要的前置:解读 02(布局与命令块导览)、解读 06(gyro/gravity 从哪来)

导览见解读 02,本篇逐函数下钻:先看输入端的三个类型,再进 build 看缓冲区怎么被切成六块,最后是三个生命周期小函数。

1. 输入端:Command、BodyPose 与模长

Command(obs.rs:76-83)是"客户端想让机器人做什么"的结构化形式:twist 三元组、head 四元组、body 三个 f64。文档点明它的身份(obs.rs:73-74):按物理单位持有,到扁平命令块的换算只发生在 build 里,"nowhere else"——换算只写一处,就不会有两处各错各的。twist_magnitude(obs.rs:87-89)三行算速度指令模长:

self.twist.iter().map(|v| v * v).sum::<f64>().sqrt()

它只看 twist:测试 twist_magnitude_ignores_head_and_body(obs.rs:391-405)把 head 与 body 全填 1.0,断言模长仍是 0——因为模长是走/站切换的唯一判据(见解读 97 的 will_stand),抬头不能被当成走路。BodyPose(obs.rs:95-99)只有 z/roll/pitch 三个字段,注释坦白 slice 2 尚不可指令(obs.rs:92-93):先占位,等 pose 意图落地,布局图先画完整。

2. build:把 61 个 f32 切成六块

签名(obs.rs:174-181)六个参数:IMU、绝对关节角、关节速度、home 位、上次动作(14 维 f32)、命令。核心手法是把缓冲区按布局表逐块"劈"出来(obs.rs:189-194):

let (gyro, rest) = data.split_first_chunk_mut::<3>().expect(LAYOUT);
let (gravity, rest) = rest.split_first_chunk_mut::<3>().expect(LAYOUT);

split_first_chunk_mut::<N>() 劈出前 N 个可变元素,返回 Option(劈不动即 None);这里各块宽度全是常量、总和恰为 61,expect 不会触发,还有测试 the_layout_widths_sum_to_the_declared_input(obs.rs:288)钉着总和。劈完每块都是定长数组,宽度进了类型,fill(obs.rs:134)就只可能收到同宽的源;它内部一行 values.map(|value| value as f32)(obs.rs:135)完成 f64→f32 收窄。位置块要过一道减法:关节角与 home 位各自压成 14 维,再逐项相减(obs.rs:199-201)——策略训练时看的是相对角。上一动作块连 fill 都不用(obs.rs:204-206):已是 f32、同宽,*previous_action = *last_action 纯拷贝。命令块是唯一"照文档手写"的段落:13 个字面量按布局表顺序排进数组字面量(obs.rs:210-227),注释说这正是目的——"这个块没有第二真相来源,所以它应该能对着文档用眼睛核对"(obs.rs:208-209)。

3. 三个生命周期小函数

Observation 本体是 [f32; 61] 而非 Vec(obs.rs:139-145):它每秒重建 50 次,不该访问堆分配器。围绕它的三个小函数:as_slice(obs.rs:155-157)把内部数组以切片借出,给 policy.rs 的有限性检查用;zeroed(obs.rs:163-167)造全零观测,注释强调它"不是有效机器人状态",只用于预热推理(obs.rs:160-162,见解读 03);From<[f32; 61]>(obs.rs:148-152)把一份已拼好的观测原样灌回来,文档标注用于离线策略回放(obs.rs:147)。三个函数合起来划出边界:观测要么由 build 从真实传感器造,要么由测试/回放显式注入,没有第三条路。

你带走的收获

延伸

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