Source code for pettingzoo.utils.wrappers.sticky_action

"""Wrappers that sometimes repeat an agent's previous action instead of the new one."""

from __future__ import annotations

import copy
from typing import Any

import numpy as np
from gymnasium.utils import seeding
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 _sticky_np_random(seed: int | None) -> np.random.Generator:
    """Builds an RNG that does not share a stream with the wrapped environment.

    ``seeding.np_random(seed)`` is a pure function of ``seed``, and nearly every
    PettingZoo environment calls it with the seed given to ``reset``. Using it
    directly here would hand the wrapper the same PCG64 stream the environment
    is drawing its own noise from, so the two would move in lockstep. Spawning a
    child sequence keeps the streams independent while staying reproducible.
    """
    # seeding.np_random validates the seed and, for seed=None, picks the entropy.
    _, np_seed = seeding.np_random(seed)
    child_seq = np.random.SeedSequence(np_seed).spawn(1)[0]
    return np.random.Generator(np.random.PCG64(child_seq))


[docs] class StickyActionV1(BaseWrapper[AgentID, ObsType, ActionType]): """Repeats an agent's previous action with probability ``repeat_action_probability``. Each agent keeps its own previous action, and an agent has nothing to repeat until it has acted once since the last :meth:`reset`, so the first action after a reset is always passed through. Only this wrapper's own reset clears the memory: an environment that auto-resets underneath it, such as ``MultiEpisodeParallelEnv`` on the inside, can still have the first action of a later episode repeated. The remembered action is a deep copy, so reusing an action buffer between steps does not defeat the wrapper. Seeding follows the usual PettingZoo pattern: :meth:`reset` reseeds from the seed it is given, which means ``reset(seed=None)`` starts a fresh unseeded stream. Pass a seed on every reset if you want a whole multi-episode run to be reproducible. :param env: The AEC environment to wrap. :param repeat_action_probability: Probability of using the previous action, in the interval [0, 1). """ def __init__( self, env: AECEnv[AgentID, ObsType, ActionType], repeat_action_probability: float, ): assert isinstance(env, AECEnv), ( "StickyActionV1 is only compatible with AEC environments, " "use StickyActionParallelV1 instead." ) assert 0 <= repeat_action_probability < 1, ( "repeat_action_probability must be in the interval [0, 1), " f"got {repeat_action_probability}." ) super().__init__(env) self.repeat_action_probability = repeat_action_probability self._prev_actions: dict[AgentID, ActionType] = {} self._np_random = _sticky_np_random(None) def _sticky_action(self, agent: AgentID, action: ActionType) -> ActionType: if ( agent in self._prev_actions and self._np_random.uniform() < self.repeat_action_probability ): # Hand out a copy so the environment cannot mutate what we remember. return copy.deepcopy(self._prev_actions[agent]) self._prev_actions[agent] = copy.deepcopy(action) return action @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> None: self._np_random = _sticky_np_random(seed) self._prev_actions = {} self.env.reset(seed=seed, options=options) @override def step(self, action: ActionType) -> None: # Read agent_selection defensively: it is missing before reset, and after # the last dead step it names an agent already dropped from terminations. agent = getattr(self.env, "agent_selection", None) if ( agent is None or self.env.terminations.get(agent, True) or self.env.truncations.get(agent, True) ): # Stepping before reset, a dead agent's None step, or a step after # every agent is done. Nothing to stick, and the inner env owns the # error or warning. self.env.step(action) self._prev_actions.pop(agent, None) else: self.env.step(self._sticky_action(agent, action)) @override def __str__(self) -> str: return f"StickyActionV1<{self.env!s}>"
[docs] class StickyActionParallelV1(BaseParallelWrapper[AgentID, ObsType, ActionType]): """Repeats an agent's previous action with probability ``repeat_action_probability``. Each agent keeps its own previous action, and an agent has nothing to repeat until it has acted once since the last :meth:`reset`, so the first action after a reset is always passed through. Only this wrapper's own reset clears the memory: an environment that auto-resets underneath it, such as ``MultiEpisodeParallelEnv`` on the inside, can still have the first action of a later episode repeated. The remembered action is a deep copy, so reusing an action buffer between steps does not defeat the wrapper. Seeding follows the usual PettingZoo pattern: :meth:`reset` reseeds from the seed it is given, which means ``reset(seed=None)`` starts a fresh unseeded stream. Pass a seed on every reset if you want a whole multi-episode run to be reproducible. :param env: The parallel environment to wrap. :param repeat_action_probability: Probability of using the previous action, in the interval [0, 1). """ def __init__( self, env: ParallelEnv[AgentID, ObsType, ActionType], repeat_action_probability: float, ): assert 0 <= repeat_action_probability < 1, ( "repeat_action_probability must be in the interval [0, 1), " f"got {repeat_action_probability}." ) super().__init__(env) self.repeat_action_probability = repeat_action_probability self._prev_actions: dict[AgentID, ActionType] = {} self._np_random = _sticky_np_random(None) def _sticky_action(self, agent: AgentID, action: ActionType) -> ActionType: if ( agent in self._prev_actions and self._np_random.uniform() < self.repeat_action_probability ): # Hand out a copy so the environment cannot mutate what we remember. return copy.deepcopy(self._prev_actions[agent]) self._prev_actions[agent] = copy.deepcopy(action) return action @override def reset( self, seed: int | None = None, options: dict[str, Any] | None = None ) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict[str, Any]]]: self._np_random = _sticky_np_random(seed) self._prev_actions = {} return self.env.reset(seed=seed, options=options) @override def step( self, actions: dict[AgentID, ActionType] ) -> tuple[ dict[AgentID, ObsType], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict[str, Any]], ]: actions = { agent: self._sticky_action(agent, action) for agent, action in actions.items() } observations, rewards, terminations, truncations, infos = self.env.step(actions) for agent in list(self._prev_actions): if terminations.get(agent, False) or truncations.get(agent, False): del self._prev_actions[agent] return observations, rewards, terminations, truncations, infos @override def __str__(self) -> str: return f"StickyActionParallelV1<{self.env!s}>"