# Copyright 2024 The HuggingFace Inc. team. All rights reserved.
# Copyright 2026 Tensor Auto Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
r"""This module contains configuration files for different environments. LIBERO and RoboCasa365 are supported."""
import abc
import logging
from copy import copy
from dataclasses import dataclass, field
from typing import Literal, get_args
import draccus
from opentau.configs.types import FeatureType, PolicyFeature
from opentau.constants import ACTION, OBS_IMAGES, OBS_STATE
from opentau.utils.accelerate_utils import get_proc_accelerator
# Single source of truth for the ``control_mode`` enum: the type alias is
# used in the dataclass annotation, the runtime tuple is derived from it
# via ``get_args`` so the two can't drift if a new mode is ever added.
ControlMode = Literal["joint", "ee"]
CONTROL_MODE_CHOICES: tuple[str, ...] = get_args(ControlMode)
# Step size between consecutive ``speed`` bucket labels. Must match the
# spacing of
# :data:`opentau.datasets.speed_percentiles.SPEED_BUCKET_LABELS`
# (``0, 10, 20, ..., 100``); update both call sites together if the
# label set ever changes. Named ``_STEP`` (not ``_SECONDS``) because
# the bucket label is no longer a duration in seconds — it is a
# percentile-rank index.
SPEED_BUCKET_STEP = 10
[docs]
@dataclass
class EnvConfig(draccus.ChoiceRegistry, abc.ABC):
"""Base configuration for an environment.
Args:
import_name: Name under which the environment should be imported. For LIBERO, this doesn't need to be set.
make_id: Gymnasium/Gym environment id (e.g., ``"CartPole-v1"``) when using ``gym.make``-style construction.
task: Optional task or suite identifier understood by the environment.
fps: Target stepping frequency in Hz. Exact meaning is env-specific; for
LIBERO it is the robosuite control frequency (``LiberoEnv`` overrides
the default to 20).
features: Mapping from logical feature names (e.g., ``"action"``,
``"pixels/agentview_image"``) to :class:`~opentau.configs.types.PolicyFeature`
definitions consumed by policies.
features_map: Mapping from environment keys to standardized OpenTau keys
(e.g., mapping env observations into ``OBS_IMAGES`` / ``OBS_STATE``).
max_parallel_tasks: Maximum number of tasks to run in parallel within the env.
disable_env_checker: Whether to disable Gymnasium environment checking.
metadata: Optional pi07 metadata fields (speed/quality/mistake/
robot_type/control_mode) broadcast across the eval batch.
Defaults to all-``None`` (no metadata injected).
"""
import_name: str = None
make_id: str = None
task: str | None = None
fps: int = 30
features: dict[str, PolicyFeature] = field(default_factory=dict)
features_map: dict[str, str] = field(default_factory=dict)
max_parallel_tasks: int = 1
disable_env_checker: bool = True
metadata: EnvMetadataConfig = field(default_factory=EnvMetadataConfig)
@property
def type(self) -> str:
"""Return the registered choice name for this config.
Returns:
The draccus choice name used in configs/CLI.
"""
return self.get_choice_name(self.__class__)
@property
@abc.abstractmethod
def gym_kwargs(self) -> dict:
"""Keyword arguments used to construct the environment.
Subclasses must implement this to return the kwargs consumed by the project’s
environment builder (often ``gym.make`` or an equivalent factory).
Returns:
A dict of keyword arguments for environment construction.
"""
raise NotImplementedError()
[docs]
@EnvConfig.register_subclass("libero")
@dataclass
class LiberoEnv(EnvConfig):
r"""Configuration for the LIBERO environment.
Args:
task: The LIBERO task or suite to use (e.g., ``"libero_10"``).
task_ids: Optional list of specific task IDs within the suite to use (if ``None``, all tasks in the suite are used).
fps: Robosuite control frequency (Hz) for the LIBERO sim — the rate at
which each ``env.step`` advances the simulation. Threaded through to
``OffScreenRenderEnv(control_freq=...)``. Defaults to 20, robosuite's
native LIBERO rate (the value used before this field was wired up).
episode_length: Maximum length of each episode in steps.
obs_type: Type of observations to use (e.g., ``"pixels_agent_pos"``).
render_mode: Rendering mode for the environment (e.g., ``"rgb_array"``).
camera_name: Comma-separated LIBERO raw camera names to render — both
count and ordering of LIBERO cameras at eval are driven by this
string. Defaults to ``"agentview_image,robot0_eye_in_hand_image"``
(agentview + wrist eye-in-hand). Set to ``"agentview_image"``
(single camera) for agentview-only rollouts. When the underlying
policy was trained with a larger ``cfg.num_cams`` (e.g. a
multi-domain mixture with 4 camera slots),
``preprocess_observation`` zero-fills the remaining ``cameraN``
slots so the train↔eval input structure stays aligned —
independent of how many real LIBERO cameras this field renders.
init_states: Whether to initialize states randomly.
camera_name_mapping: Optional mapping from camera names to standardized keys.
subgoal_source: HuggingFace repo id of the v2.1 LeRobot dataset to source
subgoal images from at eval time, or ``None`` to disable subgoal
injection. When set (today only ``"TensorAuto/libero"`` is exercised),
:func:`opentau.scripts.eval.eval` constructs a
:class:`~opentau.envs.subgoal.LiberoLastFrameSubgoalGenerator` that
samples a random matching episode per env at each ``rollout()`` call
and serves its last frame as the subgoal — matching the pi07
low-level / pi07-paligemma training-time ``subgoal{k}`` input. Behavior
with repos other than ``"TensorAuto/libero"`` is undefined.
features: Mapping from logical feature names to :class:`~opentau.configs.types.PolicyFeature` definitions.
features_map: Mapping from environment keys to standardized OpenTau keys.
"""
task: str = "libero_10" # can also choose libero_spatial, libero_object, etc.
task_ids: list[int] | None = None
fps: int = 20 # robosuite control frequency (Hz); see field docstring above
episode_length: int = 520
obs_type: str = "pixels_agent_pos"
render_mode: str = "rgb_array"
camera_name: str = "agentview_image,robot0_eye_in_hand_image"
init_states: bool = True
camera_name_mapping: dict[str, str] | None = None
subgoal_source: str | None = None
features: dict[str, PolicyFeature] = field(
default_factory=lambda: {
"action": PolicyFeature(type=FeatureType.ACTION, shape=(7,)),
}
)
features_map: dict[str, str] = field(
default_factory=lambda: {
"action": ACTION,
"agent_pos": OBS_STATE, # TODO: fix the name
"pixels/agentview_image": f"{OBS_IMAGES}.image", # TODO: fix the name
"pixels/robot0_eye_in_hand_image": f"{OBS_IMAGES}.image2", # TODO: fix the name
}
)
def __post_init__(self):
if self.fps <= 0:
raise ValueError(
f"LIBERO env.fps (robosuite control frequency in Hz) must be positive, got {self.fps}"
)
if self.obs_type == "pixels":
self.features["pixels/agentview_image"] = PolicyFeature(
type=FeatureType.VISUAL, shape=(360, 360, 3)
)
self.features["pixels/robot0_eye_in_hand_image"] = PolicyFeature(
type=FeatureType.VISUAL, shape=(360, 360, 3)
)
elif self.obs_type == "pixels_agent_pos":
self.features["agent_pos"] = PolicyFeature(type=FeatureType.STATE, shape=(8,))
self.features["pixels/agentview_image"] = PolicyFeature(
type=FeatureType.VISUAL, shape=(360, 360, 3)
)
self.features["pixels/robot0_eye_in_hand_image"] = PolicyFeature(
type=FeatureType.VISUAL, shape=(360, 360, 3)
)
else:
raise ValueError(f"Unsupported obs_type: {self.obs_type}")
@property
def gym_kwargs(self) -> dict:
r"""Return the keyword arguments used to construct the LIBERO environment."""
suite_names = [s.strip() for s in str(self.task).split(",") if s.strip()]
accelerator = get_proc_accelerator()
task_ids: dict[str, list[int] | None]
if accelerator is None:
task_ids = {suite: copy(self.task_ids) for suite in suite_names}
logging.info(f"[LIBERO environment] No accelerator found, using {task_ids=}.")
else:
from opentau.envs.libero import _get_suite
task_ids = {
suite: _get_suite(suite).tasks if self.task_ids is None else self.task_ids
for suite in suite_names
}
logging.info(f"[LIBERO environment] Before distributing, using {task_ids=}.")
task_ids = {
suite: [
tsk
for idx, tsk in enumerate(tasks)
if idx % accelerator.num_processes == accelerator.process_index
]
for suite, tasks in task_ids.items()
}
logging.info(
f"[LIBERO environment] After distributing, using {task_ids=} on {accelerator.process_index=}."
)
return {
"obs_type": self.obs_type,
"render_mode": self.render_mode,
"task_ids": task_ids,
"control_freq": self.fps,
}
[docs]
@EnvConfig.register_subclass("robocasa")
@dataclass
class RoboCasaEnv(EnvConfig):
r"""Configuration for the RoboCasa365 kitchen environment.
RoboCasa runs on robosuite 1.5 (shared with LIBERO since the libero extra was
bumped to robosuite 1.5.2), so it co-installs in the same venv. The default
robot is the PandaOmron mobile manipulator — hence the 12-D action and 16-D
state, distinct from LIBERO's 7-D/8-D. Set ``metadata.robot_type`` /
``eval.control_mode`` to select the matching per-(robot_type, control_mode)
projection head when evaluating a co-trained policy.
Args:
task: A RoboCasa task name (e.g. ``"CloseFridge"``), a comma-separated
list of task names, or a benchmark-group shortcut
(``atomic_seen``/``composite_seen``/``composite_unseen``/
``pretrain50``/``pretrain100``/``pretrain200``/``pretrain300``), which
auto-expands to the upstream task list and auto-sets ``split``.
fps: RoboCasa control frequency (Hz); also the ``render_fps`` for videos.
episode_length: Maximum steps per episode (``_max_episode_steps``).
Defaults to 1000; set to ``null`` (None) to use RoboCasa's official
per-task horizon from the dataset registry (e.g. OpenCabinet=1050,
TurnOnMicrowave=450) instead of a single global cap.
obs_type: ``"pixels"`` or ``"pixels_agent_pos"``.
render_mode: Rendering mode for the environment.
camera_name: Comma-separated raw RoboCasa camera names to render. The
wrapper remaps them to ``camera0``/``camera1``/... so the policy input
structure matches LIBERO regardless of the raw names; when the policy
was trained with a larger ``cfg.num_cams``, ``preprocess_observation``
zero-fills the remaining slots.
observation_height: Height of observation images.
observation_width: Width of observation images.
visualization_height: Height of visualization frames.
visualization_width: Width of visualization frames.
split: RoboCasa dataset split (``None``/``"all"``/``"pretrain"``/
``"target"``). Defaults to ``"pretrain"`` when left ``None`` — every
task-group shortcut and concrete single-task config resolves to the
pretrain kitchen-scene distribution; set explicitly to override.
obj_registries: Object-mesh registries to sample assets from. Defaults to
``["lightwheel"]`` (the pack the asset downloader ships by default);
add ``"objaverse"`` only after downloading that ~30GB pack.
assets_root: Directory to store/read RoboCasa kitchen assets, kept outside the
(ephemeral) uv venv. ``None`` resolves to the ``ROBOCASA_ASSETS_ROOT`` env
var, else ``HF_OPENTAU_HOME/robocasa/assets``.
auto_download_assets: If ``True`` (default), the asset packs ``obj_registries``
needs are downloaded automatically (once) on first env build.
features: Mapping from logical feature names to ``PolicyFeature`` definitions.
features_map: Mapping from environment keys to standardized OpenTau keys.
subgoal_frames_dirs: Comma-separated list of ``goal_frames/`` directories
(each with a ``manifest.csv`` + the per-``(task, seed, decoder,
camera)`` ``.png`` files harvested from successful rollouts). When set,
eval feeds the harvested terminal frame of the *same scene seed* as the
subgoal image whenever one is available for that ``(task, seed)``;
scenes with no harvested success get no subgoal (padded). Probes how
much a perfect goal-image (world-model output) would help. Requires a
pinned ``eval.seed`` so the seed→scene map matches the harvest. ``None``
(default) = no subgoal conditioning.
"""
task: str = "CloseFridge"
fps: int = 20
episode_length: int | None = 1000
obs_type: str = "pixels_agent_pos"
render_mode: str = "rgb_array"
camera_name: str = "robot0_agentview_left,robot0_eye_in_hand,robot0_agentview_right"
observation_height: int = 256
observation_width: int = 256
visualization_height: int = 512
visualization_width: int = 512
split: str | None = None
obj_registries: list[str] = field(default_factory=lambda: ["lightwheel"])
assets_root: str | None = None
auto_download_assets: bool = True
subgoal_frames_dirs: str | None = None
features: dict[str, PolicyFeature] = field(
default_factory=lambda: {
"action": PolicyFeature(type=FeatureType.ACTION, shape=(12,)),
}
)
features_map: dict[str, str] = field(
default_factory=lambda: {
"action": ACTION,
"agent_pos": OBS_STATE,
}
)
def __post_init__(self):
if self.fps <= 0:
raise ValueError(f"RoboCasa env.fps (control frequency in Hz) must be positive, got {self.fps}")
if self.obs_type not in ("pixels", "pixels_agent_pos"):
raise ValueError(f"Unsupported obs_type: {self.obs_type}")
# The wrapper remaps the i-th raw camera to ``camera{i}``; mirror that in
# the feature map using OpenTau's ``image`` / ``image2`` / ... convention
# (camera0 -> image, camera1 -> image2, ...), matching LIBERO.
cams = [c.strip() for c in self.camera_name.split(",") if c.strip()]
for i, cam in enumerate(cams):
self.features[f"pixels/{cam}"] = PolicyFeature(
type=FeatureType.VISUAL,
shape=(self.observation_height, self.observation_width, 3),
)
mapped = "image" if i == 0 else f"image{i + 1}"
self.features_map[f"pixels/{cam}"] = f"{OBS_IMAGES}.{mapped}"
if self.obs_type == "pixels_agent_pos":
self.features["agent_pos"] = PolicyFeature(type=FeatureType.STATE, shape=(16,))
@property
def gym_kwargs(self) -> dict:
r"""Return the keyword arguments used to construct the RoboCasa environment.
Task resolution and per-rank sharding live in ``create_robocasa_envs`` (they
need the ``robocasa`` package for group expansion), so this stays sim-free
and only carries the obs/render parameters plus an optional ``split``.
"""
kwargs: dict = {
"obs_type": self.obs_type,
"render_mode": self.render_mode,
"observation_height": self.observation_height,
"observation_width": self.observation_width,
"visualization_height": self.visualization_height,
"visualization_width": self.visualization_width,
}
if self.split is not None:
kwargs["split"] = self.split
return kwargs