Download tools/check_action_layout.py from mlnha/pose6daug-scripts: direct link, hf CLI and curl.
- Browser
- Download file 5.59 kB
-
https://huggingface.co/mlnha/pose6daug-scripts/resolve/main/tools/check_action_layout.py
- Command line
-
hf download hf://mlnha/pose6daug-scripts/tools/check_action_layout.py
-
curl -L -o check_action_layout.py https://huggingface.co/mlnha/pose6daug-scripts/resolve/main/tools/check_action_layout.py
5.59 kB
| #!/usr/bin/env python3 | |
| """Check that a gr00t_views dataset's `action` column really is in the LeRobot order its | |
| meta/modality.json claims. | |
| RoboCasa's simulator emits actions as | |
| [eef_pos(3), eef_rot(3), gripper(1), base(3), torso(1), base_mode(1)] | |
| while a gr00t_views dataset declares | |
| [base_motion(4), control_mode(1), eef_pos(3), eef_rot(3), gripper_close(1)] | |
| Any builder that copies simulator actions through without reordering produces a dataset whose | |
| numbers are right but whose columns are mislabelled. Training then puts arm motion on the | |
| base-motion channel and a constant on the gripper channel, so the policy drives the robot away | |
| and never closes the gripper. The loss still converges -- nothing errors -- which is why this | |
| has to be checked explicitly. | |
| The two layouts are told apart by which dimensions are constant, no reference dataset needed: | |
| LeRobot dims 0-3 all zero, dim 4 constant, dim 11 two-valued (the gripper) | |
| robosuite dims 0-5 continuous, dim 6 two-valued (the gripper), dims 7-10 zero, dim 11 constant | |
| python check_action_layout.py --dataset <gr00t_views dataset> [--episodes 20] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import glob | |
| import json | |
| import os | |
| import sys | |
| import numpy as np | |
| import pyarrow.parquet as pq | |
| LEROBOT_NAMES = ["base_x", "base_y", "base_th", "torso", "ctrl_mode", | |
| "eef_x", "eef_y", "eef_z", "rot_x", "rot_y", "rot_z", "gripper"] | |
| ROBOSUITE_NAMES = ["eef_x", "eef_y", "eef_z", "rot_x", "rot_y", "rot_z", | |
| "gripper", "base_x", "base_y", "base_th", "torso", "base_mode"] | |
| # robosuite index -> LeRobot position; apply as actions[:, FIX] to repair a dataset. | |
| FIX = [7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6] | |
| def load_actions(dataset: str, n_episodes: int) -> np.ndarray: | |
| paths = sorted(glob.glob(os.path.join(dataset, "data", "chunk-*", "*.parquet"))) | |
| if not paths: | |
| sys.exit(f"no parquet under {dataset}/data/chunk-*/") | |
| rows = [] | |
| for p in paths[:n_episodes]: | |
| rows.append(np.stack(pq.read_table(p, columns=["action"]).column("action").to_pylist())) | |
| return np.vstack(rows), len(paths), min(n_episodes, len(paths)) | |
| def classify(a: np.ndarray) -> tuple[str, list[str]]: | |
| """Which layout do the constant/continuous dimensions match?""" | |
| const = [i for i in range(a.shape[1]) if len(np.unique(np.round(a[:, i], 4))) == 1] | |
| zero = [i for i in const if abs(a[:, i]).max() < 1e-6] | |
| twoval = [i for i in range(a.shape[1]) if len(np.unique(np.round(a[:, i], 4))) == 2] | |
| notes = [f" all-zero dims : {zero}", f" constant dims : {const}", | |
| f" two-valued : {twoval} (the gripper is two-valued)"] | |
| lerobot = set([0, 1, 2, 3]).issubset(zero) and 4 in const and 11 in twoval | |
| robosuite = set([7, 8, 9, 10]).issubset(zero) and 11 in const and 6 in twoval | |
| if lerobot and not robosuite: | |
| return "lerobot", notes | |
| if robosuite and not lerobot: | |
| return "robosuite", notes | |
| return "unknown", notes | |
| def main() -> int: | |
| ap = argparse.ArgumentParser(description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter) | |
| ap.add_argument("--dataset", required=True, help="gr00t_views dataset directory") | |
| ap.add_argument("--episodes", type=int, default=20, help="episodes to sample (default 20)") | |
| a = ap.parse_args() | |
| actions, n_total, n_read = load_actions(a.dataset, a.episodes) | |
| print(f"dataset : {a.dataset}") | |
| print(f"sampled : {n_read} of {n_total} episodes, {actions.shape[0]} frames x {actions.shape[1]} dims\n") | |
| if actions.shape[1] != 12: | |
| print(f"FAIL expected 12 action dims, found {actions.shape[1]}") | |
| return 2 | |
| verdict, notes = classify(actions) | |
| names = LEROBOT_NAMES if verdict == "lerobot" else ROBOSUITE_NAMES | |
| for i in range(12): | |
| col = actions[:, i] | |
| uniq = len(np.unique(np.round(col, 4))) | |
| tag = " <- constant" if uniq == 1 else (" <- two-valued" if uniq == 2 else "") | |
| label = names[i] if verdict != "unknown" else "?" | |
| print(f" dim{i:2d} {label:10s} {col.min():8.3f} .. {col.max():7.3f} " | |
| f"|mean| {abs(col).mean():6.3f} uniq {uniq:5d}{tag}") | |
| print() | |
| print("\n".join(notes)) | |
| print() | |
| modality = os.path.join(a.dataset, "meta", "modality.json") | |
| if os.path.exists(modality): | |
| act = json.load(open(modality)).get("action", {}) | |
| print(" modality.json action slices: " | |
| + ", ".join(f"{k} [{v['start']}:{v['end']}]" for k, v in act.items())) | |
| print() | |
| if verdict == "lerobot": | |
| print("PASS action column is in LeRobot order, matching modality.json.") | |
| return 0 | |
| if verdict == "robosuite": | |
| print("FAIL action column is in RoboCasa/robosuite order but modality.json declares") | |
| print(" LeRobot order. Training would read arm motion as base motion and a") | |
| print(" constant as the gripper.") | |
| print(f" Repair: actions = actions[:, {FIX}]") | |
| print(" Fix the builder and rebuild; do not patch the parquet in place unless you") | |
| print(" also confirm nothing else consumed it.") | |
| return 1 | |
| print("UNKNOWN neither layout matched. Inspect the dimensions above by hand:") | |
| print(" LeRobot expects dims 0-3 zero, dim 4 constant, dim 11 two-valued.") | |
| print(" An all-constant or degenerate episode sample can also land here --") | |
| print(" re-run with a larger --episodes before concluding.") | |
| return 3 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |