File size: 5,589 Bytes
5d08972
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/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())