Skip to content

Commit b4bbebb

Browse files
geng-haoranclaude
andcommitted
feat(tasks): complete native BDDL predicates for LIBERO (In/On/Open/Close/TurnOn/TurnOff)
_native_util.py now covers every predicate that appears in the 130 LIBERO task goals: In (region world-box), On (body-center on-top OR, when the target is a region site, LIBERO's SiteObject.under = site-local xy + z-from-surface), Open/ Close (articulated open-range, non-free joints only), TurnOn/TurnOff (FlatStove turnon range qpos>=0.5). Verified on representative tasks (wine-on-cabinet, bowl-on-stove, turn-on-stove) against recorded done. vendor_all_native.py: one pass that remote-reads each task's model_file + initial/final states from HF, writes a native bundle, and checks native BDDL success at the demo-final state == recorded done. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 0ba42ab commit b4bbebb

2 files changed

Lines changed: 159 additions & 8 deletions

File tree

roboverse_pack/tasks/libero/_native_util.py

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,41 @@ def _in_region(model, data, obj: str, region: str) -> bool:
6565
return bool(np.all(np.abs(data.xpos[bid] - sp) < half))
6666

6767

68+
# turn-on/off ranges (FlatStove etc.): turn_on if qpos >= min(range); off if < max(off)
69+
_TURNON_RANGES = {"flat_stove": [0.5, 2.1]}
70+
71+
72+
def _artic_qpos(model, data, base: str):
73+
"""qpos values of the object's articulated (non-free, 1-dof) joints."""
74+
out = []
75+
for j in range(model.njnt):
76+
jn = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, j) or ""
77+
if base in jn and model.jnt_type[j] in (mujoco.mjtJoint.mjJNT_HINGE, mujoco.mjtJoint.mjJNT_SLIDE):
78+
out.append(float(data.qpos[model.jnt_qposadr[j]]))
79+
return out
80+
81+
82+
def _region_base(region: str) -> str:
83+
return re.sub(r"_(\w+_)?(region|level)$", "", region)
84+
85+
6886
def _is_open(model, data, region: str) -> bool:
69-
base = re.sub(r"_(\w+_)?(region|level)$", "", region)
87+
base = _region_base(region)
7088
cls = next((k for k in _OPEN_RANGES if k in base.lower()), None)
7189
rng, direction = _OPEN_RANGES.get(cls, ([-0.14, -0.14], "lt"))
72-
for j in range(model.njnt):
73-
jn = mujoco.mj_id2name(model, mujoco.mjtObj.mjOBJ_JOINT, j) or ""
74-
if base in jn and any(k in jn for k in ("level", "joint", "cabinet", "door")):
75-
q = data.qpos[model.jnt_qposadr[j]]
76-
if (q < max(rng)) if direction == "lt" else (q > min(rng)):
77-
return True
90+
for q in _artic_qpos(model, data, base):
91+
if (q < max(rng)) if direction == "lt" else (q > min(rng)):
92+
return True
7893
return False
7994

8095

96+
def _turn_on(model, data, region: str) -> bool:
97+
base = _region_base(region)
98+
cls = next((k for k in _TURNON_RANGES if k in base.lower()), None)
99+
rng = _TURNON_RANGES.get(cls, [0.5, 2.1])
100+
return any(q >= min(rng) for q in _artic_qpos(model, data, base))
101+
102+
81103
def _contact(model, data, body_a: int, body_b: int) -> bool:
82104
if body_a < 0 or body_b < 0:
83105
return False
@@ -90,7 +112,26 @@ def _contact(model, data, body_a: int, body_b: int) -> bool:
90112

91113

92114
def _on(model, data, a: str, b: str) -> bool:
93-
"""LIBERO On(a,b): a above b, in contact, xy-aligned (<0.03). (b.check_ontop(a))"""
115+
"""LIBERO On(a,b) = b.check_ontop(a).
116+
117+
If ``b`` is a region *site* (e.g. ``cabinet_top_side``) LIBERO uses the
118+
site's box containment (``SiteObject.under``; site contact is always True) —
119+
same as :func:`_in_region`. If ``b`` is a movable *object* it's the
120+
body-center on-top test (above + in contact + xy-aligned <0.03).
121+
"""
122+
sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, b)
123+
if sid >= 0:
124+
# SiteObject.under: obj within site-local xy half-size, z from surface up to +0.10
125+
bid = _bid(model, a) if _bid(model, a) >= 0 else _bid(model, a + "_main")
126+
if bid < 0:
127+
return False
128+
size = model.site_size[sid]
129+
delta = data.site_xmat[sid].reshape(3, 3) @ (data.xpos[bid] - data.site_xpos[sid])
130+
return bool(
131+
size[2] - 0.005 < delta[2] < size[2] + 0.10
132+
and abs(delta[0]) < size[0]
133+
and abs(delta[1]) < size[1]
134+
)
94135
ba = _bid(model, a) if _bid(model, a) >= 0 else _bid(model, a + "_main")
95136
bb = _bid(model, b) if _bid(model, b) >= 0 else _bid(model, b + "_main")
96137
if ba < 0 or bb < 0:
@@ -112,6 +153,10 @@ def check_bddl_success(model, data, goal_terms) -> bool:
112153
ok = _is_open(model, data, args[0])
113154
elif p == "close" and len(args) == 1:
114155
ok = not _is_open(model, data, args[0])
156+
elif p == "turnon" and len(args) == 1:
157+
ok = _turn_on(model, data, args[0])
158+
elif p == "turnoff" and len(args) == 1:
159+
ok = not _turn_on(model, data, args[0])
115160
else:
116161
return False
117162
if not ok:
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Vendor + verify ALL 130 LIBERO tasks as native MetaSim bundles.
2+
3+
For every task: remote-read its model_file + a demo's initial/final states +
4+
recorded done from HF (range requests, no full download); read the BDDL goal
5+
locally; write a native bundle (model.xml + goal.json + init.npz); load the model
6+
and check that the native BDDL success at the demo's final state equals the
7+
recorded done. One pass vendors all bundles AND validates the native predicates.
8+
9+
python -m tools.libero_integration.vendor_all_native --out <report>/metasim_1to1/native130.json
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import argparse
15+
import json
16+
import os
17+
import time
18+
from pathlib import Path
19+
20+
os.environ.setdefault("MUJOCO_GL", "egl")
21+
os.environ.setdefault("HF_HUB_OFFLINE", "0")
22+
23+
import fsspec # noqa: E402
24+
import h5py # noqa: E402
25+
import mujoco # noqa: E402
26+
import numpy as np # noqa: E402
27+
28+
from roboverse_pack.tasks.libero._native_util import check_bddl_success, parse_goal, remap_libero_model # noqa: E402
29+
30+
_HF = "https://huggingface.co/datasets/yifengzhu-hf/LIBERO-datasets/resolve/main"
31+
_BDDL = "/venv/roboverse/lib/python3.11/site-packages/libero/libero/bddl_files"
32+
_BUNDLES = Path(__file__).resolve().parents[2] / "roboverse_pack/tasks/libero/native_bundles"
33+
_ASSETS = str(Path(__file__).resolve().parents[2] / "third_party/LIBERO/libero/libero/assets")
34+
_LOCAL = {
35+
("libero_object", "pick_up_the_butter_and_place_it_in_the_basket"): "datasets/libero/butter_demo.hdf5",
36+
("libero_goal", "open_the_middle_drawer_of_the_cabinet"): "datasets/libero/goal_drawer_demo.hdf5",
37+
}
38+
39+
40+
def _read(suite, stem):
41+
local = _LOCAL.get((suite, stem))
42+
f = h5py.File(local, "r") if local and os.path.exists(local) else h5py.File(fsspec.open(f"{_HF}/{suite}/{stem}_demo.hdf5", "rb").open(), "r")
43+
g = f["data"]["demo_0"]
44+
mf = g.attrs["model_file"]
45+
s0 = g["states"][0]
46+
sN = g["states"][-1]
47+
done = int(g["dones"][()][-1]) if "dones" in g else int(g["rewards"][()][-1] >= 1.0)
48+
f.close()
49+
return mf, s0, sN, done
50+
51+
52+
def main() -> None:
53+
ap = argparse.ArgumentParser()
54+
ap.add_argument("--out", type=Path, required=True)
55+
ap.add_argument("--tasks-json", default="datasets/libero/all_tasks.json")
56+
args = ap.parse_args()
57+
tasks = json.loads(Path(args.tasks_json).read_text())
58+
59+
results, vendored, matched, errs = [], 0, 0, 0
60+
by_suite_match: dict[str, list[bool]] = {}
61+
for i, (suite, pf, stem) in enumerate(tasks):
62+
name = f"{suite.replace('libero_', '')}__{stem}"[:120]
63+
try:
64+
mf, s0, sN, done = _read(suite, stem)
65+
xml = remap_libero_model(mf, _ASSETS)
66+
m = mujoco.MjModel.from_xml_string(xml)
67+
d = mujoco.MjData(m)
68+
nq, nv = m.nq, m.nv
69+
goal = parse_goal(Path(f"{_BDDL}/{pf}/{stem}.bddl").read_text())
70+
# write bundle
71+
out = _BUNDLES / name
72+
out.mkdir(parents=True, exist_ok=True)
73+
(out / "model.xml").write_text(mf)
74+
(out / "goal.json").write_text(json.dumps(goal))
75+
np.savez(out / "init.npz", qpos=s0[1 : 1 + nq], qvel=s0[1 + nq : 1 + nq + nv])
76+
vendored += 1
77+
# verify native success at demo-final == recorded done
78+
d.qpos[:] = sN[1 : 1 + nq]
79+
d.qvel[:] = sN[1 + nq : 1 + nq + nv]
80+
native = check_bddl_success(m, d, goal)
81+
ok = native == bool(done)
82+
matched += ok
83+
by_suite_match.setdefault(suite, []).append(ok)
84+
rec = {"name": name, "suite": suite, "goal": goal, "native_success": native, "recorded_done": done, "match": ok}
85+
print(f" [{i + 1:3d}/130] {name[:60]:60s} goal={[g[0] for g in goal]} native={native} done={done} match={ok}", flush=True)
86+
except Exception as e: # noqa: BLE001
87+
errs += 1
88+
rec = {"name": name, "suite": suite, "error": str(e)[:140]}
89+
print(f" [{i + 1:3d}/130] {name[:60]:60s} ERROR {str(e)[:60]}", flush=True)
90+
results.append(rec)
91+
time.sleep(0.25)
92+
93+
summary = {
94+
"n": len(tasks), "vendored": vendored, "success_match": f"{matched}/{len(tasks)}", "errors": errs,
95+
"per_suite_match": {s: f"{sum(v)}/{len(v)}" for s, v in by_suite_match.items()},
96+
"tasks": results,
97+
}
98+
args.out.parent.mkdir(parents=True, exist_ok=True)
99+
args.out.write_text(json.dumps(summary, indent=2))
100+
print(f"\nNATIVE 130: vendored {vendored}/{len(tasks)} | success-match {summary['success_match']} | errors {errs}")
101+
print("per-suite match:", summary["per_suite_match"])
102+
print(f"wrote {args.out}")
103+
104+
105+
if __name__ == "__main__":
106+
main()

0 commit comments

Comments
 (0)