ReTime / real_robot /source /scripts /validate_xtrainer2_lerobot.py
mmmmmcyaaa's picture
Add Xtrainer2 real robot source and Chinese handoff guide
8101696 verified
Raw History Blame Contribute Delete
5.87 kB
#!/usr/bin/env python3
"""Validate a converted xtrainer2 LeRobot dataset.
Two independent checks:
1. Decoded rotations must reproduce the official ``norm_stats.json`` in the
20-D model space, which is what proves the quaternion convention is right.
2. StarVLA's dataloader must load a sample end to end.
"""
from __future__ import annotations
import argparse
import glob
import importlib.util
import json
import sys
from pathlib import Path
import numpy as np
import pandas as pd
import torch
from starVLA.dataloader.gr00t_lerobot.datasets import LeRobotSingleDataset
from starVLA.dataloader.gr00t_lerobot.transform.state_action import RotationTransform
REPO_ROOT = Path(__file__).resolve().parents[1]
OFFICIAL_STATS = "/apdcephfs_nj7/share_305204761/arianliu/data/benchmark/norm_stats.json"
ARM_BLOCKS = ((0, 3, 7), (8, 11, 15))
def load_data_config():
"""The examples tree is not a package, so load the registry by path."""
path = REPO_ROOT / "examples/Xtrainer2/train_files/data_registry/data_config.py"
spec = importlib.util.spec_from_file_location("_xtrainer2_data_config", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module.Xtrainer2BimanualEefDataConfig()
def to_model_space(raw: np.ndarray, scramble_back_to_xyzw: bool = False) -> np.ndarray:
"""Expand stored 16-D poses into the 20-D layout used by the official stats."""
transform = RotationTransform("quaternion", "rotation_6d")
columns = []
for position, rotation, gripper in ARM_BLOCKS:
columns.append(raw[:, position : position + 3])
block = raw[:, rotation : rotation + 4]
if scramble_back_to_xyzw:
block = block[:, [1, 2, 3, 0]]
six = transform.forward(torch.from_numpy(np.ascontiguousarray(block)))
columns.append(six.numpy())
columns.append(raw[:, gripper : gripper + 1])
return np.concatenate(columns, axis=1)
def _stats_error(model_space: np.ndarray, official: dict) -> tuple[float, np.ndarray]:
ref_mean = np.array(official["state"]["mean"])[0]
ref_lo = np.array(official["state"]["p_low"])[0]
ref_hi = np.array(official["state"]["p_high"])[0]
mean = model_space.mean(0)
lo = np.quantile(model_space, 0.01, axis=0)
hi = np.quantile(model_space, 0.99, axis=0)
error = float(
(np.abs(mean - ref_mean).mean() + np.abs(lo - ref_lo).mean() + np.abs(hi - ref_hi).mean())
/ 3
)
return error, mean
def read_states(dataset_root: Path) -> np.ndarray:
frames = [
pd.read_parquet(path)["observation.state"]
for path in sorted(glob.glob(str(dataset_root / "data" / "chunk-*" / "*.parquet")))
]
return np.stack([np.asarray(row, dtype=np.float32) for f in frames for row in f])
def check_convention(dataset_roots: list[Path]) -> tuple[float, float]:
"""Compare the stored convention against the wrong one, pooled over all tasks.
The official stats cover the whole benchmark, so any single task is biased
against them; only the pooled corpus is comparable, and only the gap between
the two conventions is meaningful.
"""
official = json.load(open(OFFICIAL_STATS))
ref_mean = np.array(official["state"]["mean"])[0]
raw = np.concatenate([read_states(root) for root in dataset_roots], axis=0)
stored_error, mean = _stats_error(to_model_space(raw), official)
wrong_error, _ = _stats_error(to_model_space(raw, scramble_back_to_xyzw=True), official)
print(f" pooled frames={len(raw)} over {len(dataset_roots)} datasets")
print(f" ours mean {np.round(mean, 3)}")
print(f" ref mean {np.round(ref_mean, 3)}")
print(f" err as stored (wxyz) : {stored_error:.4f}")
print(f" err if misread (xyzw) : {wrong_error:.4f}")
return stored_error, wrong_error
def check_dataloader(dataset_root: Path) -> None:
config = load_data_config()
dataset = LeRobotSingleDataset(
dataset_path=dataset_root,
modality_configs=config.modality_config(),
embodiment_tag=config.embodiment_tag,
transforms=config.transform(),
data_cfg={"lerobot_version": "v2.0", "action_mode": "abs", "include_state": True},
)
sample = dataset[0]
print(f" episodes={len(dataset.trajectory_lengths)} samples={len(dataset)}")
for key, value in sample.items():
if isinstance(value, (np.ndarray, torch.Tensor)):
print(f" {key}: {tuple(value.shape)} {value.dtype}")
else:
print(f" {key}: {value!r}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--root",
type=Path,
default=Path("/apdcephfs_nj7/share_305204761/arianliu/data/lerobot"),
)
parser.add_argument(
"--min-margin",
type=float,
default=3.0,
help="How many times better the stored convention must fit the official stats",
)
args = parser.parse_args()
dataset_roots = sorted(p for p in args.root.iterdir() if p.is_dir())
failures = []
print("=== rotation convention vs official norm_stats ===")
stored_error, wrong_error = check_convention(dataset_roots)
margin = wrong_error / max(stored_error, 1e-9)
print(f" margin: {margin:.1f}x")
if margin < args.min_margin:
failures.append(f"convention margin {margin:.1f}x < {args.min_margin}x")
for dataset_root in dataset_roots:
print(f"\n=== {dataset_root.name}: StarVLA dataloader ===")
check_dataloader(dataset_root)
if failures:
print("\nFAILED:")
for failure in failures:
print(f" {failure}")
return 1
print("\nAll checks passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())