Rate this Page

Source code for torchrl.envs.libs.lbforaging

# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations

import importlib

import numpy as np
import torch
from tensordict import TensorDict, TensorDictBase

from torchrl.data.tensor_specs import Categorical, Composite, Unbounded
from torchrl.envs.common import _EnvWrapper
from torchrl.envs.libs.gym import _gym_to_torchrl_spec_transform, set_gym_backend
from torchrl.envs.utils import _classproperty

_has_lbforaging = importlib.util.find_spec("lbforaging") is not None


def _get_envs() -> list[str]:
    if not _has_lbforaging:
        raise ImportError("lbforaging is not installed in your virtual environment.")
    import gymnasium
    import lbforaging  # noqa: F401 - registers the Foraging-* Gymnasium environments.

    return sorted(k for k in gymnasium.envs.registry if k.startswith("Foraging-"))


[docs] class LBForagingWrapper(_EnvWrapper): """Level-Based Foraging environment wrapper. `Level-Based Foraging <https://github.com/semitable/lb-foraging>`__ is a fully-cooperative, sparse-reward Gymnasium environment for evaluating multi-agent credit assignment: a variable number of agents, each with a randomly assigned level, must coordinate to collect food items that also have levels, and a food item is only collected when the agents currently adjacent to it have levels summing to at least its own. ``lbforaging`` exposes a single ``gymnasium.Env`` whose observation, action and reward spaces are ``gymnasium.spaces.Tuple`` instances, one entry per agent, and whose ``terminated``/``truncated`` flags are shared by the whole team (LBF episodes end when either all food is collected or a step limit is reached, for every agent at once). This wrapper exposes that structure the way every other TorchRL multi-agent wrapper does: per-agent entries nested under a single ``"agents"`` group, and the shared ``done``/``terminated``/``truncated`` at the root. Reaching LBF's step limit with food still on the board is a truncation; collecting the last food item is a termination, including at the step limit. Args: env (gymnasium.Env): a Level-Based Foraging environment, i.e. the result of ``gymnasium.make("Foraging-<...>-v3")`` after ``import lbforaging``. Keyword Args: categorical_actions (bool, optional): whether discrete actions should be provided as categorical indices or one-hot encodings. Defaults to ``True``. seed (int, optional): the seed to use to reset the environment on the first call to :meth:`~.reset`. Defaults to ``None``. Examples: >>> import gymnasium >>> import lbforaging >>> from torchrl.envs.libs.lbforaging import LBForagingWrapper >>> base_env = gymnasium.make("Foraging-8x8-2p-3f-v3") >>> env = LBForagingWrapper(base_env, categorical_actions=False) >>> env.rollout(3) TensorDict( fields={ agents: TensorDict( fields={ action: Tensor(shape=torch.Size([3, 2, 6]), device=cpu, dtype=torch.int64, is_shared=False), observation: Tensor(shape=torch.Size([3, 2, 15]), device=cpu, dtype=torch.float32, is_shared=False)}, batch_size=torch.Size([3, 2]), device=None, is_shared=False), done: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False), next: TensorDict( fields={ agents: TensorDict( fields={ observation: Tensor(shape=torch.Size([3, 2, 15]), device=cpu, dtype=torch.float32, is_shared=False), reward: Tensor(shape=torch.Size([3, 2, 1]), device=cpu, dtype=torch.float32, is_shared=False)}, batch_size=torch.Size([3, 2]), device=None, is_shared=False), done: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False), terminated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False), truncated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False)}, batch_size=torch.Size([3]), device=None, is_shared=False), terminated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False), truncated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False)}, batch_size=torch.Size([3]), device=None, is_shared=False) """ git_url = "https://github.com/semitable/lb-foraging" libname = "lbforaging" @_classproperty def available_envs(cls): if not _has_lbforaging: return [] return _get_envs() def __init__( self, env=None, *, categorical_actions: bool = True, seed: int | None = None, **kwargs, ): if env is not None: kwargs["env"] = env self.categorical_actions = categorical_actions self._seed = seed super().__init__(**kwargs) @property def lib(self): import lbforaging return lbforaging def _check_kwargs(self, kwargs: dict): import gymnasium from lbforaging.foraging.environment import ForagingEnv if "env" not in kwargs: raise TypeError("Could not find environment key 'env' in kwargs.") env = kwargs["env"] if not isinstance(env, gymnasium.Env) or not isinstance( env.unwrapped, ForagingEnv ): raise TypeError("env is not a Level-Based Foraging gymnasium.Env.") def _build_env(self, env, **kwargs): if len(self.batch_size): raise RuntimeError( f"LBForaging does not support custom batch_size {self.batch_size}." ) return env @set_gym_backend("gymnasium") def _make_specs(self, env) -> None: self.n_agents = env.unwrapped.n_agents self.group_map = {"agents": [str(i) for i in range(self.n_agents)]} observation_spec = _gym_to_torchrl_spec_transform( env.observation_space, device=self.device ) action_spec = _gym_to_torchrl_spec_transform( env.action_space, categorical_action_encoding=self.categorical_actions, device=self.device, ) self.full_observation_spec = Composite( { "agents": Composite( {"observation": observation_spec}, shape=torch.Size((self.n_agents,)), ) } ) self.full_action_spec = Composite( { "agents": Composite( {"action": action_spec}, shape=torch.Size((self.n_agents,)) ) } ) self.full_reward_spec = Composite( { "agents": Composite( { "reward": Unbounded( shape=torch.Size((self.n_agents, 1)), device=self.device ) }, shape=torch.Size((self.n_agents,)), ) } ) self.full_done_spec = Composite( { key: Categorical( n=2, shape=torch.Size((1,)), dtype=torch.bool, device=self.device ) for key in ("done", "terminated", "truncated") } ) def _init_env(self) -> None: pass def _set_seed(self, seed: int | None) -> None: self._seed = seed def _stack_obs(self, observations) -> torch.Tensor: return torch.as_tensor( np.stack(observations), dtype=torch.float32, device=self.device ) def _reset( self, tensordict: TensorDictBase | None = None, **kwargs ) -> TensorDictBase: observations, _ = self._env.reset(seed=self._seed) self._seed = None agents_td = TensorDict( {"observation": self._stack_obs(observations)}, batch_size=torch.Size((self.n_agents,)), device=self.device, ) return TensorDict({"agents": agents_td}, batch_size=(), device=self.device) def _step(self, tensordict: TensorDictBase) -> TensorDictBase: action = tensordict.get(("agents", "action")) action_np = self.full_action_spec[self.action_key].to_numpy(action) observations, rewards, terminated, truncated, _info = self._env.step( [int(a) for a in action_np] ) # LBF reports both food completion and its native time limit as # termination. Preserve bootstrapping when only the time limit ended play. base_env = self._env.unwrapped if ( terminated and base_env.current_step >= base_env._max_episode_steps and base_env.field.any() ): terminated = False truncated = True agents_td = TensorDict( { "observation": self._stack_obs(observations), "reward": torch.tensor( rewards, dtype=torch.float32, device=self.device ).unsqueeze(-1), }, batch_size=torch.Size((self.n_agents,)), device=self.device, ) terminated_t = torch.tensor( [bool(terminated)], dtype=torch.bool, device=self.device ) truncated_t = torch.tensor( [bool(truncated)], dtype=torch.bool, device=self.device ) return TensorDict( { "agents": agents_td, "done": terminated_t | truncated_t, "terminated": terminated_t, "truncated": truncated_t, }, batch_size=(), device=self.device, ) def close(self, *, raise_if_closed: bool = True) -> None: self._env.close()
[docs] class LBForagingEnv(LBForagingWrapper): """Level-Based Foraging environment wrapper, constructed from an environment name. See :class:`~torchrl.envs.libs.lbforaging.LBForagingWrapper` for a description of the environment and the tensordict layout it produces. Args: env_name (str): the name of a registered Level-Based Foraging Gymnasium environment, e.g. ``"Foraging-8x8-2p-3f-v3"`` (an 8x8 grid, 2 players, 3 food items). See :attr:`~.available_envs` for the full list. Keyword Args: categorical_actions (bool, optional): whether discrete actions should be provided as categorical indices or one-hot encodings. Defaults to ``True``. seed (int, optional): the seed to use to reset the environment on the first call to :meth:`~.reset`. Defaults to ``None``. **kwargs: forwarded to ``gymnasium.make``. Examples: >>> from torchrl.envs.libs.lbforaging import LBForagingEnv >>> env = LBForagingEnv("Foraging-8x8-2p-3f-v3", categorical_actions=False) >>> rollout = env.rollout(3) """ def __init__( self, env_name: str, *, categorical_actions: bool = True, seed: int | None = None, **kwargs, ): if not _has_lbforaging: raise ImportError( f"lbforaging python package was not found. Please install this dependency. " f"More info: {self.git_url}." ) kwargs["env_name"] = env_name kwargs["categorical_actions"] = categorical_actions kwargs["seed"] = seed super().__init__(**kwargs) def _check_kwargs(self, kwargs: dict): if "env_name" not in kwargs: raise TypeError("Expected 'env_name' to be part of kwargs") def _build_env( self, env_name: str, **kwargs, ): import gymnasium import lbforaging # noqa: F401 - registers the Foraging-* Gymnasium environments. # LBF returns a per-agent list of rewards, which Gymnasium's passive # env checker flags as invalid (it expects a single scalar) even # though it is the documented, intended behaviour for this env. env = gymnasium.make(env_name, disable_env_checker=True, **kwargs) return super()._build_env(env)