主题
HDF5 Schema:L2 中间产物
L2 三种来源(scripted / RL / teleop)共享同一个 HDF5 schema,这样 L3 转换器只需要写一套读取逻辑。schema 直接对齐 IsaacLab 官方 HDF5DatasetFileHandler[1],不自己另造一套。
文件结构
demos.hdf5 顶层
├─ attrs:
│ format_version = 1 1 = XYZW quat;0 = legacy WXYZ
│
└─ /data group
├─ attrs:
│ total = sum(num_samples) 所有 episode 的 transition 数累加
│ env_args = "{...json...}" 含 env_name, type
│
├─ /data/demo_0 第 0 条 episode
│ ├─ attrs:
│ │ num_samples = T 该 episode 长度
│ │ seed = ... 可选
│ │ success = True/False 可选
│ │
│ ├─ /data/demo_0/actions (T, action_dim)
│ └─ /data/demo_0/obs group 或 dataset(取决于 concatenate_terms)
│ ├─ /obs/joint_pos (T, ...) 各 obs term 单独存(concat=False)
│ ├─ /obs/object_position (T, 3)
│ └─ ... 其他 recorder 写入的 nested dict
│
├─ /data/demo_1
└─ ...字段说明
顶层 attr:
format_version:1 = XYZW quat、0 = legacy WXYZ,常量 + 落盘逻辑见 IsaacLab 源码[2]。data/group 的total: 所有 episode 的 transition 数累加(robomimic 约定),不是 episode 数[3]。data/group 的env_args: JSON 序列化 dict,默认{"env_name": ..., "type": 2}(type=2是 robomimic gym env 兼容标记)[4]。
Episode group(data/demo_{i})attr:
num_samples: episode 长度,直接取len(episode.data["actions"])[5]。seed/success: 可选,只在EpisodeData.seed/EpisodeData.success非None时落盘[6]。
Episode group dataset:
actions: 默认由PreStepActionsRecorder写入,落盘成(T, action_dim)[7]。obs: 形态由ObservationGroupCfg.concatenate_terms决定——False→ group(obs/<term_name>),True→ 单 dataset[8]。
设计要点
data/demo_{i}而不是episodes/{i}:早期试验用了episodes/,已弃用。新代码必须写data/demo_{i},这样 IsaacLab 自带的所有工具(mimic 扩增、GR00T 转换器)都能直接吃[9]。- 顶层
format_version=1必填:标记 quat 顺序为 XYZW(IsaacLab 标准)。读老数据时is_legacy_quaternion_format()会自动转[10]。 success必填在 episode attrs:L3 过滤 / mixed dataset 都要用。RecorderManager 在record_pre_reset自动从termination_manager["success"]取值写入,不需要手动设置[11]。num_samples必填:避免 L3 读actions.shape[0]才知道长度[5:1]。- obs 是 nested group:
concatenate_terms=False时每个 obs term 各存一份[8:1],方便 L3 按需挑选要哪些字段拼进 LeRobot 的observation.state。不要再把所有 obs concat 成一个 1-D 向量存——那会丢失语义切片信息(L2-RL rollout 因为 PPO 推理路径硬要求 1-D obs,例外地保concatenate_terms=True,但用自定义 RecorderTerm 自己按group_obs_term_dim切回 dict 落盘[12])。
来源标识
文件名约定 data/demos/<source>_<task>.hdf5,例如:
data/demos/scripted_lift_cube.hdf5data/demos/rl_lift_cube.hdf5data/demos/teleop_lift_cube.hdf5
L3 convert.py 接受多个 HDF5 输入文件时,按文件名前缀给每条 episode 打 source 标[13],写到 episodes.jsonl 的自定义字段里(已 verify lerobot loader 透传,详见 L2 三种轨迹来源 §5)。
命名约束
action / obs 内部 key 名要在三种来源之间对齐:
| L3 modality.json key | HDF5 内部存放路径 | 含义 |
|---|---|---|
state.joint_pos | /data/demo_{i}/obs/joint_pos | 关节位置(rel/abs 见 obs/action 对齐) |
state.object_position | /data/demo_{i}/obs/object_position | cube 在 robot root frame 下的 xyz |
action.ee_pose | /data/demo_{i}/actions[..., :7] | IK abs 7-d (x,y,z, qx, qy, qz, qw),scalar-last[14] |
action.gripper | /data/demo_{i}/actions[..., 7:8] | binary gripper |
怎么读
L2 venv 内:
python
import h5py
with h5py.File("data/demos/scripted_lift_cube.hdf5", "r") as f:
data_group = f["data"]
# 注意:f["data"].attrs["total"] 是 transition 数累加(robomimic 约定),不是 episode 数。
# 取 episode 数用 len() 或 keys()。
n = len(data_group)
for i in range(n):
ep = data_group[f"demo_{i}"]
T = ep.attrs["num_samples"]
success = ep.attrs["success"]
actions = ep["actions"][:] # (T, 8)
joint_pos = ep["obs/joint_pos"][:]L3 venv 内同样用 h5py 读,再 concat 成 LeRobot v2 的 observation.state / action(参考 l3/convert.py:read_episode_data)。
HDF5DatasetFileHandler类定义见IsaacLab/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py:45,本仓库 schema 直接对齐其write_episode/load_episode行为。 ↩︎DATASET_FORMAT_VERSION = 1常量定义 + 顶层 attr 写盘见IsaacLab/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py:24和:75。is_legacy_quaternion_format检测见同文件:144-150。 ↩︎data/group 创建时attrs["total"] = 0(hdf5_dataset_file_handler.py:79),每次write_episode累加+= h5_episode_group.attrs["num_samples"](:258)——即total是所有 episode 的 transition 数累加(robomimic 约定),不是 episode 数。要获取 episode 数请用len(f["data"])或HDF5DatasetFileHandler.get_num_episodes()。 ↩︎add_env_args把 dict JSON 序列化写到data/group 的env_argsattr,默认{"env_name": ..., "type": 2}。type=2是 robomimic gym env 兼容标记,见hdf5_dataset_file_handler.py:84-86,96-100与注释引用的 robomimic env_base.py。 ↩︎num_samples写盘逻辑见hdf5_dataset_file_handler.py:231-234——直接取len(episode.data["actions"]),所以 episode 必须至少有 actions 才会有非零num_samples,空 episode 在:216-217被is_empty()跳过。 ↩︎ ↩︎seed/success是可选 attr,只有EpisodeData.seed/EpisodeData.success不为None时才写盘,见hdf5_dataset_file_handler.py:236-240和EpisodeData定义IsaacLab/source/isaaclab/isaaclab/utils/datasets/episode_data.py:11-82。 ↩︎actionsdataset 由默认PreStepActionsRecorder写入,每帧调record_pre_step返回("actions", env.action_manager.action),见IsaacLab/source/isaaclab/isaaclab/envs/mdp/recorders/recorders.py:32-37;RecorderManager.add_to_episodes把每帧 tensor append 到 list,在pre_export()里torch.stack成(T, action_dim),见episode_data.py:88-117,207-217。 ↩︎PreStepFlatPolicyObservationsRecorder直接落盘("obs", env.obs_buf["policy"]),见recorders.py:40-44。obs_buf["policy"]的形态由ObservationGroupCfg.concatenate_terms决定:True→ 1-DTensor(写盘成/data/demo_i/obsdataset),False→dict[str, Tensor](递归写盘成/data/demo_i/obs/<term_name>group)。dict 递归落盘逻辑见hdf5_dataset_file_handler.py:242-247(create_dataset_helper)。本仓库 L2-scripted/teleop 强制concatenate_terms = False(l2/scripted/env_cfg.py:65)。 ↩︎ ↩︎命名格式
demo_{i}写在HDF5DatasetFileHandler.write_episode:hdf5_dataset_file_handler.py:220-223(默认按_demo_count递增,可传demo_id自定义)。open()时_demo_count = len(self._hdf5_data_group)(同文件:61),所以读路径也是按data/demo_{i}key 找。 ↩︎老数据(version 0)转新格式工具:
HDF5DatasetFileHandler.convert_dataset_to_xyzw(hdf5_dataset_file_handler.py:282-344),只对 key=root_pose且尾维度为 7 的 dataset 做 wxyz→xyzw 转换。 ↩︎自动取 success 见
RecorderManager.record_pre_reset:IsaacLab/source/isaaclab/isaaclab/managers/recorder_manager.py:419-425——只要termination_manager有 active term 名为"success",RecorderManager 在每个 reset 自动读取并写到episode.success。本仓库l2/scripted/env_cfg.py:72-75/l2/rl/env_cfg.py:137-140都靠这条路径,不需要手动set_success_to_episodes。 ↩︎L2-RL 的训练/rollout 必须保
concatenate_terms=True(PPOMLPModel硬要求 1-D obs)。rollout 录制层用自定义PolicySplitObsRecorder(l2/rl/recorders.py:105-181)在record_pre_step时按observation_manager.group_obs_term_dim["policy"]把 1-D Tensor 切回 dict,落盘后 HDF5 与 scripted/teleop 形态一致(/data/demo_i/obs/<term_name>)。 ↩︎文件名前缀解析逻辑见
l3/convert.py:infer_source——按scripted_/rl_/teleop_前缀匹配,无前缀返回"unknown"。 ↩︎当前 IsaacLab develop 分支 quat 全部为 scalar-last (qx, qy, qz, qw),与 Warp
wp.transform一致。完整 reference 列表见 obs/action 对齐 §Quat 顺序确认;scripted SM 的输出 schema[xyz, qx, qy, qz, qw, gripper]见l2/scripted/state_machine.py:186-200,RL rollout 的等价 action 见l2/rl/recorders.py:59-87。 ↩︎