diff --git a/community_v3_migration/classify.py b/community_v3_migration/classify.py index e8e1249ee..dfd76e730 100644 --- a/community_v3_migration/classify.py +++ b/community_v3_migration/classify.py @@ -25,6 +25,9 @@ def is_so_robot_type(rt: str) -> bool: return bool(rt) and (rt.startswith(SO_PREFIXES) or rt in SO_EXACT) and rt not in NEVER_FIX +SO_JOINTS = ("shoulder_pan", "shoulder_lift", "elbow_flex", "wrist_flex", "wrist_roll", "gripper") + + def is_end_effector(info: dict) -> bool: """True if action/observation.state are task-space end-effector features (e.g. ``ee_x``, ``ee_roll``) rather than joint angles. Such datasets are out of scope for the joint fix.""" @@ -38,6 +41,22 @@ def is_end_effector(info: dict) -> bool: return False +def is_mislabeled_so(info: dict) -> bool: + """True when ``robot_type`` claims SO but the features prove it isn't a standard 6-DOF SO arm: + the joint dim isn't a multiple of 6, or (when names are present) the first 6 joints don't match + the canonical SO set. Such datasets keep their joints untouched and are relabeled 'unknown' + rather than being degrees-converted on a wrong assumption.""" + feats = info.get("features", {}) + dims = [feats[c]["shape"][0] for c in ("action", "observation.state") if feats.get(c, {}).get("shape")] + if any(d % 6 != 0 for d in dims): + return True + for key in ("action", "observation.state"): + names = [str(n).lower() for n in (feats.get(key, {}).get("names") or [])] + if len(names) >= 6 and not all(SO_JOINTS[i] in names[i] for i in range(6)): + return True + return False + + def load_info(root: Path) -> dict: return json.loads((Path(root) / "meta" / "info.json").read_text()) @@ -106,6 +125,10 @@ def classify(root) -> dict: return {**out, "is_so": False, "encoding": "end_effector", "note": "task-space end-effector features"} + if is_so_robot_type(rt) and is_mislabeled_so(info): + return {**out, "is_so": False, "encoding": "non_so", "mislabeled_so": True, + "note": "robot_type claims SO but joint dim/names don't match a 6-DOF SO arm"} + is_so = is_so_robot_type(rt) if not is_so: return {**out, "is_so": False, "encoding": "non_so"} diff --git a/community_v3_migration/fix_dataset.py b/community_v3_migration/fix_dataset.py index 7ad58e942..4bbb0569a 100644 --- a/community_v3_migration/fix_dataset.py +++ b/community_v3_migration/fix_dataset.py @@ -17,6 +17,13 @@ def _stack(col_values) -> np.ndarray: return np.stack([np.asarray(v, dtype=np.float64) for v in col_values]) # (N, D) +def _set_robot_type(root: Path, robot_type: str) -> None: + info_path = root / "meta" / "info.json" + info = json.loads(info_path.read_text()) + info["robot_type"] = robot_type + info_path.write_text(json.dumps(info, indent=4)) + + def _rewrite_parquet(root: Path, encoding: str) -> None: for pq in sorted((root / "data").glob("*/*.parquet")): df = pd.read_parquet(pq) @@ -62,6 +69,14 @@ def fix_dataset_in_place(root) -> dict: """Returns the classification dict augmented with the action taken.""" root = Path(root) cls = classify(root) + if cls.get("mislabeled_so"): + # robot_type claims SO but the joints prove otherwise (wrong dim or non-SO names). + # Relabel to 'unknown' and migrate structurally rather than degrees-converting on a + # false assumption; the joint values are left exactly as recorded. + _set_robot_type(root, "unknown") + return {**cls, "robot_type": "unknown", "converted": False, + "action": f"structural v2.1->v3.0 only; robot_type relabeled '{cls.get('robot_type')}'" + "->'unknown' (joints don't match a 6-DOF SO arm), joint values left unchanged"} enc = cls.get("encoding") if not cls.get("is_so") or enc in ("radians", "unknown", "non_so"): reason = { diff --git a/community_v3_migration/run_migration.py b/community_v3_migration/run_migration.py index 353cb0f93..86ee9bcf4 100644 --- a/community_v3_migration/run_migration.py +++ b/community_v3_migration/run_migration.py @@ -16,7 +16,7 @@ from pathlib import Path from huggingface_hub import HfApi import so_arm_frame -from classify import classify, is_end_effector, is_so_robot_type, load_info +from classify import classify, is_end_effector, load_info from fix_dataset import fix_dataset_in_place SRC_REPO = "HuggingFaceVLA/community_dataset_v3" @@ -168,14 +168,6 @@ def migrate_one(api, dst_repo, sub, work_dir, no_upload) -> dict: if is_end_effector(info): return {"root": sub, "robot_type": info.get("robot_type"), "action": "skipped: end-effector (task-space) dataset, out of scope"} - feats = info.get("features", {}) - dims = [feats[c]["shape"][0] for c in ("action", "observation.state") if feats.get(c, {}).get("shape")] - if is_so_robot_type(info.get("robot_type", "") or "") and any(d % 6 != 0 for d in dims): - # We only migrate datasets usable right away by specifying joints: a clean stack of - # 6-DOF SO arms. Extra appended columns (bbox, EE pose, ...) push the dim off a - # multiple of 6 and mean the degrees mapping doesn't cleanly apply -> out of scope. - return {"root": sub, "robot_type": info.get("robot_type"), - "action": f"skipped: non-standard SO arm (joint dims {dims} not a multiple of 6), out of scope"} result = fix_dataset_in_place(local) # SO-arm value fix (or structural_only)