Skip to content

Commit 0e888c9

Browse files
committed
Merge: libero native tasks + vendored HF assets (all 130, library-deletable)
Land the roboverse_pack.tasks.libero_native pack: run all 130 base LIBERO tasks (scene + BDDL checker + OSC_POSE + render) with zero libero/robosuite import, plus the migration that vendors each task as portable MJCF + 193 deduped shared assets to HF RoboVerseOrg/roboverse_data (library-deletable). Additive: 20 new files + the integration doc page. Verified: checker 130/130 bitwise, vendored model fields 130/130 exact; tests 5/5; package imports without libero/robosuite.
2 parents 1930844 + 0173d99 commit 0e888c9

21 files changed

Lines changed: 5239 additions & 0 deletions

docs/source/dataset_benchmark/integrations/libero.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ upstream. On top of that we verify a **pure-MetaSim native reproduction** (load
1717
the scene into MetaSim's own MuJoCo handler) and a **bitwise OSC_POSE controller
1818
port** so LIBERO EE-delta policies can run *inside* MetaSim.
1919

20+
Beyond passthrough (which still imports the upstream library), the
21+
`roboverse_pack.tasks.libero_native` pack runs **all 130 base LIBERO tasks with
22+
zero `libero` / `robosuite` import** — the scene (a portable MJCF + deduplicated
23+
shared assets vendored to HuggingFace `RoboVerseOrg/roboverse_data`), the BDDL
24+
success checker, the OSC_POSE controller and the renderer all run on MuJoCo +
25+
NumPy alone. A fresh machine with the upstream packages **deleted** resolves each
26+
task's MJCF and meshes from HF on demand and runs it; verified bitwise on all 130
27+
(see [Native MetaSim tasks](#native-metasim-tasks-run-and-delete-libero)).
28+
2029
## Status
2130

2231
| Capability | Result | Where |
@@ -28,6 +37,8 @@ port** so LIBERO EE-delta policies can run *inside* MetaSim.
2837
| MetaSim MuJoCo migration | 6/6 dims: state-set Δ=0, **engine Δ=0** | `scripts/migrate_liberoplus_metasim.py` |
2938
| OSC_POSE port | **bitwise** — per-state joint-torque Δ = 5.55e-15 N·m | `scripts/osc/` |
3039
| BC policy (closed-loop) | clean 100 % / light 0 % / camera 50 % / noise 75 %; passthrough==native Δ=0 | `scripts/policy/` |
40+
| **Native tasks (no libero/robosuite)** | **130 / 130** base tasks: ported BDDL checker bitwise (0 mismatch vs `env._check_success`), render bitwise (state-replay) | `roboverse_pack/tasks/libero_native/` |
41+
| **Vendored assets (library-deletable)** | 130 portable MJCF + **193 deduped assets / 265 MB** on HF; fresh-machine model fields **130/130** exact (max\|Δ\|=0), checker **130/130** | `RoboVerseOrg/roboverse_data` `libero/` |
3142

3243
MetaSim core changes: **0** (the passthrough reuses the unmodified upstream env;
3344
the MetaSim handler loads the scene MJCF verbatim).
@@ -67,6 +78,65 @@ env = make_liberoplus_env("libero_object", 7, seed=0)
6778
obs, reward, done, info = env.step([0.0] * 7) # native legacy-gym 4-tuple (kept for fidelity)
6879
```
6980

81+
## Native MetaSim tasks — run (and delete) LIBERO
82+
83+
The passthrough is bitwise but still *imports* `libero` / `robosuite`. The
84+
`roboverse_pack.tasks.libero_native` pack removes that dependency: a LIBERO task's
85+
**scene, success checker, controller and renderer** all run on `mujoco` + `numpy`.
86+
87+
```python
88+
from roboverse_pack.tasks.libero_native.native_task import LiberoNativeTask
89+
90+
# Resolves the portable MJCF + every mesh/texture from a local roboverse_data
91+
# clone, or downloads them from HF on demand. No `libero` / `robosuite` import.
92+
task = LiberoNativeTask.from_vendored("libero_object",
93+
"pick_up_the_alphabet_soup_and_place_it_in_the_basket")
94+
task.reset(state) # set a flat [time, qpos, qvel] state
95+
task.success() # ported BDDL goal checker (bitwise vs env._check_success)
96+
task.render() # agentview RGB (geom-group matched to robosuite)
97+
task.step(action, grip) # one OSC_POSE policy step (inlined controller)
98+
```
99+
100+
What was ported, and how it stays 1:1:
101+
102+
- **BDDL success checker** (`checker.py`) — every goal predicate reduced to a
103+
MuJoCo-state test: `In`=oriented-box containment, `On`(object)=`check_ontop`
104+
with exact `contact_geoms`, `On`(region-site)=`SiteObject.under` + parent
105+
contact, `Open/Close/TurnOn/TurnOff`=articulated-joint thresholds (any/all).
106+
Thresholds are recovered at export by probing the live object's own method, so
107+
they are exactly faithful. **0 mismatch vs `env._check_success` on all 130.**
108+
- **OSC_POSE** (`osc.py`) — robosuite's operational-space control math inlined as
109+
pure NumPy; per-step joint torque Δ = 1.8e-14 vs robosuite.
110+
- **Scene** — each task ships as a portable MJCF whose `file=` paths are vendored
111+
asset relpaths; the loader re-applies robosuite's runtime fixture placement
112+
(`body_pos/quat`, captured with `seed=0`) so the compiled model is
113+
**field-for-field identical** (`max|Δ| = 0`).
114+
- **Render**`mujoco.Renderer`'s geom-group mask is matched to robosuite's
115+
offscreen `vopt` so a state-replay render is pixel-identical.
116+
117+
### Vendored assets on HuggingFace (library-deletable)
118+
119+
`scripts/native/migrate_libero_assets.py` exports all 130 tasks to a deduplicated
120+
tree (193 unique files / 265 MB — the Franka/arena meshes are shared by every
121+
task) published to the
122+
[`RoboVerseOrg/roboverse_data`](https://huggingface.co/datasets/RoboVerseOrg/roboverse_data/tree/main/libero)
123+
dataset under `libero/`:
124+
125+
```
126+
libero/
127+
manifest.json
128+
assets/robosuite/<...> # shared Franka / gripper / arena meshes
129+
assets/libero/<...> # per-scene objects, textures, meshes
130+
tasks/<suite>/<task>.xml # portable MJCF (file= are asset relpaths)
131+
tasks/<suite>/<task>.goal.json # resolved BDDL goal + OSC cfg + body_pos/quat
132+
```
133+
134+
`_locator.py` resolves these from a local `roboverse_data` clone or HuggingFace on
135+
demand — so on a machine where `libero` and `robosuite` are **uninstalled**, the
136+
native task still loads and runs. Verified across all 130: model fields exact
137+
**130/130** (`max|Δ| = 0`), ported checker **130/130** (0 mismatch). Use
138+
`ROBOVERSE_DATA_DIR` to point at a shared cache.
139+
70140
## Reproduce — run commands
71141

72142
All commands assume `MUJOCO_GL=egl` (headless) and the dedicated env. The
@@ -100,6 +170,16 @@ python -m scripts.policy.train_bc_libero \
100170
LIBERO_CONFIG_PATH=$HOME/.libero_plus MUJOCO_GL=egl \
101171
python -m scripts.policy.eval_bc_liberoplus --ckpt scripts/policy/ckpt/bc.pt \
102172
--base <task> --suite libero_object --episodes 8
173+
174+
# --- native tasks (no libero/robosuite): vendor assets -> verify -> side-by-side ---
175+
ROBOVERSE_DATA_DIR=/path/to/roboverse_data MUJOCO_GL=egl LIBERO_CONFIG_PATH=$HOME/.libero_plus \
176+
python -m scripts.native.migrate_libero_assets # 130 tasks -> roboverse_data/libero
177+
ROBOVERSE_DATA_DIR=/path/to/roboverse_data MUJOCO_GL=egl LIBERO_CONFIG_PATH=$HOME/.libero_plus \
178+
python -m scripts.native.verify_vendored # all 130: model fields + checker 1:1
179+
MUJOCO_GL=egl LIBERO_CONFIG_PATH=$HOME/.libero_plus \
180+
python -m scripts.native.sweep_all_native # per-task side-by-side + checker + OSC
181+
# unit tests (assert the native task imports/runs with NO libero/robosuite):
182+
python -m pytest tests/test_libero_native.py -v # in liberoplus
103183
```
104184

105185
## Side-by-side: native LIBERO-plus vs MetaSim
@@ -217,3 +297,11 @@ benchmark measures:
217297
passthrough==native), not a SOTA reproduction. The official
218298
`Sylvest/openvla-7b-oft-finetuned-libero-plus` checkpoint runs through the same
219299
bridge for absolute benchmark numbers.
300+
- The native pack's **success checking and physics are bitwise** (checker 0
301+
mismatch, model fields `max|Δ| = 0`). Its **render** is recompiled from the
302+
vendored MJCF, so meshes are re-triangulated and shading differs by ~2–5 / 255
303+
vs robosuite (visually identical); the export path's compiled `model.mjb`
304+
reproduces the agentview pixel-exactly when that is needed.
305+
- The native pack covers the **130 base tasks**. The 10,120 LIBERO-plus
306+
perturbations remain passthrough-only (their value is the upstream
307+
perturbation pipeline; vendoring all of them is future work).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""MetaSim-native LIBERO tasks — run a LIBERO scene + checker with NO ``libero`` import.
2+
3+
A self-contained bundle (compiled ``model.mjb`` + init states + resolved BDDL goal)
4+
is loaded into MetaSim's MuJoCo handler, driven by the ported OSC_POSE controller,
5+
and evaluated by the ported BDDL checker (:mod:`.checker`). This is the path to
6+
running LIBERO without the upstream library.
7+
"""
8+
9+
from __future__ import annotations
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Resolve vendored LIBERO assets/MJCF from a local roboverse_data clone or HF.
2+
3+
The native LIBERO tasks reference their scene MJCF + every mesh/texture by a
4+
stable relpath under the ``libero/`` prefix of the ``RoboVerseOrg/roboverse_data``
5+
HF dataset (laid out by ``scripts/native/migrate_libero_assets.py``)::
6+
7+
libero/tasks/<suite>/<task>.xml portable MJCF (file= are asset relpaths)
8+
libero/tasks/<suite>/<task>.goal.json resolved BDDL goal + OSC config
9+
libero/assets/robosuite/<...> Franka / gripper / arena meshes (shared)
10+
libero/assets/libero/<...> chiliocosm objects / textures
11+
12+
Resolution prefers a local ``roboverse_data`` clone (dev fast path / offline) and
13+
falls back to downloading the single file from HF on demand — so a clone-less
14+
machine works without the ``libero`` package. The local-clone path imports nothing
15+
heavy (keeps the native task ``mujoco``+``numpy`` only); the HF path lazily imports
16+
``metasim.utils.hf_util`` (never ``libero``/``robosuite``).
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import os
22+
from pathlib import Path
23+
24+
_PREFIX = "libero" # top-level dir inside the roboverse_data dataset
25+
26+
27+
def _data_dirs():
28+
"""Candidate ``roboverse_data`` roots, most specific first."""
29+
cands = []
30+
env = os.environ.get("ROBOVERSE_DATA_DIR")
31+
if env:
32+
cands.append(Path(env).expanduser())
33+
repo_root = Path(__file__).resolve().parents[3]
34+
cands += [
35+
Path.cwd() / "roboverse_data",
36+
repo_root / "roboverse_data",
37+
repo_root.parent / "roboverse_data", # sibling clone (~/projects/RoboVerse/roboverse_data)
38+
Path.home() / "projects" / "RoboVerse" / "roboverse_data",
39+
]
40+
return cands
41+
42+
43+
def libero_data(relpath: str) -> str:
44+
"""Resolve ``libero/<relpath>`` to a concrete local file (clone or HF download).
45+
46+
Args:
47+
relpath: path under the dataset's ``libero/`` prefix, e.g.
48+
``tasks/libero_object/<task>.xml`` or
49+
``assets/robosuite/grippers/meshes/panda_gripper/finger.stl``.
50+
51+
Returns:
52+
An existing local filesystem path.
53+
"""
54+
rel = f"{_PREFIX}/{relpath}"
55+
for d in _data_dirs():
56+
p = d / rel
57+
if p.exists():
58+
return str(p)
59+
# cold (clone-less) machine: pull the single file from HF into LOCAL_DIR
60+
from metasim.utils.hf_util import LOCAL_DIR, check_and_download_single
61+
62+
target = os.path.join(LOCAL_DIR, rel)
63+
check_and_download_single(target)
64+
return target
65+
66+
67+
def libero_asset(relpath: str) -> str:
68+
"""Resolve an asset relpath (the ``file=`` value inside a vendored task MJCF)."""
69+
return libero_data(f"assets/{relpath}")
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.mjb
Binary file not shown.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"goal": [
3+
{
4+
"fn": "in",
5+
"obj": "alphabet_soup_1_main",
6+
"region": "basket_1_contain_region"
7+
}
8+
],
9+
"suite": "libero_object",
10+
"base": "pick_up_the_alphabet_soup_and_place_it_in_the_basket",
11+
"task": "pick_up_the_alphabet_soup_and_place_it_in_the_basket_table_1",
12+
"osc": {
13+
"eef_site": "gripper0_grip_site",
14+
"arm_qvel_index": [
15+
0,
16+
1,
17+
2,
18+
3,
19+
4,
20+
5,
21+
6
22+
],
23+
"arm_act_index": [
24+
0,
25+
1,
26+
2,
27+
3,
28+
4,
29+
5,
30+
6
31+
],
32+
"grip_act_index": [
33+
7,
34+
8
35+
],
36+
"initial_joint": [
37+
0.0,
38+
-0.161037389,
39+
0.0,
40+
-2.44459747,
41+
0.0,
42+
2.2267522,
43+
0.7853981633974483
44+
],
45+
"torque_limits": [
46+
[
47+
-80.0,
48+
-80.0,
49+
-80.0,
50+
-80.0,
51+
-80.0,
52+
-12.0,
53+
-12.0
54+
],
55+
[
56+
80.0,
57+
80.0,
58+
80.0,
59+
80.0,
60+
80.0,
61+
12.0,
62+
12.0
63+
]
64+
],
65+
"substeps": 25
66+
}
67+
}
Binary file not shown.
Binary file not shown.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Ported LIBERO BDDL success checker — evaluates the goal with NO ``libero`` import.
2+
3+
LIBERO checks task success by AND-ing the BDDL ``goal_state`` predicates against
4+
the live object states. This module reimplements the geometric primitives those
5+
predicates reduce to, reading them straight from a MuJoCo ``model``/``data`` pair
6+
(e.g. a MetaSim handler's physics). It is the piece that lets a LIBERO task be
7+
*evaluated* inside MetaSim without the upstream library.
8+
9+
Predicate reduction (from ``libero/libero/envs``):
10+
11+
* ``In(obj, region_site)`` — the region is a ``SiteObjectState`` whose
12+
``check_contact`` is always ``True`` (no dynamics for site objects), so it
13+
reduces to ``in_box``: the object's body position lies inside the region site's
14+
oriented box (``site_pos ± |site_mat @ site_half_size|``, lower z extended by
15+
1 cm) — verbatim from ``objects/site_object.py::in_box``.
16+
* ``On(a, b)`` with ``b`` an object — ``ObjectState.check_ontop``: the surface ``b``
17+
sits below the placed object ``a`` (``z[b] <= z[a]``) AND their ``contact_geoms``
18+
touch AND ``||a.xy - b.xy|| < 0.03``.
19+
* ``On(a, b)`` with ``b`` a region site — ``SiteObjectState.check_ontop`` via
20+
``SiteObject.under``: ``a``'s body lies in the site's oriented box above its floor
21+
(``size[2]-0.005 < (mat·(a-site))_z < size[2]+0.10`` and ``|·xy| < size[:2]``) AND
22+
the region's parent object's ``contact_geoms`` touch ``a``'s. Contact uses the
23+
exact robosuite ``contact_geoms`` sets (resolved to geom ids by the exporter).
24+
* ``Open/Close/TurnOn/TurnOff(obj)`` — articulated-joint predicates. LIBERO reads
25+
each of the object's joint qpos and compares it to a per-object threshold
26+
(``articulated_objects.py``); ``Open``/``TurnOn`` are satisfied if **any** joint
27+
passes, ``Close``/``TurnOff`` only if **all** joints pass. The export step probes
28+
the live object's own ``is_open``/``is_close``/``turn_on``/``turn_off`` method to
29+
recover the exact ``(op, threshold)`` per object + the qpos addresses, so this
30+
checker reduces the predicate to ``op(qpos[addr], thr)`` with no libero import.
31+
32+
A goal is exported (see ``scripts/native/export_libero_task.py``) as a list of
33+
predicate dicts with model names already resolved, so this checker needs only the
34+
names + the MuJoCo state.
35+
"""
36+
37+
from __future__ import annotations
38+
39+
import operator
40+
41+
import mujoco
42+
import numpy as np
43+
44+
# comparison operators recovered by the exporter when probing the articulated
45+
# object's own threshold method (e.g. ``qpos < max(open_ranges)`` -> "lt").
46+
_OPS = {"lt": operator.lt, "le": operator.le, "gt": operator.gt, "ge": operator.ge}
47+
48+
49+
def _site(model, data, name):
50+
sid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_SITE, name)
51+
if sid < 0:
52+
raise KeyError(f"site {name!r} not in model")
53+
return np.array(data.site_xpos[sid]), np.array(data.site_xmat[sid]).reshape(3, 3), np.array(model.site_size[sid])
54+
55+
56+
def _body_xpos(model, data, name):
57+
bid = mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_BODY, name)
58+
if bid < 0:
59+
raise KeyError(f"body {name!r} not in model")
60+
return np.array(data.xpos[bid])
61+
62+
63+
def in_box(site_pos, site_mat, site_half_size, other_pos) -> bool:
64+
"""Verbatim LIBERO ``SiteObject.in_box`` (axis-aligned-ish containment test)."""
65+
total = np.abs(site_mat @ site_half_size)
66+
ub = site_pos + total
67+
lb = site_pos - total
68+
lb[2] -= 0.01
69+
return bool(np.all(other_pos > lb) and np.all(other_pos < ub))
70+
71+
72+
def _in_contact(data, geoms_a, geoms_b) -> bool:
73+
"""Robosuite ``check_contact``: any active contact between the two geom-id sets."""
74+
a, b = set(geoms_a), set(geoms_b)
75+
for i in range(data.ncon):
76+
c = data.contact[i]
77+
if (c.geom1 in a and c.geom2 in b) or (c.geom1 in b and c.geom2 in a):
78+
return True
79+
return False
80+
81+
82+
def under(site_pos, site_mat, site_size, other_pos) -> bool:
83+
"""Verbatim LIBERO ``SiteObject.under`` (object resting in a region site box)."""
84+
delta = site_mat @ (other_pos - site_pos)
85+
return bool(site_size[2] - 0.005 < delta[2] < site_size[2] + 0.10 and np.all(np.abs(delta[:2]) < site_size[:2]))
86+
87+
88+
def eval_predicate(model, data, p) -> bool:
89+
"""Evaluate one exported predicate dict against the current MuJoCo state."""
90+
fn = p["fn"]
91+
if fn == "in": # object inside a region site
92+
sp, sm, ss = _site(model, data, p["region"])
93+
return in_box(sp, sm, ss, _body_xpos(model, data, p["obj"]))
94+
if fn == "on": # placed object `obj` (arg1) resting on surface object `obj2` (arg2)
95+
a, b = _body_xpos(model, data, p["obj"]), _body_xpos(model, data, p["obj2"])
96+
return (
97+
bool(b[2] <= a[2]) # surface below the placed object (ObjectState.check_ontop)
98+
and _in_contact(data, p["obj_geoms"], p["obj2_geoms"])
99+
and float(np.linalg.norm(a[:2] - b[:2])) < 0.03
100+
)
101+
if fn == "on_site": # placed object `obj` (arg1) resting in region site `site` (arg2)
102+
sp, sm, ss = _site(model, data, p["site"])
103+
if not under(sp, sm, ss, _body_xpos(model, data, p["obj"])):
104+
return False
105+
# a region with no backing object (table region) has no contact term
106+
return p["parent_geoms"] is None or _in_contact(data, p["parent_geoms"], p["obj_geoms"])
107+
if fn == "joint": # open / close / turnon / turnoff on an articulated object
108+
op = _OPS[p["op"]]
109+
checks = [bool(op(float(data.qpos[addr]), p["thr"])) for addr in p["qpos_addr"]]
110+
return any(checks) if p["reduce"] == "any" else all(checks)
111+
raise NotImplementedError(f"predicate {fn!r} not ported")
112+
113+
114+
def check_success(model, data, goal) -> bool:
115+
"""AND over all goal predicates (LIBERO ``_check_success`` semantics)."""
116+
return all(eval_predicate(model, data, p) for p in goal)

0 commit comments

Comments
 (0)