Skip to content

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 quat0 = 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.successNone 时落盘[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 groupconcatenate_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.hdf5
  • data/demos/rl_lift_cube.hdf5
  • data/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 keyHDF5 内部存放路径含义
state.joint_pos/data/demo_{i}/obs/joint_pos关节位置(rel/abs 见 obs/action 对齐
state.object_position/data/demo_{i}/obs/object_positioncube 在 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)。


  1. HDF5DatasetFileHandler 类定义见 IsaacLab/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py:45,本仓库 schema 直接对齐其 write_episode / load_episode 行为。 ↩︎

  2. DATASET_FORMAT_VERSION = 1 常量定义 + 顶层 attr 写盘见 IsaacLab/source/isaaclab/isaaclab/utils/datasets/hdf5_dataset_file_handler.py:24:75is_legacy_quaternion_format 检测见同文件 :144-150↩︎

  3. 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()↩︎

  4. add_env_args 把 dict JSON 序列化写到 data/ group 的 env_args attr,默认 {"env_name": ..., "type": 2}type=2 是 robomimic gym env 兼容标记,见 hdf5_dataset_file_handler.py:84-86,96-100 与注释引用的 robomimic env_base.py↩︎

  5. num_samples 写盘逻辑见 hdf5_dataset_file_handler.py:231-234——直接取 len(episode.data["actions"]),所以 episode 必须至少有 actions 才会有非零 num_samples,空 episode 在 :216-217is_empty() 跳过。 ↩︎ ↩︎

  6. seed / success 是可选 attr,只有 EpisodeData.seed / EpisodeData.success 不为 None 时才写盘,见 hdf5_dataset_file_handler.py:236-240EpisodeData 定义 IsaacLab/source/isaaclab/isaaclab/utils/datasets/episode_data.py:11-82↩︎

  7. actions dataset 由默认 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↩︎

  8. PreStepFlatPolicyObservationsRecorder 直接落盘 ("obs", env.obs_buf["policy"]),见 recorders.py:40-44obs_buf["policy"] 的形态由 ObservationGroupCfg.concatenate_terms 决定:True → 1-D Tensor(写盘成 /data/demo_i/obs dataset),Falsedict[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)。 ↩︎ ↩︎

  9. 命名格式 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 找。 ↩︎

  10. 老数据(version 0)转新格式工具:HDF5DatasetFileHandler.convert_dataset_to_xyzw(hdf5_dataset_file_handler.py:282-344),只对 key=root_pose 且尾维度为 7 的 dataset 做 wxyz→xyzw 转换。 ↩︎

  11. 自动取 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↩︎

  12. L2-RL 的训练/rollout 必须保 concatenate_terms=True(PPO MLPModel 硬要求 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>)。 ↩︎

  13. 文件名前缀解析逻辑见 l3/convert.py:infer_source——按 scripted_ / rl_ / teleop_ 前缀匹配,无前缀返回 "unknown"↩︎

  14. 当前 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↩︎