Source code for pettingzoo.utils.wrappers.nan_noop

"""Wrappers that replace NaN actions with a caller-supplied no-op action."""

from __future__ import annotations

import copy
import warnings
from typing import Any

import gymnasium.spaces
import numpy as np
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_noop(
    space: gymnasium.spaces.Space[Any], no_op_action: Any, agent: Any
) -> None:
    if not space.contains(no_op_action):
        raise ValueError(
            f"The no-op action {no_op_action!r} is outside the action space "
            f"for agent {agent!r}: {space}."
        )


def _replace_nan(action: Any, no_op_action: Any, agent: Any, env: Any) -> Any:
    if action is None or not np.isnan(action).any():
        return action
    # Also validate at replacement time for environments with dynamic agents.
    _check_noop(env.action_space(agent), no_op_action, agent)
    warnings.warn(
        f"Step received a NaN action {action} for agent {agent!r}. "
        "Taking the supplied no-op action.",
        stacklevel=3,
    )
    # A mutable replacement must not be shared between agents or steps.
    return copy.deepcopy(no_op_action)


[docs] class NanNoopV1(BaseWrapper[AgentID, ObsType, Any]): """Replaces a numeric action containing a NaN with a supplied no-op action. A warning is emitted for each replacement. Numeric scalars and arrays are supported. Actions without NaNs, and ``None`` actions for dead agents, pass through unchanged. The advertised action spaces are not modified. The same caller-supplied no-op value is used for every agent. It must belong to each agent's action space, including its shape and dtype, or a ``ValueError`` is raised. Known agents are checked at construction and the affected agent is checked on each replacement. Each replacement receives a copy of the value. :param env: The AEC environment to wrap. :param no_op_action: The no-op action, valid for all affected agents. """ def __init__( self, env: AECEnv[AgentID, ObsType, ActionType], no_op_action: ActionType ): assert isinstance(env, AECEnv), ( "NanNoopV1 is only compatible with AEC environments, " "use NanNoopParallelV1 instead." ) super().__init__(env) self.no_op_action = copy.deepcopy(no_op_action) for agent in getattr(env, "possible_agents", []): _check_noop(env.action_space(agent), self.no_op_action, agent) @override def step(self, action: Any) -> None: self.env.step( _replace_nan(action, self.no_op_action, self.agent_selection, self.env) ) @override def __str__(self) -> str: return f"NanNoopV1<{self.env!s}>"
[docs] class NanNoopParallelV1(BaseParallelWrapper[AgentID, ObsType, Any]): """Replaces each numeric action containing a NaN with a supplied no-op action. Each supplied action is checked independently and a warning is emitted for each replacement. Numeric scalars and arrays are supported. Actions without NaNs pass through unchanged, and the input dictionary is not modified. As in :class:`NanNoopV1`, the same caller-supplied no-op value must be valid in every affected agent's action space. Known agents are checked at construction and the affected agent is checked on each replacement. Each replacement receives a copy. The advertised action spaces are not modified. :param env: The parallel environment to wrap. :param no_op_action: The no-op action, valid for all affected agents. """ def __init__( self, env: ParallelEnv[AgentID, ObsType, ActionType], no_op_action: ActionType ): super().__init__(env) self.no_op_action = copy.deepcopy(no_op_action) for agent in getattr(env, "possible_agents", []): _check_noop(env.action_space(agent), self.no_op_action, agent) @override def step( self, actions: dict[AgentID, Any] ) -> tuple[ dict[AgentID, ObsType], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict[str, Any]], ]: return self.env.step( { agent: _replace_nan(action, self.no_op_action, agent, self.env) for agent, action in actions.items() } ) @override def __str__(self) -> str: return f"NanNoopParallelV1<{self.env!s}>"