Current code snapshot: smooth/bridge task configs, smooth random terrain, roll/pitch stability rewards, M20 URDF updates, eval profile and stability training documentation.
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. 激活火蚁项目的虚拟环境
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,所以设一下即可(不必去动别人的目录):
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 分钟。
# 标准评估(默认用 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/ 分开,避免两套结果混在一个目录里。
快速自检(不需要仿真):
python3 eval_kit/tests/test_core.py # 24 项:指标、聚合、名字解析、轨迹存取
python3 eval_kit/tests/test_cli.py # 19 项:参数解析、口径覆盖、路径规则、推流开关
4. 用 WebRTC 看画面(可视化)
机器上没有显示器时,用 IsaacLab 的推流模式看评估过程:加 --livestream 即可
(这个参数由 AppLauncher 提供,eval_suite.py 自己没有定义)。
# 局域网直连(推荐):客户端和服务器在同一网段
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
脚本为推流额外做三件事:
- 视口相机默认跟着被评估的机器人。
--view-env N选第 N 个环境(默认 env 0), 上限评估时会打印这个编号对应哪种地形、哪个难度等级。不设的话推流画面停在 默认相机上,看不到机器人。 想自己控制镜头就加--free-camera:改为不跟随任何物体,起始摆在俯视全场的 位置(按实际地形范围算中心与半径),之后在客户端里用鼠标/键盘自由移动 —— Isaac Lab 的ViewportCameraController只在origin_type="asset_root"时每帧 改写相机,设成world就只在开头写一次,不会把你的操作抢回去。同时给了--view-env时以--free-camera为准。 - 开始 rollout 前留出连接时间:
--pause-s(秒),推流时默认 30 s, 期间物理被冻结、画面照常刷新,在终端按回车可提前开始。不推流时默认 0。 已验证:停顿与否跑出来的指标逐位相同(同一 seed)。 注意这一窗口里机器人是悬空静止的:它停在协议规定的起始位姿上,而standard_v1_clean的start_z_offset_m=0.5会把它抬高 0.5 m 再落下, 所以「离地没落下、也不动」正是正常现象;sim.play()之后才开始动。 启动时会打印倒计时,别把它当成卡死。 - 打印客户端连接地址(从 carb 设置里读实际端口,不是写死的)。
想看清机器人走路:smoke/standard_v1 一类协议一个 episode 只有 2~10 s 仿真时间,
扣掉 30 s 等待后留给你看的时间很短。只为观察的话把 episode 拉长(这会改变 protocol
指纹,别拿这些数字做对比):
# 只看画面: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 直接把评估打断。处置:
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"),所以要先让它说话:
--kit_args=--/app/livestream/logLevel=debug
# 另外可以试:--kit_args=--/renderer/multiGpu/maxGpuCount=1
# (IsaacLab 自己的 isaaclab.python.rendering.kit 就是这么设的,多卡机器上更保险)
再不行就用 --record-frames 那条路径看评估过程(功能上等价,只是不能实时移动相机)。
推流不可用时的替代路径:抓帧成视频。 --record-frames DIR 会在 rollout 期间按
间隔把视口画面存成 PNG,跑完打印合成命令:
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 即按仿真速度播放):
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 加一条记录即可,评估代码不动:
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)
比较的是机器人的世界坐标与整张地图的范围,而不是机器人自己那块地砖:
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 是否触发。
两个后果,都影响可比性:
- 这些环境永远没有机会尝试地形,却全部被记成失败 → 成功率和 score 系统性偏低。
- 地形行序是打乱的(哪一行落在最外侧每次都不同)→ 被清零的是哪个难度等级 跨种子都不稳定,所以逐等级成功率和 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 源码里
写得很清楚但很容易忽略:
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)。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,不要看单次数值。