Skip to content

Commit 1b26f32

Browse files
committed
Merge feat/libero-1to1: LIBERO + LIBERO-plus 1:1 integration
- LIBERO (130) + LIBERO-plus (10,120 perturbation tasks) passthrough, bitwise 1:1 - asset deep-audit (0 silent fallback) + MetaSim MuJoCo backend migration (engine Δ=0) - OSC_POSE port bitwise-faithful (per-state torque Δ=5.55e-15) for in-MetaSim control - ACT-style BC policy + openVLA-OFT bridge + parity/test harnesses Additive: 21 new files + 2 small edits (.gitignore, _passthrough_index prefixes). Backward-compat verified: 222 tests pass in roboverse env, libero tiers pass in dedicated envs.
2 parents 2daf405 + bfa8752 commit 1b26f32

23 files changed

Lines changed: 3296 additions & 0 deletions

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,9 @@ tools/publish_report*.sh
135135
# local-only: personal agent rules (commit identity, push habits, private infra) and env override
136136
CLAUDE.local.md
137137
ENVIRONMENTS.local.md
138+
139+
# --- LIBERO 1:1 isolated integration (feat/libero-1to1) ---
140+
third_party/
141+
INTEGRATION_LOG.md
142+
*.hdf5
143+
libero_datasets/

get_started/libero_passthrough.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Get started: run native LIBERO tasks through RoboVerse (1:1 passthrough).
2+
3+
LIBERO's 130 tasks (suites: spatial / object / goal / 10 / 90) run via their
4+
*original* robosuite + MuJoCo simulator, exposed through RoboVerse. Because the
5+
underlying env is LIBERO's own ``OffScreenRenderEnv``, behaviour is **1:1 by
6+
construction** — no cross-sim porting.
7+
8+
Prerequisites (LIBERO pins conflict with the default RoboVerse env, so use a
9+
dedicated conda env)::
10+
11+
git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git
12+
cd LIBERO && pip install -e . # robosuite==1.4.0, bddl, robomimic, ...
13+
pip install gymnasium numpy-quaternion loguru rootutils
14+
15+
Run headless::
16+
17+
MUJOCO_GL=egl python get_started/libero_passthrough.py
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import os
23+
24+
os.environ.setdefault("MUJOCO_GL", "egl") # headless offscreen render
25+
26+
from loguru import logger as log
27+
28+
# Importing the package auto-registers every ``Libero/<suite>__<task>`` id.
29+
import roboverse_pack.tasks.libero # noqa: F401
30+
from roboverse_pack.tasks._passthrough_index import list_passthrough_envs
31+
from roboverse_pack.tasks.libero._passthrough import make_libero_env
32+
33+
34+
def main() -> None:
35+
# 1) Discover registered LIBERO tasks.
36+
ids = list_passthrough_envs().get("Libero", [])
37+
log.info(f"Registered LIBERO tasks: {len(ids)}")
38+
for env_id in ids[:3]:
39+
log.info(f" e.g. {env_id}")
40+
41+
# 2) Build one task via the native LIBERO path (1:1). ``make_libero_env``
42+
# resolves the BDDL, builds the offscreen env, seeds, resets, and applies
43+
# init state 0 — the exact construction LIBERO's own eval uses.
44+
env = make_libero_env("libero_spatial", 0, seed=0, init_state_index=0)
45+
46+
# 3) Step a few zero actions. NOTE: LIBERO uses the legacy gym 4-tuple API:
47+
# step -> (obs, reward, done, info); reset -> obs. ``make_libero_env``
48+
# already reset + applied init state 0, so step straight away.
49+
for t in range(5):
50+
obs, reward, done, info = env.step([0.0] * 7)
51+
if t == 0:
52+
log.info(f"obs keys ({len(obs)}): {sorted(obs)[:6]} ...")
53+
log.info(f"agentview_image: {obs['agentview_image'].shape} {obs['agentview_image'].dtype}")
54+
log.info(f"robot0_eef_pos: {obs['robot0_eef_pos']}")
55+
log.info(f" step {t}: reward={reward:.3f} done={done}")
56+
if done:
57+
break
58+
59+
env.close()
60+
log.info("done.")
61+
62+
63+
if __name__ == "__main__":
64+
main()

roboverse_pack/tasks/_passthrough_index.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"MyoSuite/",
2626
"GymRobotics/",
2727
"DMControl/",
28+
"Libero/",
29+
"LiberoPlus/",
2830
]
2931

3032

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
"""LIBERO passthrough — expose all 130 LIBERO tasks through RoboVerse.
2+
3+
LIBERO (*Lifelong Robot Learning*, Liu et al. 2023) tasks are robosuite / MuJoCo
4+
environments defined by BDDL task files across five suites:
5+
``libero_spatial`` (10), ``libero_object`` (10), ``libero_goal`` (10),
6+
``libero_10`` (10, a.k.a. LIBERO-Long) and ``libero_90`` (90). Together they are
7+
the 130-task LIBERO benchmark (``libero_90`` + ``libero_10`` = LIBERO-100).
8+
9+
This module registers each task under a ``Libero/<suite>__<task_name>`` gym id
10+
with a *lazy* entry point: registration does NOT import LIBERO (so it is safe
11+
even when LIBERO's heavy deps — ``robosuite==1.4.0`` / ``bddl==1.0.1`` /
12+
``robomimic==0.2.0`` / ``numpy<1.24`` — are absent from the active env), and the
13+
underlying env is only built when the env is actually made.
14+
15+
LIBERO must be importable in the active env. Because its pins (numpy<1.24,
16+
robosuite, mujoco) conflict with the default RoboVerse env, install it in a
17+
**dedicated** conda env::
18+
19+
git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git
20+
cd LIBERO && pip install -e .
21+
22+
Until then, registration registers nothing (the ids only become available once
23+
LIBERO is importable) and :func:`make_libero_env` raises a clear, actionable
24+
error. Once installed, the factory runs the **native** LIBERO env
25+
(``OffScreenRenderEnv``) via the exact code path from LIBERO's README / lifelong
26+
eval → **1:1 by construction**, no cross-sim porting.
27+
28+
Note:
29+
LIBERO uses the *legacy* gym step API — ``env.step(action)`` returns the
30+
4-tuple ``(obs, reward, done, info)`` and ``env.reset()`` returns ``obs``
31+
(not gymnasium's 5-tuple / ``(obs, info)``). This is preserved deliberately
32+
for bitwise fidelity with the original simulator; downstream 1:1 demo replay
33+
and the LIBERO benchmark eval both rely on the native interface. Prefer
34+
:func:`make_libero_env` (returns the raw native env) for replay / eval; the
35+
gym registration exists primarily for discovery (``list_passthrough_envs``).
36+
"""
37+
38+
from __future__ import annotations
39+
40+
import os
41+
from typing import Any
42+
43+
from loguru import logger as log
44+
45+
#: The five standard LIBERO suites, in canonical order (name -> n_tasks shown for
46+
#: reference only; the authoritative counts come from the benchmark dict).
47+
LIBERO_SUITES: tuple[str, ...] = (
48+
"libero_spatial",
49+
"libero_object",
50+
"libero_goal",
51+
"libero_10",
52+
"libero_90",
53+
)
54+
55+
#: Separator between suite and task name inside a gym id. A single ``/`` already
56+
#: denotes the gym namespace, so the suite/task split uses ``__`` (both halves
57+
#: stay valid ``[\w:-]+`` gym-name characters and the split is reversible).
58+
_SEP = "__"
59+
60+
61+
def _ensure_imported():
62+
"""Import LIBERO's benchmark module (raises if LIBERO is not installed)."""
63+
import libero # noqa: F401
64+
from libero.libero import benchmark
65+
66+
return benchmark
67+
68+
69+
def discover_libero_tasks() -> dict[str, list[str]]:
70+
"""Return ``{suite_name: [task_name, ...]}`` from LIBERO's benchmark dict.
71+
72+
Uses the authoritative ``benchmark.get_benchmark_dict()`` ordering (so task
73+
index ``i`` matches ``suite.get_task(i)`` / ``get_task_init_states(i)`` /
74+
``get_task_demonstration(i)``). Returns ``{}`` if LIBERO is not importable,
75+
which keeps :func:`register_libero_passthrough` safe in envs without LIBERO.
76+
"""
77+
try:
78+
benchmark = _ensure_imported()
79+
except Exception:
80+
return {}
81+
bdict = benchmark.get_benchmark_dict()
82+
out: dict[str, list[str]] = {}
83+
for name in LIBERO_SUITES:
84+
if name not in bdict:
85+
continue
86+
try:
87+
suite = bdict[name]()
88+
out[name] = list(suite.get_task_names())
89+
except Exception as e: # pragma: no cover - defensive
90+
log.debug(f"[libero_passthrough] could not enumerate suite {name!r}: {e}")
91+
return out
92+
93+
94+
def make_libero_env(
95+
benchmark_name: str,
96+
task_id: int,
97+
*,
98+
camera_heights: int = 128,
99+
camera_widths: int = 128,
100+
seed: int | None = None,
101+
init_state_index: int | None = None,
102+
**env_kwargs: Any,
103+
):
104+
"""Build the native LIBERO ``OffScreenRenderEnv`` for ``(suite, task_id)``.
105+
106+
This is the canonical 1:1 construction path (matches LIBERO's README and
107+
``libero/lifelong`` eval): resolve the task's BDDL file, build the offscreen
108+
env, optionally seed + reset + apply a stored init state. Returns the raw
109+
native env (legacy gym 4-tuple ``step`` API).
110+
111+
Args:
112+
benchmark_name: One of :data:`LIBERO_SUITES` (e.g. ``"libero_spatial"``).
113+
task_id: Task index within the suite (``0 <= task_id < suite.n_tasks``).
114+
camera_heights: Offscreen RGB camera height (LIBERO default 128).
115+
camera_widths: Offscreen RGB camera width (LIBERO default 128).
116+
seed: If given, ``env.seed(seed)`` before reset (LIBERO determinism).
117+
init_state_index: If given, ``env.set_init_state(init_states[idx])`` after
118+
reset, where ``init_states = suite.get_task_init_states(task_id)`` —
119+
the exact init-state path the benchmark eval uses.
120+
**env_kwargs: Extra kwargs forwarded to ``OffScreenRenderEnv`` (e.g.
121+
``camera_depths=True``). A gym-injected ``render_mode`` is ignored
122+
(LIBERO is always offscreen).
123+
124+
Returns:
125+
The native LIBERO ``OffScreenRenderEnv`` instance.
126+
127+
Raises:
128+
RuntimeError: If LIBERO is not importable in the active env.
129+
"""
130+
env_kwargs.pop("render_mode", None) # gym.make may inject this; LIBERO is offscreen-only
131+
try:
132+
from libero.libero import benchmark, get_libero_path
133+
from libero.libero.envs import OffScreenRenderEnv
134+
except Exception as e:
135+
raise RuntimeError(
136+
"LIBERO is not importable in the active env. Install it in a dedicated conda env "
137+
"(it pins numpy<1.24 / robosuite==1.4.0 / bddl==1.0.1 / robomimic==0.2.0, which "
138+
"conflict with the default RoboVerse env):\n"
139+
" git clone https://github.com/Lifelong-Robot-Learning/LIBERO.git\n"
140+
" cd LIBERO && pip install -e .\n"
141+
f"Original import error: {type(e).__name__}: {e}"
142+
) from e
143+
144+
bdict = benchmark.get_benchmark_dict()
145+
if benchmark_name not in bdict:
146+
raise KeyError(f"Unknown LIBERO suite {benchmark_name!r}; available: {sorted(bdict.keys())}")
147+
suite = bdict[benchmark_name]()
148+
n = suite.get_num_tasks()
149+
if not (0 <= task_id < n):
150+
raise IndexError(f"task_id {task_id} out of range for suite {benchmark_name!r} (n_tasks={n})")
151+
152+
task = suite.get_task(task_id)
153+
bddl_file = os.path.join(get_libero_path("bddl_files"), task.problem_folder, task.bddl_file)
154+
if not os.path.isfile(bddl_file):
155+
raise FileNotFoundError(f"BDDL file for {benchmark_name}/{task.name} not found: {bddl_file}")
156+
157+
env_args = {"bddl_file_name": bddl_file, "camera_heights": camera_heights, "camera_widths": camera_widths}
158+
env_args.update(env_kwargs)
159+
env = OffScreenRenderEnv(**env_args)
160+
if seed is not None:
161+
env.seed(seed)
162+
env.reset()
163+
if init_state_index is not None:
164+
init_states = suite.get_task_init_states(task_id)
165+
env.set_init_state(init_states[init_state_index])
166+
return env
167+
168+
169+
def _make_libero_env(benchmark_name: str, task_id: int, **kwargs: Any):
170+
"""Gym entry-point shim (positional-suite + task_id via registered kwargs)."""
171+
return make_libero_env(benchmark_name, task_id, **kwargs)
172+
173+
174+
def register_libero_passthrough(prefix: str = "Libero/") -> list[str]:
175+
"""Register every LIBERO task as ``Libero/<suite>__<task_name>`` (lazy).
176+
177+
Idempotent. Returns the registered ids. Safe when LIBERO is absent (returns
178+
``[]`` — the ids only become discoverable once LIBERO is importable, since
179+
the suite/task enumeration is authoritative and comes from LIBERO itself).
180+
"""
181+
from gymnasium.envs.registration import register, registry
182+
183+
tasks = discover_libero_tasks()
184+
if not tasks:
185+
log.debug(
186+
"[libero_passthrough] LIBERO not importable; no tasks registered "
187+
"(ids become available once LIBERO is installed in this env)"
188+
)
189+
return []
190+
191+
registered: list[str] = []
192+
failed: list[tuple[str, str]] = []
193+
for bench_name, task_names in tasks.items():
194+
for task_id, task_name in enumerate(task_names):
195+
ns_id = f"{prefix}{bench_name}{_SEP}{task_name}"
196+
if ns_id in registry:
197+
registered.append(ns_id)
198+
continue
199+
try:
200+
register(
201+
id=ns_id,
202+
entry_point="roboverse_pack.tasks.libero._passthrough:_make_libero_env",
203+
kwargs={"benchmark_name": bench_name, "task_id": task_id},
204+
# The native LIBERO env is legacy-gym (4-tuple step); keep gym's
205+
# wrappers out of the way so the passthrough stays bitwise-1:1.
206+
disable_env_checker=True,
207+
order_enforce=False,
208+
)
209+
registered.append(ns_id)
210+
except Exception as e:
211+
failed.append((ns_id, str(e)))
212+
if failed:
213+
log.debug(f"[libero_passthrough] {len(failed)} registration failures (first: {failed[0]})")
214+
log.info(f"[libero_passthrough] registered {len(registered)} LIBERO tasks under {prefix!r}")
215+
return registered
216+
217+
218+
# Registering on import mirrors the other ``roboverse_pack.tasks.*`` passthroughs.
219+
# The ``libero`` package ``__init__`` auto-imports every submodule, so importing
220+
# ``roboverse_pack.tasks.libero`` triggers this. It is a safe no-op when LIBERO is
221+
# not installed (``discover_libero_tasks`` returns ``{}``).
222+
try:
223+
register_libero_passthrough()
224+
except Exception as _e: # pragma: no cover - registration must never break import
225+
log.debug(f"[libero_passthrough] auto-registration skipped: {_e}")

0 commit comments

Comments
 (0)