Source code for pettingzoo.utils.wrappers.dtype

from __future__ import annotations

from typing import Any

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 _cast_space(
    space: gymnasium.spaces.Space[Any], dtype: np.dtype, agent: AgentID, wrapper: str
) -> Box:
    """Returns ``space`` with its bounds and dtype cast to ``dtype``."""
    assert isinstance(space, Box), (
        f"{wrapper} only supports Box observation spaces, "
        f"agent {agent!r} has a {type(space).__name__}."
    )
    try:
        return Box(
            low=space.low.astype(dtype),
            high=space.high.astype(dtype),
            dtype=dtype.type,
        )
    except ValueError as e:
        raise ValueError(
            f"{wrapper} cannot cast agent {agent!r}'s observation space "
            f"{space} to {dtype}: the cast bounds are not a valid Box."
        ) from e


[docs] class DtypeObservationV1(BaseWrapper[AgentID, Any, ActionType]): """Recasts each agent's observation to ``dtype``. The observation space becomes a Box with the same shape and the new dtype, with the old bounds put through the same cast. Every space in ``possible_agents`` is built and checked when the wrapper is created. Bounds that do not fit the new dtype wrap around, and casting an infinite bound to an integer dtype is undefined in numpy, so an unbounded Box cast to an integer dtype collapses to a single point that will not contain the observations. Both match SuperSuit's ``dtype_v0``. :param env: The AEC environment to wrap. :param dtype: Anything ``numpy.dtype`` accepts, e.g. ``np.float32``. """ def __init__(self, env: AECEnv[AgentID, ObsType, ActionType], dtype: Any): assert isinstance(env, AECEnv), ( "DtypeObservationV1 is only compatible with AEC environments, " "use DtypeObservationParallelV1 instead." ) super().__init__(env) self.dtype = np.dtype(dtype) self._obs_spaces: dict[AgentID, Box] = { agent: _cast_space( env.observation_space(agent), self.dtype, agent, type(self).__name__ ) for agent in getattr(env, "possible_agents", []) } @override def observation_space(self, agent: AgentID) -> Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _cast_space( self.env.observation_space(agent), self.dtype, agent, type(self).__name__, ) return self._obs_spaces[agent] @override def observe(self, agent: AgentID) -> Any: obs = self.env.observe(agent) if obs is None: return None return np.asarray(obs).astype(self.dtype) @override def __str__(self) -> str: return f"DtypeObservationV1<{self.env!s}>"
[docs] class DtypeObservationParallelV1(BaseParallelWrapper[AgentID, Any, ActionType]): """Recasts each agent's observation to ``dtype``. The observation space becomes a Box with the same shape and the new dtype, with the old bounds put through the same cast. Every space in ``possible_agents`` is built and checked when the wrapper is created. Bounds that do not fit the new dtype wrap around, and casting an infinite bound to an integer dtype is undefined in numpy, so an unbounded Box cast to an integer dtype collapses to a single point that will not contain the observations. Both match SuperSuit's ``dtype_v0``. :param env: The parallel environment to wrap. :param dtype: Anything ``numpy.dtype`` accepts, e.g. ``np.float32``. """ def __init__(self, env: ParallelEnv[AgentID, ObsType, ActionType], dtype: Any): super().__init__(env) self.dtype = np.dtype(dtype) self._obs_spaces: dict[AgentID, Box] = { agent: _cast_space( env.observation_space(agent), self.dtype, agent, type(self).__name__ ) for agent in getattr(env, "possible_agents", []) } @override def observation_space(self, agent: AgentID) -> Box: if agent not in self._obs_spaces: self._obs_spaces[agent] = _cast_space( self.env.observation_space(agent), self.dtype, agent, type(self).__name__, ) return self._obs_spaces[agent] def _cast(self, observations: dict[AgentID, Any]) -> dict[AgentID, Any]: return { agent: np.asarray(obs).astype(self.dtype) for agent, obs in observations.items() } @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> tuple[dict[AgentID, Any], dict[AgentID, dict[str, Any]]]: observations, infos = self.env.reset(seed=seed, options=options) return self._cast(observations), infos @override def step( self, actions: dict[AgentID, ActionType] ) -> tuple[ dict[AgentID, Any], 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._cast(observations), rewards, terminations, truncations, infos @override def __str__(self) -> str: return f"DtypeObservationParallelV1<{self.env!s}>"