Skip to content

Commit 3ac621b

Browse files
committed
✨ feat: 新增 CS123 八个 Lab 配套练习代码
1 parent 1737c5c commit 3ac621b

117 files changed

Lines changed: 14248 additions & 21 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Pretrained RL policies — fetched via shared/rl/fetch_policies.sh
2+
# (test_policy.json ≈ 38 MB; pulled from cs123-stanford/lab_5_fall_2025)
3+
shared/rl/policies/*.json
4+
5+
# Generated artifacts
6+
**/tmp/
7+
**/portfolio/
8+
**/tb/
9+
10+
# uv / Python
11+
.venv/
12+
__pycache__/
13+
*.pyc
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# CS123 Lab
2+
3+
这里放 8 个配套 Lab。章节正文里的"演示"是作者带你跑代码;章末的
4+
`Lab` 是你自己写、自己跑、最后放进作品集的任务。
5+
6+
## 8 Lab 总览
7+
8+
|| Lab | 作品集产物 |
9+
|---|---|---|
10+
| Ch1 PID | `lab_1_pid_bode` | `bode_pupper_hfe.png` + HFE 正弦踢腿 GIF |
11+
| Ch2 FK | `lab_2_fk_teleop` | `leg_teleop.gif` + 单腿工作空间图 |
12+
| Ch3 IK | `lab_3_stepping` | `leg_stepping.gif` |
13+
| Ch4 URDF | `lab_4_urdf_surgery` | `pupper_zoo.gif` + PD 调参图 |
14+
| Ch5 步态 | `lab_5_gait_zoo` | `gait_zoo.gif` |
15+
| Ch6 RL | `lab_6_rl_pupper` | `rl_pupper_commands.gif` + PPO checkpoint |
16+
| Ch7 LLM | `lab_7_llm_control` | 5 张任务卡 GIF + 消息轨迹 |
17+
| Ch8 视觉 | `lab_8_ball_chase` | `ball_chase.gif` |
18+
19+
## 命名约定
20+
21+
- **演示**: 章节正文里的完整代码和现象,作者带你看,照跑即可。
22+
- **思考**: 不写代码的概念题,一段话回答。
23+
- **Lab**: 章末延伸任务,你自己写、自己跑、有产出。
24+
- **作品集**: 8 个 Lab 的成果合订本,课程结束后留档。
25+
26+
## 共享资产
27+
28+
`shared/` 是跨 Lab 资源树:
29+
30+
- `shared/models/leg.xml`: 语义关节名 `HAA/HFE/KFE` 的 Pupper 单腿
31+
- `shared/models/meshes/`: Pupper V3 STL 的唯一副本
32+
- `shared/controllers/`: 控制器 dataclass 和数值工具
33+
- `shared/viz/`: GIF 与绘图工具
34+
35+
各 Lab 用相对路径 include/import,不要在自己的 `models/` 下复制 mesh。
36+
37+
## 预置策略(Lab 6/7/8 必做)
38+
39+
Lab 6/7/8 都要加载上游 CS123 的 RTNeural 预置策略 `test_policy.json`(约 38 MB)。
40+
该文件不入库,首次跑这三个 Lab 之前在 `exercises/` 下执行一次:
41+
42+
```bash
43+
bash shared/rl/fetch_policies.sh
44+
```
45+
46+
脚本逻辑:优先用 `tmp/lab_source/lab_5_fall_2025/` 下的本地副本,否则从
47+
`https://raw.githubusercontent.com/cs123-stanford/lab_5_fall_2025/main/test_policy.json`
48+
下载;已存在则跳过(`FORCE=1` 强制重拉)。落盘路径
49+
`shared/rl/policies/test_policy.json` 已被 `.gitignore` 忽略,不会进仓库。
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Lab 1:Pupper 腿踢脚——HFE 正弦追踪
2+
3+
教程 §1.6.2 让一根抽象单摆稳到一个固定角度,§1.6.4 又加了一个常值外扰。两件事在脑子里都讲清楚了,但你手里还没"自己的狗"。
4+
5+
本 Lab 把单摆换成 **Pupper 真腿的 HFE 关节**——`HAA``KFE``<equality>` 锁在 0,整条 thigh + calf 以 HFE 为支点,像人抬腿一样前后摆。再把"稳到固定角度"换成"追一条 $q_d(t) = 0.3\sin(2\pi f t)$ 的正弦线"。扫七个频率,你会亲手画出这条 PD 回路的 Bode 图——不靠 `scipy.signal`,全靠仿真数据。
6+
7+
## 为什么是这件事
8+
9+
| 候选任务 | 教程已覆盖? | 出片程度 | 留下的产物 |
10+
|---|---|---|---|
11+
| Bang-bang 对比 P | §1.2 讲了,没做实验 || 一段抖动视频 |
12+
| 加采样延迟 | §1.1.2 讲了 gap,没做实验 || 延迟 → 欠阻尼曲线 |
13+
| **正弦追踪 + Bode 图** | 完全没做 || **2 段 GIF + 1 张图** |
14+
15+
正弦追踪赢在三件事:视觉最直观(红色目标 / 蓝色实际两条线同屏动)、图表最经典(学生第一次亲手画 Bode)、直接铺垫 Ch3/Ch5(画圆和踏步本质都是"跟踪一条时变轨迹",正弦是最简形式)。
16+
17+
## 你交什么
18+
19+
`make_artifacts.py` 跑完后,`portfolio/` 里会有三件东西:
20+
21+
- `deliverable.png` = `bode_pupper_hfe.png`:7 个频点的幅值(dB)和相位(°),标出 −3 dB 带宽
22+
- `deliverable.gif` = `hfe_sine_0p3hz.gif`:12 秒 / 640×400 / 15 fps,HFE 在 0.3 Hz 下几乎没可见滞后,动作更适合观察
23+
- `notes.md`:50–100 字反思
24+
25+
## 起点
26+
27+
打开 `starter.py`:仿真主循环、MuJoCo 渲染、`imageio` 写 GIF、Bode 拟合(最小二乘 sin/cos)都已经写好。算法核心留了三处 `# TODO:`
28+
29+
| TODO | 对应任务 | 干什么 |
30+
|---|---|---|
31+
| TODO 1 | 任务 B |`mujoco.mj_fullM` 展开 `data.qM`,取 HFE 对角项 = 反射惯量 $I_\text{hfe}$ |
32+
| TODO 2 | 任务 B | 套 $K_d = 2\zeta\sqrt{K_p\,I}$、$\zeta=0.7$ 自动算 $K_d$ |
33+
| TODO 3 | 任务 A/C/D | 写 PD 控制律 $\tau = K_p(q_d - q) + K_d(\dot q_d - \dot q)$ |
34+
35+
填完这三处就能直接跑出 Bode 图和 GIF。本目录里不写 `solution.py`
36+
37+
## 分步任务
38+
39+
1. **任务 A · 常值目标(20 min)**:先跑 `q_des = 0.3`,PD 把 HFE 稳到约 17°,确认控制器活着——对应教程 §1.6.2 的直觉。
40+
2. **任务 B · 反射惯量 + 自动调 $K_d$(20 min)**:徒手 $mL^2/3$ 不准,混合连杆组合公式不干净;用 `mj_fullM` 读,再代二阶系统经验公式(§1.5.4)。
41+
3. **任务 C · 低频正弦 GIF(30 min)**:把目标换成 $q_d(t) = 0.3\sin(2\pi f t)$,用 0.3 Hz 跑 12 秒导出作品集 GIF;0.5 Hz 仍会在任务 D 的 Bode 扫频里测到。
42+
4. **任务 D · 扫频 + Bode(40 min)**:固定 $K_p$,在 $f \in \{0.2, 0.5, 1, 2, 3, 5, 8\}$ Hz 各跑 5 秒,记录稳态幅值和相位差。横轴 $\log f$,左纵轴 $20\log_{10}(A_\text{out}/A_\text{in})$,右纵轴 $\Delta\phi$,标出 −3 dB 带宽。
43+
5. **任务 E · 刚度对比(Stretch,20 min)**:$K_p \in \{K_p^*,\ 2K_p^*,\ 5K_p^*\}$ 三组叠到同一张图——刚度越高带宽越高,但更容易振荡。`starter.py` 里只留了 TODO 注释,不强制完成。
44+
45+
## MuJoCo 场景
46+
47+
`models/scene.xml` 通过 `<include>` 引用 `shared/models/leg.xml`(语义名 `HAA / HFE / KFE`),再用 `<equality>``HAA``KFE` 锁到 0:
48+
49+
```xml
50+
<include file="../../shared/models/leg.xml"/>
51+
<material name="lab1_grid" texture="lab1_grid" texrepeat="1 1"/>
52+
<geom name="floor" type="plane" size="0.7 0.7 0.02" material="lab1_grid"/>
53+
<equality>
54+
<joint joint1="HAA" polycoef="0 0 0 0 0"/>
55+
<joint joint1="KFE" polycoef="0 0 0 0 0"/>
56+
</equality>
57+
```
58+
59+
Lab 2 的升级路径很简单:**删掉这块 `<equality>`** 就解锁成 3 DoF 全腿——同一份 MJCF 一路走到 Lab 4。
60+
61+
## 评分点
62+
63+
- 任务 A 末段满足 $|q - 0.3| < 0.02$ rad(PD 把腿稳住了)
64+
- 任务 D 在 0.2 Hz 增益 $|G| > 0.95$(低频几乎透传)
65+
- 任务 D 在 8 Hz 增益 $|G| < 0.5$(高频明显衰减)
66+
- Bode 图轴标签、频点和 −3 dB 带宽都标出来
67+
- GIF 能一眼看出目标线和 HFE 实测线的差距
68+
69+
## 常见坑
70+
71+
1. $K_p$ 调到很大就发散——大概率不是 PD 错了,是 `timestep` 太粗。本 Lab 用 5 ms,安全档。
72+
2. $K_d$ 不是 MJCF 里的 `damping`。前者是控制律里的阻尼项,后者是物理摩擦——两者不能互替。
73+
3. 力矩饱和:本 Lab `ctrlrange=[-3, 3] N·m`,扫频到高频时会先撞限幅再撞带宽,画图前看一眼 `torque_log` 是否经常顶到 ±3。
74+
75+
## 与教程的衔接
76+
77+
- **复用**:§1.6.2 的脚本骨架、§1.5.4 的 $\omega_n / \zeta$ 公式
78+
- **扩展**
79+
- §1.1.3 引入了"反射惯量"概念——本 Lab 第一次用 `mj_fullM` 把它读出来
80+
- 教程只做 step response(§1.6.2 / §1.6.3);本 Lab 第一次做 frequency response
81+
- 教程用抽象 `pendulum.xml`;本 Lab 第一次把 PD 接到 Pupper 真腿
82+
- **不重复**:§1.6.3 的 $K_p / K_d$ 三组对照、§1.6.4 的稳态误差实验,作为本 Lab 的前置练习,不再重做
83+
84+
## 运行
85+
86+
`exercises/` 是独立的 uv 项目,命令都从 `exercises/` 目录里跑:
87+
88+
```bash
89+
uv run python lab_1_pid_bode/starter_todo.py # 学生起点:会在 TODO 1 处报 NotImplementedError
90+
uv run python lab_1_pid_bode/starter.py # 参考答案:直接跑通,打印 I_hfe / Kp / Kd 与任务 A 误差
91+
uv run python lab_1_pid_bode/make_artifacts.py # 写出 portfolio/ 下三件交付物
92+
uv run python lab_1_pid_bode/tests.py # 三条数值 assert 全过
93+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""运行 Lab 1 starter,并写出 portfolio 交付物。"""
2+
3+
from __future__ import annotations
4+
5+
import shutil
6+
from pathlib import Path
7+
8+
from starter import GIF_FREQUENCY_HZ, run_experiment, save_bode_plot, save_gif
9+
10+
11+
LAB_DIR = Path(__file__).resolve().parent
12+
PORTFOLIO_DIR = LAB_DIR / "portfolio"
13+
GIF_WRITE_KWARGS = {
14+
"fps": 8,
15+
"max_frames": 45,
16+
"width": 560,
17+
}
18+
19+
20+
def _freq_slug(freq: float) -> str:
21+
return f"{freq:g}".replace(".", "p")
22+
23+
24+
def main() -> None:
25+
PORTFOLIO_DIR.mkdir(parents=True, exist_ok=True)
26+
27+
data = run_experiment(render=True)
28+
gains = data["gains"]
29+
inertia = data["inertia"]
30+
bode_points = data["bode_points"]
31+
sine_gif = data["sine_gif"]
32+
33+
bode_named = PORTFOLIO_DIR / "bode_pupper_hfe.png"
34+
gif_named = PORTFOLIO_DIR / f"hfe_sine_{_freq_slug(GIF_FREQUENCY_HZ)}hz.gif"
35+
36+
save_bode_plot(bode_points, bode_named, gains, inertia)
37+
save_gif(sine_gif, gif_named, **GIF_WRITE_KWARGS)
38+
39+
shutil.copyfile(bode_named, PORTFOLIO_DIR / "deliverable.png")
40+
shutil.copyfile(gif_named, PORTFOLIO_DIR / "deliverable.gif")
41+
42+
print(f"Lab 1 交付物已写入 {PORTFOLIO_DIR}/")
43+
44+
45+
if __name__ == "__main__":
46+
main()
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<mujoco model="lab1_hfe_bode">
2+
<option timestep="0.005" gravity="0 0 -9.81"/>
3+
<compiler angle="radian" meshdir="../../shared/models/meshes/"/>
4+
5+
<include file="../../shared/models/leg.xml"/>
6+
7+
<asset>
8+
<texture name="lab1_grid" type="2d" builtin="checker" width="512" height="512"
9+
rgb1=".2 .4 .6" rgb2=".4 .6 .8"/>
10+
<material name="lab1_grid" texture="lab1_grid" texrepeat="1 1" texuniform="true"
11+
reflectance="0"/>
12+
</asset>
13+
14+
<visual>
15+
<global offwidth="640" offheight="400"/>
16+
</visual>
17+
18+
<worldbody>
19+
<geom name="floor" type="plane" size="0.7 0.7 0.02" pos="0 0 0"
20+
material="lab1_grid"/>
21+
</worldbody>
22+
23+
<!-- Lab 1:只让 HFE 实际可动。Lab 2 删除这一块即可解锁。 -->
24+
<equality>
25+
<joint joint1="HAA" polycoef="0 0 0 0 0"/>
26+
<joint joint1="KFE" polycoef="0 0 0 0 0"/>
27+
</equality>
28+
</mujoco>

0 commit comments

Comments
 (0)