Files
lerobot/tests/test_robocasa_env.py
T
Pepijn 8904768db4 feat(envs): add RoboCasa composite-task benchmark integration
Integrates 5 selected RoboCasa kitchen tasks (3 short + 2 long) as a
LeRobot benchmark environment, following the same pattern as Libero.

Selected tasks:
  Short: PickPlaceCounterToCabinet, PrepareToast, CoffeeSetupMug
  Long:  PrepareCoffee, RestockPantry

Changes:
- envs/robocasa.py: RoboCasaEnv wrapper with flat 12D Box action space,
  3-camera pixel obs, and 16D proprioceptive state
- envs/configs.py: RoboCasaEnv config with features_map
- envs/factory.py: wire robocasa into make_env + make_env_pre_post_processors
- processor/env_processor.py: RoboCasaProcessorStep for obs key remapping
- tests/test_robocasa_env.py: full test suite (auto-skips if assets missing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 17:08:32 +01:00

177 lines
6.7 KiB
Python

#!/usr/bin/env python
# Copyright 2025 The HuggingFace Inc. team. 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.
"""Tests for RoboCasa LeRobot integration.
Requires: robocasa installed + kitchen assets downloaded.
Tests are skipped automatically if robocasa is not available.
"""
from __future__ import annotations
import numpy as np
import pytest
# Skip entire module if robocasa is not installed or assets are missing
robocasa = pytest.importorskip("robocasa", reason="robocasa not installed")
from lerobot.envs.robocasa import ACTION_DIM, STATE_DIM, CAM_KEY_TO_NAME, RoboCasaEnv, create_robocasa_envs
# The 5 benchmark tasks (3 short + 2 long)
BENCHMARK_TASKS = [
"PickPlaceCounterToCabinet", # short
"PrepareToast", # short
"CoffeeSetupMug", # short
"PrepareCoffee", # long
"RestockPantry", # long
]
SHORT_TASKS = BENCHMARK_TASKS[:3]
LONG_TASKS = BENCHMARK_TASKS[3:]
IMAGE_SIZE = 64 # small for fast tests
@pytest.fixture(scope="module")
def single_env():
"""Shared env instance for lightweight tests."""
env = RoboCasaEnv(task="PickPlaceCounterToCabinet", image_size=IMAGE_SIZE)
yield env
env.close()
class TestRoboCasaEnvSpaces:
def test_action_space_is_flat_box(self, single_env):
import gymnasium as gym
assert isinstance(single_env.action_space, gym.spaces.Box)
assert single_env.action_space.shape == (ACTION_DIM,)
assert single_env.action_space.dtype == np.float32
def test_action_bounds(self, single_env):
assert np.all(single_env.action_space.low == -1.0)
assert np.all(single_env.action_space.high == 1.0)
def test_observation_space_has_pixels_and_state(self, single_env):
import gymnasium as gym
assert isinstance(single_env.observation_space, gym.spaces.Dict)
assert "pixels" in single_env.observation_space.spaces
assert "robot_state" in single_env.observation_space.spaces
def test_observation_space_cameras(self, single_env):
pixels_space = single_env.observation_space["pixels"]
expected_cams = set(CAM_KEY_TO_NAME.values())
assert set(pixels_space.spaces.keys()) == expected_cams
def test_state_dim(self, single_env):
state_space = single_env.observation_space["robot_state"]
assert state_space.shape == (STATE_DIM,)
class TestRoboCasaEnvReset:
def test_reset_returns_obs_and_info(self, single_env):
obs, info = single_env.reset()
assert isinstance(obs, dict)
assert isinstance(info, dict)
def test_reset_obs_has_pixels(self, single_env):
obs, _ = single_env.reset()
assert "pixels" in obs
for cam_name in CAM_KEY_TO_NAME.values():
assert cam_name in obs["pixels"], f"Missing camera: {cam_name}"
def test_reset_obs_image_shape(self, single_env):
obs, _ = single_env.reset()
for cam_name, img in obs["pixels"].items():
assert img.shape == (IMAGE_SIZE, IMAGE_SIZE, 3), f"Bad shape for {cam_name}: {img.shape}"
assert img.dtype == np.uint8
def test_reset_obs_state_shape(self, single_env):
obs, _ = single_env.reset()
assert obs["robot_state"].shape == (STATE_DIM,)
assert obs["robot_state"].dtype == np.float32
def test_reset_info_has_task(self, single_env):
_, info = single_env.reset()
assert "task" in info
assert info["task"] == "PickPlaceCounterToCabinet"
class TestRoboCasaEnvStep:
def test_step_10_random_actions(self, single_env):
single_env.reset()
for _ in range(10):
action = single_env.action_space.sample()
obs, reward, terminated, truncated, info = single_env.step(action)
assert obs["robot_state"].shape == (STATE_DIM,)
assert isinstance(reward, float)
assert isinstance(terminated, bool)
assert isinstance(truncated, bool)
def test_step_bad_action_raises(self, single_env):
single_env.reset()
with pytest.raises(ValueError, match="Expected 1-D action"):
single_env.step(np.zeros((2, ACTION_DIM)))
def test_step_info_has_is_success(self, single_env):
single_env.reset()
_, _, _, _, info = single_env.step(single_env.action_space.sample())
assert "is_success" in info
class TestRoboCasaConfig:
def test_robocasa_env_config(self):
from lerobot.envs.configs import RoboCasaEnv as RoboCasaEnvConfig
from lerobot.configs.types import FeatureType
cfg = RoboCasaEnvConfig(task="PickPlaceCounterToCabinet", image_size=IMAGE_SIZE)
assert cfg.type == "robocasa"
# action feature
assert "action" in cfg.features
assert cfg.features["action"].shape == (ACTION_DIM,)
# camera features
for cam in ("agentview_left", "agentview_right", "eye_in_hand"):
assert cam in cfg.features
assert cfg.features[cam].type == FeatureType.VISUAL
assert cfg.features[cam].shape == (IMAGE_SIZE, IMAGE_SIZE, 3)
# state feature
assert "robot_state" in cfg.features
assert cfg.features["robot_state"].shape == (STATE_DIM,)
def test_make_env_config_robocasa(self):
from lerobot.envs.factory import make_env_config
cfg = make_env_config("robocasa", task="PickPlaceCounterToCabinet")
assert cfg.type == "robocasa"
class TestRoboCasaProcessorStep:
def test_processor_remaps_keys(self):
import torch
from lerobot.processor.env_processor import RoboCasaProcessorStep
from lerobot.utils.constants import OBS_IMAGES, OBS_STATE
step = RoboCasaProcessorStep()
B = 2
obs = {
f"{OBS_IMAGES}.agentview_left": torch.zeros(B, 3, IMAGE_SIZE, IMAGE_SIZE),
f"{OBS_IMAGES}.agentview_right": torch.zeros(B, 3, IMAGE_SIZE, IMAGE_SIZE),
f"{OBS_IMAGES}.eye_in_hand": torch.zeros(B, 3, IMAGE_SIZE, IMAGE_SIZE),
f"observation.robot_state": torch.zeros(B, STATE_DIM),
}
out = step._process_observation(obs)
assert OBS_STATE in out
assert out[OBS_STATE].dtype == torch.float32
for cam in ("agentview_left", "agentview_right", "eye_in_hand"):
assert f"{OBS_IMAGES}.{cam}" in out