Files
huoyi_training_level/eval_kit/README.md
T
syfffffjj 7d290ca6ff Add eval_kit evaluation suite with WebRTC visualization
- eval_kit/core: 零框架依赖的指标内核(Trace/metrics/aggregation/terrain_spec/report/naming)
- eval_kit/isaaclab: 适配层(env_cfg 改造、rollout 采集、checkpoint 加载)
- eval_kit/profiles.py / protocols.py: 机器人形态描述与评估口径预设
- eval_kit/tests: 24 项内核自检 + 21 项命令行自检(都不需要仿真)
- eval_suite.py: 编排入口,每个(阶段, 种子)一个子进程;与 eval.py 指标口径一致
- WebRTC 可视化:--livestream 透传、视口相机跟随/自由(--view-env / --free-camera)、
  连接前停顿(--pause-s)、自动放开客户端改画幅、打印连接地址
- 诊断与替代路径:rollout 进度打印、--record-frames 抓帧
2026-10-20 10:40:13 +08:00

561 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# eval_kit —— 可搬运的机器人性能评价套件
**这份套件与 `scripts/reinforcement_learning/rsl_rl/eval.py` 并存,不导入也不修改它。**
两者的指标名完全一致、取点一致、公式一致,所以输出可以直接互相对照。
## 为什么要另写一套
`eval.py` 是一个能跑通的评估脚本,但它把三件事混在一起:读仿真环境、算指标、格式化输出。
其中「读环境」必须知道具体机器人长什么样——它写死了 `.*_wheel` 和 `["base_link", ".*_knee"]`。
于是它只能评估火蚁;而且因为 IsaacLab 的 `find_bodies` 对匹配不到的正则会直接抛
`ValueError`,换一个没有轮子的机器人跑会在进循环之前就崩,不是给出一个错的分数。
本套件把三层切开,让不依赖框架的部分可以单独测试、单独搬运:
```
eval_kit/
├── core/ ★ 零框架依赖(只用 numpy + 标准库),可整块拷到别的项目
│ ├── schema.py │ RobotProfile / EvalProtocol / Trace 三个数据契约
│ ├── metrics.py │ 指标纯函数(唯一算数的地方)
│ ├── aggregation.py │ 上限网格、score、跨种子 mean±std
│ ├── terrain_spec.py │ 地形等级规格(纯数据)
│ ├── report.py │ 格式化与指标筛选
│ └── naming.py │ 容错版的名字/正则解析
├── isaaclab/ × 依赖 isaaclab,唯一接触仿真内部的地方
│ ├── env_setup.py │ 把协议与地形规格翻译成 env_cfg 改动
│ ├── recorder.py │ 跑 rollout 并采集成 Trace
│ └── policy.py │ checkpoint 解析与策略加载
├── profiles.py ★ 机器人形态描述:加机器人 = 加一条记录
├── protocols.py ★ 评估协议预设:改口径 = 改这里
└── tests/
├── test_core.py ★ 合成轨迹测试指标(不需要仿真)
└── test_cli.py ★ 命令行参数与口径覆盖自检(不需要仿真)
```
打 ★ 的是「可套用」的部分:`core/`、`profiles.py`、`protocols.py` 与两个测试文件
可以在没有 Isaac Sim、没有 GPU 的机器上运行。将来要接别的框架(legged_gym、mujoco),
只需要再写一个适配层产出 `Trace`,`core/` 一行都不用改。
## 用法
### 1. 激活火蚁项目的虚拟环境
```bash
cd ~/syf/IsaacLab-2.3.2
source env_isaaclab_py311/bin/activate
cd ~/syf/huoyi_training # 评估脚本在仓库根目录跑
```
验证环境是否正确:`python -c "import isaaclab"` 应当无报错,且 `isaaclab.__file__`
指向 `IsaacLab-2.3.2/source/isaaclab/isaaclab/__init__.py`(可编辑安装)。
注意有两个容易踩空的近邻环境:系统 `python3`(3.10)和 conda 的
`env_rl_training` 都**不能**跑——前者没有 isaaclab/isaacsim,后者有 isaacsim 5.1
但没有 isaaclab。只有 `env_isaaclab_py311` 是完整的。仓库根目录是必须的,
因为 `logs/` 查找用的是相对 cwd 的路径。
### 2. 让 IsaacLab 的日志目录可写
`/tmp/isaaclab` 属于机器上的其它用户,默认会 `PermissionError`。
`configure_logging` 用的是 `tempfile.gettempdir()/isaaclab/logs`,而 `gettempdir()`
尊重 `TMPDIR`,所以设一下即可(不必去动别人的目录):
```bash
export TMPDIR=/home/suyufei/tmp
```
### 3. 跑评估
两条硬性约束,先看:
- **必须传 `--checkpoint`**(绝对路径最省事)。编排进程不启动仿真,因此不加载
hydra,无法按 `--load-run` 用实验名查找权重。
- **每个种子一个子进程**。IsaacLab 在本配置下**一个进程只能创建一个环境**:
第一个环境建好后,`ROUGH_TERRAINS_CFG` 这类模块级单例会被运行时长出自引用,
之后任何 cfg 构造(新实例、开场存的 pristine 深拷贝、重新深拷贝)都会在
`cfg.validate()` 里 RecursionError —— 三种补救方式都实测失败。所以
`eval_suite.py` 的编排进程自己不建环境,而是为每个(阶段, 种子)fork 一个子进程。
代价是每个子进程要重新启动一次 Isaac Sim(约 15 s),所以
`--protocol standard_v1_multiseed`(5 个种子)大约要 5~10 分钟。
```bash
# 标准评估(默认用 solenopsis 描述 + standard_v1 协议)
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--headless
# 上限评估 + 5 个随机种子 + 存轨迹
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--protocol standard_v1_multiseed --save-trace /tmp/traces --headless
# 抗扰评估(每 2.01 s 施加一次水平扰动)
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--eval-type robustness --headless
# 只看某一种地形的上限
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--eval-type upper_limits --terrain-type stairs_up --headless
# 推荐用于机器人对比:放大地砖,消除越界终止项的误判(见下一节)
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--protocol standard_v1_clean --headless
# 或者直接关掉那个终止项
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--no-terrain-bounds --headless
# 冒烟测试(小规模,只为确认流水线通)
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_19999.pt \
--protocol smoke --headless
```
输出默认写到 `<checkpoint 目录>/eval_kit/<eval_type>_<时间戳>.json`——刻意与
`eval.py` 的 `<checkpoint 目录>/eval/` 分开,避免两套结果混在一个目录里。
快速自检(不需要仿真):
```bash
python3 eval_kit/tests/test_core.py # 24 项:指标、聚合、名字解析、轨迹存取
python3 eval_kit/tests/test_cli.py # 19 项:参数解析、口径覆盖、路径规则、推流开关
```
### 4. 用 WebRTC 看画面(可视化)
机器上没有显示器时,用 IsaacLab 的推流模式看评估过程:加 `--livestream` 即可
(这个参数由 `AppLauncher` 提供,`eval_suite.py` 自己没有定义)。
```bash
# 局域网直连(推荐):客户端和服务器在同一网段
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 \
--checkpoint /绝对路径/model_18200.pt \
--protocol smoke --terrain-type stairs_up \
--livestream 2 --device cuda:6
```
- `--livestream 2`:局域网/私有网络 WebRTC,客户端直连 `<本机 IP>:49100`。
- `--livestream 1`:公网 WebRTC,走 NVIDIA NVCF 信令,需要 `PUBLIC_IP` 且能出公网。
- 两个值都会让 `AppLauncher` 自动进入 headless(推流由服务端渲染),
**不要再传 `--headless`**(传了也无害,只是多余)。
客户端用 NVIDIA 的 **Isaac Sim WebRTC Streaming Client**(随 Isaac Sim 一起提供,
Windows/Linux 都有),填入服务器地址即可;端口默认 49100,脚本启动时会把实际
地址打印出来:
```
[eval_kit] WebRTC 推流已启用(--livestream 2,局域网直连)
[eval_kit] 客户端连接地址:10.62.25.137:49100 192.168.77.26:49100
```
脚本为推流额外做三件事:
1. **视口相机默认跟着被评估的机器人**。`--view-env N` 选第 N 个环境(默认 env 0),
上限评估时会打印这个编号对应哪种地形、哪个难度等级。不设的话推流画面停在
默认相机上,看不到机器人。
想自己控制镜头就加 **`--free-camera`**:改为不跟随任何物体,起始摆在俯视全场的
位置(按实际地形范围算中心与半径),之后在客户端里用鼠标/键盘自由移动 ——
Isaac Lab 的 `ViewportCameraController` 只在 `origin_type="asset_root"` 时每帧
改写相机,设成 `world` 就只在开头写一次,不会把你的操作抢回去。同时给了
`--view-env` 时以 `--free-camera` 为准。
2. **开始 rollout 前留出连接时间**:`--pause-s`(秒),推流时默认 30 s,
期间**物理被冻结**、画面照常刷新,在终端按回车可提前开始。不推流时默认 0。
已验证:停顿与否跑出来的指标逐位相同(同一 seed)。
注意这一窗口里机器人是**悬空静止**的:它停在协议规定的起始位姿上,而
`standard_v1_clean` 的 `start_z_offset_m=0.5` 会把它抬高 0.5 m 再落下,
所以「离地没落下、也不动」正是正常现象;`sim.play()` 之后才开始动。
启动时会打印倒计时,别把它当成卡死。
3. **打印客户端连接地址**(从 carb 设置里读实际端口,不是写死的)。
**想看清机器人走路**:`smoke`/`standard_v1` 一类协议一个 episode 只有 2~10 s 仿真时间,
扣掉 30 s 等待后留给你看的时间很短。只为观察的话把 episode 拉长(这会改变 `protocol`
指纹,**别拿这些数字做对比**):
```bash
# 只看画面:60 s 的 episode、60 个机器人,混合地形上一直走
python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 --checkpoint /绝对路径/model_18200.pt \
--protocol standard_v1_clean --eval-type velocity_error \
--episode-length-s 60 --num-envs 60 \
--livestream 2 --view-env 0 --pause-s 30 --device cuda:0
```
**每个(阶段, 种子)是一个独立子进程**,各自开一次推流;子进程退出时画面中断,
下一个阶段会自动重新推流,客户端重连即可。所以 `--protocol standard_v1_multiseed`
这种 8 个种子的协议会看到 8 次「断线—重连」。想连续观察就用单种子的协议
(`standard_v1` / `smoke`),或者按地形单独跑。
**这块机器上跑推流要先挑一张空闲卡**。渲染 + 编码要占显存,而渲染路径(MGPU/
流编码器)会在 GPU 0 上申请资源,GPU 0 正被别人的训练占满时会刷大量
`Out of GPU memory` / `Buffer creation failed for the device: 0`,严重时
`CUDA error: an illegal memory access` 直接把评估打断。处置:
```bash
nvidia-smi --query-gpu=index,memory.used --format=csv # 挑一张空卡
--device cuda:N # 渲染与物理都钉到 N
--rendering_mode performance # 可选:低画质换显存与延迟
--kit_args=--/renderer/multiGpu/maxGpuCount=1 # 可选:限制渲染只用一张卡
```
`--device` 现在同时决定**策略网络**放在哪张卡:以前策略只认 agent 配置里的
`device`(默认 `cuda:0`),在别人占着 0 号卡时会莫名其妙 OOM。
**读数提醒(未解)**:同一 checkpoint、同一协议、同一 seed,**启动方式不同会得到不同的
数值**。实测(`--protocol smoke --eval-type velocity_error --num-envs 64`,同一个
`model_18200.pt`):
| 启动方式 | mean_lin_vel_error | mean_yaw_vel_error | 中途早退 |
| --- | --- | --- | --- |
| `--headless --device cuda:6`(三次独立重跑) | 0.8159 | 0.1480 | 10 |
| `CUDA_VISIBLE_DEVICES=6 --headless --device cuda:0` | 0.8507 | 0.3334 | 17 |
| `--headless --enable_cameras`(渲染开,无窗口) | 0.8507 | 0.3334 | 17 |
| `--livestream 2 --device cuda:6`(推流,暂停 0~30 s 都试过) | 0.8507 | 0.3334 | 17 |
每一组内部都是**逐位可复现**的(同组三次重跑完全一致),跨组则有稳定差异;`--pause-s`
已验证与差异无关(headless 加暂停与不加完全相同;推流下暂停 0 s 与 30 s 也相同)。
两档之间的成因尚未定位(与「是否渲染」和「`--device` 写法/可见卡数」都相关,未能归因到单一因素)。
**在这个问题查清之前:拿数字做对比时固定一种启动方式**,不要把可视化那次的数值与历史
headless 结果混在一个表里报。看画面用推流没有影响,只是它的数字属于另一档。
**画面卡住时先看终端,不要猜**。rollout 期间每 10% 打印一行进度:
```
[eval_kit] 停顿 5.0 s 结束,开始 rollout(画面从这里才动)
[eval_kit] rollout 100/1000 步,用时 7.0 s
[eval_kit] rollout 200/1000 步,用时 13.6 s
...
[eval_kit] 结果已保存:.../upper_limits_2026-10-19_17-16-19.json
```
- **有 `rollout` 打印在涨** → 仿真在正常推进,卡的是画面(渲染/编码侧)。按优先级试:
换到空闲卡(`--device cuda:N`,N 用空闲卡的编号)、
`--rendering_mode performance`、确认客户端没断线、机器人数量少一点(`--num-envs`)。
已知一种具体成因已经默认修掉:客户端改窗口大小/全屏会发 `onCustomResizeMessage`,
服务端默认拒绝并让推流的 `shared swapchain buffer` 进入坏状态,此后画面不再更新
(仿真还在跑)。脚本现在会给推流自动追加
`--/app/livestream/allowResize=true --/app/livestream/allowDynamicResize=true`。
如果日志里仍出现 `onCustomResizeMessage ... must both be positive`,**别缩放客户端窗口**,
并把那段日志发出来。
- **`rollout` 打印不再涨** → 是仿真停了,通常是在客户端 UI 里动了时间轴/仿真控件:
视口下方的时间轴 Play/Pause/Stop 按钮、IsaacLab 面板里的 `Record Animation`
(START 之后默认 10 s 结束会**直接关掉应用**)、`Reset Simulation on Stop`。
看画面时别点它们;真被误触了就 Ctrl-C 重跑。
- **进程已经结束**(终端出现 `结果已保存`,或被 Ctrl-C)→ 客户端只会把最后一帧留在屏幕上,
看起来就是「画面定住」,这既不是卡住也没有在算,重跑即可。
- 一轮跑完的标志只有两个:终端出现 `结果已保存:<路径>.json`,以及
`<checkpoint 目录>/eval_kit/` 下多出一个 JSON。
### 已知问题:WebRTC 画面在客户端接上后停止更新
实测到过这个现象:仿真、渲染、相机都正常(`--record-frames` 抓出的帧显示机器人在动、
相机跟着走),**只有推流给客户端的画面不再刷新**。已排除的原因:物理冻结(终端
`rollout` 进度一直在涨)、暂停窗口(停顿结束后仍不动)、客户端改画幅被拒(已默认打开
`allowResize`/`allowDynamicResize`)、时间轴被暂停(那样进度会停)。
推流器默认把日志静音(`app.livestream.logLevel = "silence"`),所以要先让它说话:
```bash
--kit_args=--/app/livestream/logLevel=debug
# 另外可以试:--kit_args=--/renderer/multiGpu/maxGpuCount=1
# (IsaacLab 自己的 isaaclab.python.rendering.kit 就是这么设的,多卡机器上更保险)
```
再不行就用 `--record-frames` 那条路径看评估过程(功能上等价,只是不能实时移动相机)。
**推流不可用时的替代路径:抓帧成视频。** `--record-frames DIR` 会在 rollout 期间按
间隔把视口画面存成 PNG,跑完打印合成命令:
```bash
CUDA_VISIBLE_DEVICES=7 python scripts/reinforcement_learning/rsl_rl/eval_suite.py \
--task Rough-Deeprobotics-M20-v0 --checkpoint /绝对路径/model_18200.pt \
--protocol standard_v1_clean --eval-type velocity_error \
--episode-length-s 30 --num-envs 16 \
--livestream 2 --pause-s 0 --view-env 0 --device cuda:0 \
--record-frames /tmp/eval_frames --record-every 5
```
抓帧依赖视口渲染,所以**必须开着渲染**:推流(`--livestream 2`)或本地 GUI 都行,
纯 headless(`--headless` 且不推流)没有视口,抓不到帧。跑完合成
(`--record-every 5` 时每帧 0.1 s 仿真时间,10 fps 即按仿真速度播放):
```bash
cd /tmp/eval_frames && ffmpeg -y -framerate 10 -pattern_type glob -i '*.png' \
-pix_fmt yuv420p -c:v libx264 -vf 'pad=ceil(iw/2)*2:ceil(ih/2)*2' /tmp/eval.mp4
```
已实测:600 步抓到 102 张 1280×720 PNG,合出的 mp4 能正常播放,画面里机器人从
起始高度落地并在地形上移动、相机跟着它走 —— 这条路径绕开了推流传输层,
在推流卡住时仍然能看评估过程。
## 三个对象
- **`RobotProfile`**(`profiles.py`):这个机器人谁是足端、谁是支撑体、哪些关节耗电、多重。
评价代码里没有任何 link/joint 名字或质量常数。
- **`EvalProtocol`**(`protocols.py`):一次评估的全部口径——指令、时长、成功距离、
倾角阈值、加速度阈值、能耗约定、地形规格、种子。决定数值的字段会被哈希成
`protocol_fingerprint` 写进输出 JSON。
- **`Trace`**(`core/schema.py`):一次 rollout 的逐帧记录(机身位姿/速度、关节力矩与速度、
足端位置、支撑接触力与时长)。第 0 帧是动作前的起始状态,之后每步一帧。
流水线是单向的:适配器产 `Trace` → 内核算指标 → 报告层格式化。
`Trace` 可以存成 `.npz`,事后加新指标不用重跑仿真。
## 加一个机器人
在 `profiles.py` 加一条记录即可,评估代码不动:
```python
MY_ROBOT = RobotProfile(
name="my_robot",
mass_kg=None, # None = 运行时从资产读真实质量
feet_body_patterns=(".*_foot",),
support_body_patterns=(), # 没有支撑体语义 → 相关指标返回 nan
actuated_joint_patterns=(".*_joint",),
support_force_threshold_n=15.0,
body_length_m=0.5,
)
```
`test_core.py::test_naming_does_not_crash_when_morphology_is_missing` 就是在验证这件事:
用火蚁的描述去解析一个没有轮子的机器人,得到的是「未命中」记录 + 跳过对应指标,而不是崩溃。
## 相对 `eval.py` 的三处有意差异
指标名、取点、公式全部保持一致,只有三处刻意不同:
| 项 | `eval.py` | 本套件 | 原因 |
|---|---|---|---|
| COT 的质量 | `--robot_mass` 默认 25.482 | 默认从 `root_physx_view.get_masses()` 读(`Solenopsis.urdf` 实为 26.581 kg),命令行可覆盖 | 旧默认值是更早的 M20 简化模型名义质量,会让 COT 偏低约 4% |
| 加速度阈值 | `--acc_threshold` 2.0 m/s²(绝对值) | `acc_threshold_g` 重力倍数(默认仍是 2.0/9.81) | 绝对值对不同量级的机器人不公平 |
| 缺失形态的指标 | 返回 0.0 | 返回 `nan`,并在 JSON 里记录 `stability_margin_method="unavailable"` | 0.0 看起来像测出来的值,实际是没测 |
另外新增了两项检查:输出 JSON 里带 `protocol_fingerprint`(防止不同口径的结果被悄悄比较),
以及 `early_resets`(环境在 episode 中途被重置时会警告,因为那会让路程/进度类指标混入两次 rollout)。
## 口径与 `criterion.py` 的对照(备忘)
`criterion.py`(来自 legged_gym 的山猫项目)与 `eval.py` 在 7 处口径上不一致,
移植/对照时要注意。已经核对过的结论:
| 项 | `criterion.py` | `eval.py` / 本套件 |
|---|---|---|
| 能耗积分 | `Σ(τ·q̇)·dt`(有符号) | `Σ\|τ·q̇\|·dt`(绝对值,本套件可切换但默认绝对值) |
| COT 质量 | 写死 27.78822 | 自动读取(26.581 kg) |
| 倾角阈值 | π/6 ≈ 0.524 rad | 0.70 rad |
| 稳定裕度 | ZMP 到足端凸包距离(scipy + shapely) | 机身到足端包围盒距离 |
| 成功判定 | env 维护目标区域 | 沿指令方向前进 ≥ success_distance |
| 时间效率基准 | 走过半块地形的时间 | 走完 success_distance 的时间 |
| 上限 score | 累积成功率加权 | **代数等价,已验算一致** ✓ |
其中「能耗积分」这一条最要紧:有符号口径下负功会抵消正功,同一段运动算出的能耗可以
差一倍以上,所以跨机器人比较时必须在协议里钉死一种。
## ⚠️ 发现的问题:越界终止项让约 20% 的环境从第 0 步就被重置
这是实跑冒烟测试时发现的,**是 `eval.py` 评估设置里既有的问题,不是本套件引入的**。
IsaacLab 的 `terrain_out_of_bounds` 终止项(`isaaclab_tasks/.../mdp/terminations.py`)
比较的是机器人的**世界坐标**与**整张地图**的范围,而不是机器人自己那块地砖:
```python
map_width = n_rows * tile + 2 * border_width
x_out_of_bounds = |root_pos_w[:, 0]| > 0.5 * map_width - distance_buffer
```
在 `eval.py` 的默认评估配置下(地砖 2.0 m、`border_width` 2.0、`distance_buffer` 3.0、
起始位姿 x = −0.9),网格最外侧格子中心在 ±(rows−1)/2 × 2.0 处,机器人起始位置因此
越过阈值。实测数据(8 难度 × 2 地形 × 每格 2 个 = 32 个环境):
- 9 行格子中有 2 个整格越界(origin x = −7.0 → 起始 x = −7.9 > 阈值 7.0)
- **首步就有 6/32 = 19% 的环境被判越界并重置**
- 5×5 网格下同理:最左一整列(20%)从第 0 步起每步都被重置
还有第二个机制:`|y|` 恰好**等于**阈值的那些列(y = ±4.0 对阈值 4.0)处于浮点边界上,
取决于最后一个 bit 是否触发。
两个后果,都影响可比性:
1. 这些环境永远没有机会尝试地形,却全部被记成失败 → 成功率和 score 系统性偏低。
2. 地形行序是**打乱**的(哪一行落在最外侧每次都不同)→ **被清零的是哪个难度等级
跨种子都不稳定**,所以逐等级成功率和 score 连自己跟自己比都不可靠。
### 三种处置(按推荐顺序)
| 方案 | 做法 | 效果 |
|---|---|---|
| 1 | `--terrain-size 4.0` 或 `--protocol standard_v1_clean` | 保留终止项,消除误判。阈值由 `minimum_terrain_size_for_bounds()` 给出:`tile >= 2*(0.9 + 3.0 − 2.0) = 3.8 m` |
| 2 | `--no-terrain-bounds` 或 `--protocol standard_v1_no_bounds` | 直接去掉该终止项。成功判定本就按前进距离算,走出地砖不构成失败 |
| 3 | 保持默认(2.0 m 地砖 + 启用终止项) | 与旧 `eval.py` 数值可比,但带着上述偏差;只建议用于对照 |
### 因果验证
同一配置(5×5 网格、10 个环境、8 步、零动作策略),只切换该终止项:
| `enforce_terrain_bounds` | 各环境中途重置次数 | 结论 |
|---|---|---|
| `True`(默认,与 eval.py 一致) | `[7, 1, 0, 0, 0, 0, 0, 7, 7, 0]` | 4/10 环境受污染 |
| `False` | `[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]` | 无早退 |
本套件的 `early_resets` 检查就是为发现这类问题加的,输出 JSON 里一直带着它。
## ⚠️ 发现的问题二、三:地形参数与生成位置
下面两处同样是实跑时发现的,也都继承自 ``eval.py`` 的评估设置。
### 问题二:机器人在倒金字塔台阶地形里被生成到几何体内部
旧写法把重置高度钉在瓦片中心的 ``env_origin.z`` 上(``pose_range.z = (0, 0)``),
这只对「瓦片中心是最高点」的地形成立。
``stairs_down`` 用的 ``MeshInvertedPyramidStairsTerrainCfg`` **中心是最低点**,
再叠加 ``platform_width`` 被从 3.0 m 压到 0.4 m(``min(0.4, tile*0.2)``),
起始点 x = −0.9 m 就落在平台之外且低于该处地表 —— 机器人被生成在几何体内部、卡住。
结果是 ``stairs_down`` **在全部 8 个难度上都是 0.00**,连只有 4 cm 台阶的 L1 也是,
看上去像「这机器人不会下台阶」,实际是 harness 的问题。
**修复**:``EvalProtocol.start_z_offset_m``(CLI ``--start-z-offset``)。
实测对照(同一 checkpoint、只跑 ``--terrain-type stairs_down``):
| 起始高度 | score | 成功率 |
|---|---|---|
| 0.0(旧 eval.py) | 0.00 / 8 | 0.05 |
| 0.3 m | 3.64 / 8 | 0.47 |
| **0.5 m** | **4.53 / 8** | — |
取 0.5 m 是因为嵌入深度随难度递增(L1 约 0.11 m、L8 约 0.50 m),0.5 刚好覆盖最深一级。
``standard_v1_clean`` 已内置该值;``standard_v1`` 保持 0.0 以维持与旧 eval.py 的可比性。
代价是机器人在平台上方落下会弹一下,``stairs_up`` 略降,但整体是净收益。
### 问题三:粗糙地形的噪声参数被当成起伏幅度
``rough_slope`` 用的 ``HfRandomUniformTerrainCfg`` 有两个容易误解的点,IsaacLab 源码里
写得很清楚但很容易忽略:
1. **``difficulty`` 参数被忽略**(docstring 原文:*"The difficulty parameter is ignored
for this terrain"*)。所以那一列**没有难度梯度**,8 行是同一分布的重复采样 ——
这也正是它能被看出来的原因:它的成功率不单调(0.12/0.00/0.38/0.25/0.62/0.75/0.12/0.00)。
2. **``noise_range`` 是绝对高度范围,不是起伏幅度**。旧 eval.py 设成 ``(0.05, 0.40)``,
而 ``noise_step`` 又被改成 0.01 —— 在 1 cm 水平网格上放 40 cm 高的随机起伏,
等于一堵堵随机尖刺墙,轮半径只有 0.096 m 的机器人无法通过。
**修复**:``noise_step`` 回到上游的 0.02,``noise_range`` 的上限改为 0.10(与训练地形
``noise_range=(0.02, 0.10)`` 同量级)。实测:
| ``noise_range`` | score | 成功率 |
|---|---|---|
| (0.05, 0.40)(旧 eval.py)+ noise_step 0.01 | 0.12 / 8 | 0.05 |
| (0.02, 0.10) + noise_step 0.02 | **6.38 / 8** | 0.86 |
同时给规格加了 ``isaaclab_extra_params``(写与等级无关的固定字段),并把裸 ``setattr``
换成带 ``hasattr`` 校验的 ``_set_sub_terrain_field``:字段名写错现在会直接报错,
不会再静默造一个假属性、让地形保持默认难度(那看起来只是「很难」)。
``rough_slope`` 的 ``unit`` 里也写明了「此地形忽略难度参数,等级排名无意义」,
所以这条限制会随输出 JSON 一起保存。
### 三处修正的合计效果
同一 checkpoint(``model_19999.pt``、seed 0),只切换协议预设:
| 地形 | 旧 eval.py 口径 | ``standard_v1_clean`` |
|---|---|---|
| stairs_up | 2.88 | 3.03 |
| stairs_down | 0.00 | **4.21** |
| smooth_slope | 6.50 | 6.50 |
| rough_slope | 0.12 | **6.03** |
| boxes | 4.28 | 4.60 |
| **总成功率** | 0.434 | **0.659** |
| **平均前进距离** | 3.59 m | **5.26 m** |
| **COT** | 4.50 | **2.45** |
| **score 总和** | 13.78 | **24.38** |
## ⚠️ 尚存的未解异常:所有地形的 L8 都是 0.00
修正后在完整评估里观察到:**五个地形在最高难度 L8 上的成功率全部是 0.00**。
对 ``stairs_up`` / ``boxes`` 这类台阶高度确实随难度递增的地形,L8 失败是合理的;
但 ``rough_slope`` 忽略难度参数,L8 的统计分布与 L1–L7 完全相同(那 7 行是 0.75–1.00),
却精确地得到 0.00 —— 这在统计上不可能是能力问题,指向**最后一行的地形原点或生成高度有问题**
(最可能是 ``terrain_origins`` 最后一行的 z 或布局异常,使机器人被放在地形之外/之下)。
影响:每个地形的 ``score`` 末项 ``acc[-1] * num_levels`` 因 ``acc[-1] = 0`` 而丢失,
score 被低估约 1 分(以 smooth_slope 为例,6.50 → 若 L8 正常约 7.40)。
**这一条尚未定位,也没有修。** 在有明确结论之前,请把矩阵最后一行的数据当作不可用,
score 当作保守下界。
## 验证状态(请如实看待)
**已在真机(`IsaacLab-2.3.2/env_isaaclab_py311` + RTX 4090)上实跑验证**:
- `eval_kit/isaaclab/` 三个模块已端到端跑通,三步冒烟全部 PASS:
- `names` —— 形态解析在真实 body 名上命中:足端 4 个
`['fl_wheel','fr_wheel','hl_wheel','hr_wheel']`、支撑体 5 个
`['base_link','fl_knee',...,'hr_knee']`、驱动关节 8 个,全部与描述文件一致
- `plain` —— 配置 → 采集 → 指标全链路:11 帧 × 4 环境,指标齐出
(`margin=0.1667 m`、`max_tilt=0.051 rad`、`energy=8.0572 J`、`COT=2.1570`)
- `upper` —— 上限网格链路:8 个环境按 2 类型 × 2 等级分格,矩阵与 score 正确折叠
- **整机质量从资产读出为 26.581 kg**,与 `docs/solenopsis_urdf_comparison.md` 的记录一致;
这确认了 `eval.py` 的 `--robot_mass` 默认值 25.482 确实偏低约 4%,本套件改为自动读取
是有必要的。
- 越界终止项的偏差已定位、量化并做过因果验证(见上一节)。
- `eval_kit/core/` 的指标用合成轨迹验证,期望值手算:22 项测试通过。
- `eval_suite.py` 的参数解析与口径覆盖用桩替换 `isaaclab.app` 后测试:11 项通过。
**仍未验证**:
- 没有用**训练到收敛的** checkpoint 跑过,所以指标数值本身没有和 `eval.py` 做过逐项对照。
现有的 `model_0.pt` 是 iteration 0(基本等于随机权重),跑出来的 `success_rate` 恒为 0、
`time_consumption` 恒为 nan,拿这种退化值对照没有信息量——**这时"通过"是弱证据,
"报错"才是强证据**,而报错部分已经全部排除。
- 因此**第一次拿到训练好的 checkpoint 时仍建议做等价性验证**:同一 checkpoint、
同一组参数分别跑 `eval.py` 与 `eval_suite.py --protocol standard_v1`,除「相对 eval.py
的三处有意差异」一节列出的三项外应逐位相同。注意 `eval.py` 目前**无法启动**(见下)。
- `profiles.py` 里的 `lite3` 记录没有在仿真里跑过。
## ⚠️ `eval.py` 目前无法启动
实跑时顺带发现的:`eval.py` 自己也加了一个 `--verbose`,而
`AppLauncher.add_app_launcher_args()` 会无条件添加同名参数,argparse 因此在**构造
parser 阶段**就抛错,脚本连 `--help` 都跑不出来:
```
argparse.ArgumentError: argument --verbose: conflicting option string: --verbose
```
`--device` 和 `--verbose` 是两次踩到的坑,但只有 `device / headless / enable_cameras /
experience / livestream / rendering_mode / xr` 这 7 个会被 AppLauncher 自己的显式检查
拦下并给出可读报错;`--verbose / --info / --cpu` 等其余 7 个由 argparse 拦。
本套件把 14 个名字都记在 `tests/test_cli.py::APPLAUNCHER_ARG_NAMES` 里,并在
`test_parser_compatible_with_app_launcher` 中复刻检查,所以这类冲突不必启动仿真就能发现。
修 `eval.py` 只需把那行 `--verbose` 改名(例如 `--verbose-terrain`)并同步
`args_cli.verbose` 的用法。**我没有改动 `eval.py`**——按你的要求新写一套、不碰原文件。
## 已知限制
- **一个进程只能建一个环境**(IsaacLab 在本配置下的限制,实测确认)。因此
`eval_suite.py` 用「编排进程 + 每(阶段, 种子)一个子进程」的结构,每个子进程
重新启动一次 Isaac Sim。这不是本套件的设计偏好,是绕不过去的约束。
- **地形规格是唯一真正不可移植的部分。** 规格数值(等级 × 类型 × 绝对高度)是纯数据,
但「把规格翻译成地形生成器」的代码是 IsaacLab 专用(`isaaclab/env_setup.py`)。
换框架需要另写这个 builder。
- **稳定裕度口径较粗。** 当前是「机身到足端轴对齐包围盒的距离」,比 ZMP/凸包版本粗糙,
但不会在退化多边形上失败、也不需要 scipy。算法名记录在输出 JSON 里。
- **重复试验只覆盖随机种子。** `aggregate_runs` 给的是种子间的 mean±std,
不含 checkpoint 之间的方差(同一实验不同迭代的权重差异)——那个要靠多取几个 checkpoint 评估。
- **单次 rollout 的噪声很大。** 比较两个策略/机器人时请用 `--seeds` 给多个种子,
看 mean±std,不要看单次数值。