主题
obs/action 对齐规则
三种 L2 来源(scripted / RL / teleop)必须用同一套 obs/action 表示,否则 L3 没法把它们合到一个 LeRobot dataset 里。这份 doc 是约束而不是描述:所有 L2 实现都要遵守。
任务锚点
第一阶段以 Isaac-Lift-Cube-Franka-IK-Abs-v0 为唯一任务,所有维度都按这个任务定。其他任务后续按需扩展。
Action 空间:8 维 IK absolute
| 维度 | 含义 | 单位 / 范围 |
|---|---|---|
[0:3] | 期望 EE 位置 (x, y, z),机器人 root frame | m |
[3:7] | 期望 EE 朝向四元数 (qx, qy, qz, qw) | unit quat(scalar-last) |
[7] | 二值夹爪指令 | +1=OPEN, -1=CLOSE |
来自 Isaac-Lift-Cube-Franka-IK-Abs-v0 的 ActionsCfg: DifferentialInverseKinematicsActionCfg(command_type="pose", use_relative_mode=False) + BinaryJointPositionActionCfg(>0 → 0.04m、≤0 → 0.0m)。
三来源转换义务
| 来源 | 自然产出 | 对齐手段 |
|---|---|---|
l2/scripted | IK abs(SM 直接算 EE 目标 pose) | 无需转换 |
l2/rl | joint_pos(PPO 学 7 个关节增量 + gripper) | rollout 时录制层重写:用 ee_frame sensor 取当前 EE pose(root frame, scalar-last quat),gripper 取 finger joint state 二值化 → 写成 8d IK-abs 等价 action |
l2/teleop | IK rel (keyboard 出 7d [dx, dy, dz, rx, ry, rz, gripper],rotvec 形式) | record loop 里读 ee_frame sensor 当前 EE pose,做 rel→abs 复合:abs_pos = ee_pos_curr + dpos、abs_quat = quat_mul(delta_quat, ee_quat_curr)、gripper 透传 +1/-1。详见 l2/teleop/keyboard_adapter.py:keyboard_delta_to_ik_abs。 |
l2/rl 的"等价 action"语义说明:因为 PPO 的策略空间是关节命令而不是 EE pose, 写盘的 8d action 不是策略真正的"决策变量",而是该步执行后预期 EE 跟踪到的 pose (= 当前帧 ee_frame 读数 + 一步控制延迟)。下游 BC / VLA 训练时把这视为 "demonstrate 出来的 EE 目标 pose"来学,跟 scripted/teleop 的语义一致:模型学的都是"下一帧应该到这个 位姿"。这一替换的副作用是 RL 策略的 explore 信号被丢失(action_rate 会更平滑,因为不再 反映 PPO 的 noise),但不影响 BC 蒸馏的目标分布。
详细推理见 L2 三种轨迹来源 §2 "action 空间对齐"。
Quat 顺序确认(重要)
当前 IsaacLab develop 分支(commit f0234a8)已经统一所有内部 quat 为 scalar-last (x, y, z, w):
body_quat_w,root_quat_w:(x, y, z, w),见IsaacLab/source/isaaclab/isaaclab/assets/articulation/base_articulation_data.py:430等target_quat_w(FrameTransformer / ee_frame):(x, y, z, w),见IsaacLab/source/isaaclab/isaaclab/sensors/frame_transformer/base_frame_transformer_data.py:57UniformPoseCommand输出:(x, y, z, w),见IsaacLab/source/isaaclab/isaaclab/envs/mdp/commands/pose_command.pyDifferentialIKController.set_command接受(x, y, z, w),见IsaacLab/source/isaaclab/isaaclab/controllers/differential_ik.py:112- HDF5 顶层
format_version=1也是 XYZW(见 HDF5 Schema)
Warp 的 wp.transform 同样是 scalar-last (x, y, z, w),所以 SM kernel 拿到的输入和输出 quat 可以直接送给 IK action / 直接喂给 RecorderManager 而无需 reorder。
历史警告:早期文档说 "IsaacLab 内部 (w, x, y, z)" 是基于较老版本,已不适用于当前 commit。LeRobot v2 dataset 端的 quat 顺序约定(如果不一致)由 L3
convert.py在转换时处理,HDF5 这一层保持 IsaacLab 原生格式即可。
Observation:分项存储,L3 按需挑
HDF5 中间产物里 obs 各项分开存(/data/demo_{i}/obs/<key>),不预先 concat。这样:
- 不同来源可能多 / 少几个字段,存着不冲突
- L3 转换器决定哪些字段进 LeRobot 的
observation.state、哪些丢弃 - 加新观测时不破坏旧数据
强制存的字段
| HDF5 key | shape | 含义 |
|---|---|---|
obs/joint_pos | (T, 9) | 7 个臂关节 + 2 个 finger,joint_pos_rel(相对 default) |
obs/joint_vel | (T, 9) | 同上 9 个关节速度 |
obs/object_position | (T, 3) | cube 在 robot root frame 下的 xyz |
obs/target_object_position | (T, 7) | command 目标位姿 (xyz, qx, qy, qz, qw) |
obs/actions | (T, 8) | 上一步的 action(IsaacLab mdp.last_action term,属性名是 actions) |
命名说明:HDF5 key
obs/actions在直觉上是 last_action,但落盘 key 跟LiftEnvCfg.ObservationsCfg.PolicyCfg里的属性名一致(actions = ObsTerm(func=mdp.last_action),见IsaacLab/source/isaaclab_tasks/isaaclab_tasks/manager_based/manipulation/lift/lift_env_cfg.py:110)。L3 转换层把它映射成 LeRobotstate.last_action。
可选字段
| HDF5 key | shape | 何时存 |
|---|---|---|
obs/images/front_cam | (T, H, W, 3) uint8 RGB | --with_camera 开关打开时 |
obs/images/wrist_cam | (T, H, W, 3) uint8 RGB | 双目场景 |
L3 modality.json 切片(state)
observation.state 的 1-D concat 顺序由 convert.py 决定,并写入 modality.json。本仓库默认顺序:
[joint_pos(9), joint_vel(9), object_position(3), target_object_position(7), last_action(8)]
total state_dim = 36modality.json 里的切片:
json
{
"state": {
"joint_pos": { "start": 0, "end": 9 },
"joint_vel": { "start": 9, "end": 18 },
"object_position": { "start": 18, "end": 21 },
"target_object_position": { "start": 21, "end": 28 },
"last_action": { "start": 28, "end": 36 }
},
"action": {
"ee_pose": { "start": 0, "end": 7 },
"gripper": { "start": 7, "end": 8 }
}
}L3
convert.py把 HDF5 里的obs/actions在 LeRobot dataset 里重命名成last_action(语义更清晰)。
图像 observation.images.front_cam 走外挂 mp4,不进 observation.state。
频率
- L2 venv 物理 100 Hz,policy 50 Hz(
sim.dt=0.01、decimation=2)。 - l2/scripted / l2/rl 都按 50 Hz。
- l2/teleop 调到 30 Hz(贴人手响应)——这意味着 LeRobot dataset 的
fps字段在不同来源会不同,要么混合时降采样到 30 Hz、要么按来源分两个 dataset。第一阶段建议三个来源统一 30 Hz 简化合并。
l2/rl 实现细节
策略架构(基于 IsaacLab 现成基础设施):
[训练] [rollout 录制]
Isaac-Lift-Cube-Franka-v0 → Isaac-Lift-Cube-Franka-v0
(joint_pos, concat=True) (joint_pos, concat=True 同训练,加 success term + 自定义录制器)
↓ ↓
PPO learns 9-dim joint_pos policy 推理 → joint_pos action 进 sim
↓ ↓
checkpoint policy.pt IKAbsActionRecorder 覆写 actions:
从 ee_frame sensor 拿当前 EE pose (xyz + xyzw quat)
+ 从 finger joint pos 二值化拿 gripper
拼 8d IK-abs 等价 action 写到 HDF5 actions
PolicySplitObsRecorder 把 1-D concat obs 按
group_obs_term_dim 切回 dict,actions term 替成 8d为什么 rollout 也保 concat=True:
- PPO 网络 (
MLPModelin rsl_rl) 硬要求obs[obs_group]是(E, D)的 1-D Tensor;concatenate_terms=False时obs_buf["policy"]是嵌套 dict,会触发ValueError: The MLP model only supports 1D observations。 - 所以两阶段 obs 形态保持一致,落盘的 dict 形式由自定义
PolicySplitObsRecorder自己切片 得到——这部分跟 PPO 主回路解耦,RecorderManager 在record_pre_step时点单独读obs_buf["policy"]1-D Tensor 切片,不动 PPO 路径。
为什么不用 IK-Abs task 训练 PPO:
Isaac-Lift-Cube-Franka-IK-Abs-v0在IsaacLab/source/isaaclab_tasks/.../config/franka/__init__.py里没注册任何 RL agent cfg——只 joint_pos 那条配了 PPO/SKRL/SB3 cfg。- PPO 直接学 7d EE pose(含 4d quat)的天然问题:quat 必须 unit length,单纯 box action space 学不出来;改 6D rotation 或 axis-angle 要写自定义 ActionTerm + 自定义 reward,工程量 大且收敛性不保证。
- 工程成本最低、教学含金量最高的方案是直接复用 IsaacLab 验证过的 joint_pos PPO 配方, 把"action 空间对齐"挪到 rollout 阶段当数据后处理做。这条路径在
l2/rl/rollout_and_record.py里 ~50 行就能搞定。
8d 等价 action 的具体计算(rollout 录制时由 IKAbsActionRecorder 完成):
python
# ee_frame sensor 在 root frame 下的当前 EE pose (xyzw quat)
ee_pos = ee_sensor.data.target_pos_w[..., 0, :] - env.scene.env_origins # (E, 3)
ee_quat = ee_sensor.data.target_quat_w[..., 0, :] # (E, 4) xyzw
# 从 finger joint 的当前 joint_pos 二值化 gripper:>=0.02 视为 OPEN(+1)、否则 CLOSE(-1)
finger_pos = robot.data.joint_pos[:, finger_joint_indices].mean(dim=-1) # (E,)
gripper = torch.where(finger_pos >= 0.02, 1.0, -1.0).unsqueeze(-1) # (E, 1)
action_8d = torch.cat([ee_pos, ee_quat, gripper], dim=-1) # (E, 8)写盘语义(与 scripted IK abs cfg 对齐):
[0:3]EE 期望位置 = 当前 EE 位置(满足"模型学到的就是 EE 应该到达哪里")[3:7]EE 期望朝向(xyzw)= 当前 EE 朝向[7]gripper:+1当 finger 张开(panda 默认 0.04),-1当 finger 闭合(0.0)- 同步覆写
obs/actions这个 last_action term(也是 8d),下一帧的 obs 自洽
副作用:last_action obs 在策略推理回路里是 9d(joint_pos),但写到 HDF5 的是 8d (IK-abs 等价)。这没问题,因为 obs/actions 在 HDF5 里只用于 BC 训练而不是 PPO 在线 策略;BC 模型读到的 last_action 是 8d,跟它要预测的 action 同分布,自洽。
见
l2/rl/recorders.py的IKAbsActionRecorder/PolicySplitObsRecorder实现。
l2/teleop 实现细节
teleop 来源用的是 keyboard (本仓库教学最小版只支持 keyboard,SpaceMouse / VR 留扩展点)。 关键链路:
[record loop, num_envs=1, 30Hz]
Se3Keyboard.advance() (IsaacLab 自带,7d delta + gripper)
↓
keyboard_delta_to_ik_abs(读 ee_frame 当前 pose):
abs_pos = ee_pos_curr + delta_pos
abs_quat = quat_mul(delta_quat, ee_quat_curr) # delta_quat 来自 rotvec
gripper = +1 OPEN / -1 CLOSE
↓ 8d action
env.step() (Isaac-Lift-Cube-Franka-IK-Abs-v0,与 l2/scripted 同 task)
↓
ActionStateRecorderManagerCfg (与 l2/scripted 同 schema 录制)
↓ 用户连续 N 步在 success 圈内
record_pre_reset + set_success_to_episodes(True) + export_episodes
↓
data/demos/teleop_lift_cube.hdf5为什么 task 用 IK-Abs 而不是 IK-Rel:
- 我们在脚本侧(
keyboard_adapter)就把 keyboard 的 rel 复合成 abs 了,task 直接用 IK-Abs 最自然,HDF5 schema 与l2/scripted完全一致。 - IsaacLab 自带 IK-Rel
DifferentialInverseKinematicsAction也能跑(把 keyboard 7d 直接喂 进去),但那样写盘的actions列就是 IK-rel 7d,跟 scripted/RL 的 8d 不一致——L3convert.py要按 source 分支,违背"三来源对齐到一份 schema"的核心契约。
为什么 success 走"连续 N 步"+"手动 export":
- 直接挂
terminations.success = DoneTerm(...)会在 cube 进 success 圈的第 1 帧 auto-reset, 人手抖动可能造成假 success,且 episode 切得太短。 - 模式抄自 IsaacLab 官方
record_demos.py:240-242:把 success DoneTerm 抽出来设为None, 在主 loop 里手动跑success_term.func(env, ...)计数,够num_success_steps帧才调set_success_to_episodes(True) + export_episodes。
为什么 terminations.time_out = None:
LiftEnvCfg默认episode_length_s=5.0,人手 30Hz × 5s = 150 步,够紧张。teleop 期望 让人慢慢操作,不被强制 reset;只在 success / object_dropping / 用户按 R 时 reset。
为什么不沿用 IsaacLab scripts/tools/record_demos.py:
- 它强 import
isaaclab_mimic(record_demos.py:123),需要装额外包;本仓库不依赖它。 - 它支持 XR / IsaacTeleop / SpaceMouse 等多设备,本最小版用不上,自己写 ~150 行更干净、 注释好嵌入。
见
l2/teleop/record.py/l2/teleop/keyboard_adapter.py/l2/teleop/env_cfg.py实现。