Source code for pettingzoo.utils.wrappers.reshape

from __future__ import annotations

from typing import Any, cast

import gymnasium.spaces
import numpy as np
from gymnasium.spaces import Box
from typing_extensions import override

from pettingzoo.utils.env import ActionType, AECEnv, AgentID, ObsType, ParallelEnv
from pettingzoo.utils.wrappers.base import BaseWrapper
from pettingzoo.utils.wrappers.base_parallel import BaseParallelWrapper


def _check_shape(shape: tuple[int, ...]) -> None:
    assert isinstance(shape, tuple), f"shape must be a tuple, is {shape}"
    assert len(shape) > 0, "shape must not be empty"
    assert all(np.issubdtype(type(element), np.integer) for element in shape), (
        f"shape must be a tuple of ints, is {shape}"
    )
    assert all(element != -1 for element in shape), (
        f"-1 is not supported, give the size of every axis. shape is {shape}"
    )
    assert all(element > 0 for element in shape), (
        f"shape must be a tuple of positive ints, is {shape}"
    )


def _reshaped_space(
    space: gymnasium.spaces.Space[Any], shape: tuple[int, ...], agent: AgentID
) -> Box:
    """Returns `space` reshaped to `shape`, keeping the per-element bounds."""
    assert isinstance(space, Box), (
        f"reshape is only defined for Box observation spaces, "
        f"the space of agent {agent} is {space}."
    )
    assert np.prod(shape) == np.prod(space.shape), (
        f"new shape {shape} must have as many elements as the observation "
        f"space of agent {agent}, which has shape {space.shape}."
    )
    # Box accepts a numpy dtype, its annotation just says otherwise.
    dtype = cast("type[np.floating[Any] | np.integer[Any]]", space.dtype)
    return Box(
        low=space.low.reshape(shape), high=space.high.reshape(shape), dtype=dtype
    )


[docs] class ReshapeObservationV1(BaseWrapper[AgentID, Any, ActionType]): """Reshapes each agent's observation to the given shape. The observation space is reshaped the same way, so per-element bounds are kept. Only works on Box observation spaces. :param env: The AEC environment to wrap. :param shape: The new observation shape. Must have as many elements as the old one. """ def __init__( self, env: AECEnv[AgentID, ObsType, ActionType], shape: tuple[int, ...] ): assert isinstance(env, AECEnv), ( "ReshapeObservationV1 is only compatible with AEC environments, " "use ReshapeObservationParallelV1 instead." ) _check_shape(shape) super().__init__(env) self.shape = shape self._obs_spaces: dict[AgentID, Box] = {} # Fail on construction for envs that already know their agents. Envs that # only fill possible_agents in reset() are caught on the first observation. for agent in getattr(env, "possible_agents", []): self.observation_space(agent) @override def observation_space(self, agent: AgentID) -> Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _reshaped_space( self.env.observation_space(agent), self.shape, agent ) return self._obs_spaces[agent] @override def observe(self, agent: AgentID) -> np.ndarray | None: # Checks the shape against this agent's space. Cached after the first call. self.observation_space(agent) obs = self.env.observe(agent) if obs is None: return None return np.reshape(obs, self.shape) @override def __str__(self) -> str: return f"ReshapeObservationV1<{self.env!s}>"
[docs] class ReshapeObservationParallelV1(BaseParallelWrapper[AgentID, Any, ActionType]): """Reshapes each agent's observation to the given shape. The observation space is reshaped the same way, so per-element bounds are kept. Only works on Box observation spaces. :param env: The parallel environment to wrap. :param shape: The new observation shape. Must have as many elements as the old one. """ def __init__( self, env: ParallelEnv[AgentID, ObsType, ActionType], shape: tuple[int, ...] ): _check_shape(shape) super().__init__(env) self.shape = shape self._obs_spaces: dict[AgentID, Box] = {} # Fail on construction for envs that already know their agents. Envs that # only fill possible_agents in reset() are caught on the first observation. for agent in getattr(env, "possible_agents", []): self.observation_space(agent) @override def observation_space(self, agent: AgentID) -> Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _reshaped_space( self.env.observation_space(agent), self.shape, agent ) return self._obs_spaces[agent] def _reshape(self, observations: dict[AgentID, Any]) -> dict[AgentID, np.ndarray]: reshaped = {} for agent, obs in observations.items(): # Checks the shape against this agent's space. Cached after the first call. self.observation_space(agent) reshaped[agent] = np.reshape(obs, self.shape) return reshaped @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> tuple[dict[AgentID, np.ndarray], dict[AgentID, dict[str, Any]]]: observations, infos = self.env.reset(seed=seed, options=options) return self._reshape(observations), infos @override def step( self, actions: dict[AgentID, ActionType] ) -> tuple[ dict[AgentID, np.ndarray], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict[str, Any]], ]: observations, rewards, terminations, truncations, infos = self.env.step(actions) return self._reshape(observations), rewards, terminations, truncations, infos @override def __str__(self) -> str: return f"ReshapeObservationParallelV1<{self.env!s}>"