Skip to content

Misc Wrappers

API Reference

masa.common.wrappers.RewardShapingWrapper

RewardShapingWrapper(env: Env, gamma: float = 0.99, impl: str = 'none')

Bases: ConstraintPersistentWrapper

Potential-based reward shaping wrapper for DFA-based safety constraints.

If the wrapped environment's constraint exposes a DFACostFn, this wrapper constructs a shaped cost function ShapedCostFn and updates the step cost entry inside info["constraint"]["step"] using:

\[ c'_t \;=\; c_t \;+\; \gamma \Phi(q_{t+1}) \;-\; \Phi(q_t). \]

The potential \(\Phi\) depends on impl:

  • "none": \(\Phi(q)=0\) (no shaping)
  • "vi": approximate value iteration over DFA graph to derive potentials
  • "cycle": graph-distance based shaping using a reverse-reachability BFS
Notes

This wrapper assumes the wrapped environment is already producing info["automaton_state"] and a constraint monitor-like structure info["constraint"]["step"]["cost"]. If these keys are absent, the wrapper will fall back to default values (state 0 and cost 0.0).

Parameters:

Name Type Description Default
env Env

Base environment to wrap.

required
gamma float

Discount used in the shaping term \(\gamma \Phi(q_{t+1})\).

0.99
impl str

Shaping implementation. One of {"none", "vi", "cycle"}.

'none'

Attributes:

Name Type Description
shaped_cost_fn

The cost function exposed by cost_fn after shaping.

potential_fn

Callable \(\Phi(q)\) mapping DFA states to potentials.

_last_potential

Potential at the previous step's DFA state.

_gamma

Shaping discount factor.

_impl

Shaping implementation identifier.

Source code in masa/common/wrappers.py
def __init__(self, env: gym.Env, gamma: float = 0.99, impl: str = "none"):
    super().__init__(env)
    self._last_potential = 0.0
    self._gamma = gamma
    self._impl = impl

    self._setup_potential_fn()
    self._setup_cost_fn()

_last_potential instance-attribute

_last_potential = 0.0

_gamma instance-attribute

_gamma = gamma

_impl instance-attribute

_impl = impl

cost_fn property

cost_fn

Expose the shaped cost function.

Returns:

Type Description

The shaped cost function constructed in _setup_cost_fn.

_setup_cost_fn

_setup_cost_fn()

Create shaped_cost_fn if DFA-based constraints are available.

If the underlying constraint exposes a DFACostFn, constructs a ShapedCostFn. Otherwise, uses a trivial zero-cost function.

Returns:

Type Description

None. This method sets shaped_cost_fn as a side effect.

Source code in masa/common/wrappers.py
def _setup_cost_fn(self):
    """
    Create :attr:`shaped_cost_fn` if DFA-based constraints are available.

    If the underlying constraint exposes a :class:`~masa.common.ltl.DFACostFn`,
    constructs a :class:`~masa.common.ltl.ShapedCostFn`. Otherwise, uses a
    trivial zero-cost function.

    Returns:
        ``None``. This method sets :attr:`shaped_cost_fn` as a side effect.
    """
    if hasattr(self._constraint, "cost_fn") and isinstance(self._constraint.cost_fn, DFACostFn):
        self.shaped_cost_fn = ShapedCostFn(self._constraint.cost_fn.dfa, self.potential_fn, gamma=self._gamma)
    else:
        self.shaped_cost_fn = lambda q: 0.0

_setup_potential_fn

_setup_potential_fn()

Construct the potential function potential_fn for shaping.

For impl="none", potential_fn is identically zero.

For impl="vi", a small fixed number of value-iteration steps are run over DFA states, treating accepting states as having a constant terminal value and propagating backward through reachable transitions.

For impl="cycle", a reverse-graph BFS is used to find a "furthest" state from the accepting set and then compute distances to that target, yielding a shaping potential based on distance.

Returns:

Type Description

None. This method sets potential_fn and may store

intermediate tables such as V or dist_to_furthest.

Raises:

Type Description
AssertionError

If impl != "none" but no DFA-based cost function

Source code in masa/common/wrappers.py
def _setup_potential_fn(self):
    """
    Construct the potential function :attr:`potential_fn` for shaping.

    For ``impl="none"``, :attr:`potential_fn` is identically zero.

    For ``impl="vi"``, a small fixed number of value-iteration steps are run
    over DFA states, treating accepting states as having a constant terminal
    value and propagating backward through reachable transitions.

    For ``impl="cycle"``, a reverse-graph BFS is used to find a "furthest"
    state from the accepting set and then compute distances to that target,
    yielding a shaping potential based on distance.

    Returns:
        ``None``. This method sets :attr:`potential_fn` and may store
        intermediate tables such as :attr:`V` or :attr:`dist_to_furthest`.

    Raises:
        AssertionError: If ``impl != "none"`` but no DFA-based cost function
        is available on the underlying constraint.
    """

    if self._impl != "none":
        assert hasattr(self._constraint, "cost_fn"), \
        ("RewardShapingWrapper requires env to implement a BaseConstraintEnv that exposes a cost_fn")
        assert isinstance(getattr(self._constraint, "cost_fn", None), DFACostFn), \
        ("RewardShapingWrapper requires env to implement a LTLSafetyEnv with cost_fn class: DFACostFn")

        dfa: DFA = self.env._constraint.cost_fn.dfa
    else:
        self.potential_fn = lambda q: 0.0

    if self._impl == "vi":

        VI_STEPS = 100
        GAMMA = 0.9
        self.V = {q: 0.0 for q in dfa.states}
        assert GAMMA <= self._gamma

        print("Reward shaping DFA ...")
        for i in tqdm(range(VI_STEPS)):
            diff = 0.0
            for u in dfa.states:
                V_u = self.V[u]
                self.V[u] = 1.0/(1.0 - GAMMA) if u in dfa.accepting else \
                    np.max([GAMMA * self.V[v] for v in dfa.edges[u].keys()])
                diff = max(diff, np.abs(V_u - self.V[u]))
            if diff < 1e-6:
                break

        self.potential_fn = lambda q: self.V[q]

    if self._impl == "cycle":

        print("Reward shaping DFA ...")
        edges_rev = {v: set() for v in dfa.states}
        for u in dfa.states:
            if u in dfa.edges:
                reachable_states = set(dfa.edges[u].keys())
                for v in reachable_states:
                    edges_rev[v].add(u)

        dist_to_accepting = {u: np.inf for u in dfa.states}
        queue = deque()

        for a in dfa.accepting:
            dist_to_accepting[a] = 0.0
            queue.append(a)

        max_dist = -1
        furthest_state = None

        while queue:
            current = queue.popleft()
            current_dist = dist_to_accepting[current]

            if current_dist > max_dist:
                max_dist = current_dist
                furthest_state = current

            for w in edges_rev.get(current, []):
                if dist_to_accepting[w] == np.inf:
                    dist_to_accepting[w] = current_dist + 1
                    queue.append(w)

        if furthest_state is None:
            furthest_state = dfa.initial

        self.dist_to_furthest = {u: np.inf for u in dfa.states}
        max_finite_dist = 0.0

        if furthest_state is not None:
            u_target = furthest_state
            self.dist_to_furthest[u_target] = 0.0
            queue = deque([u_target])

            while queue:
                current = queue.popleft()
                current_dist = self.dist_to_furthest[current]

                if current_dist > max_finite_dist:
                    max_finite_dist = current_dist

                for w in edges_rev.get(current, []):
                    if self.dist_to_furthest[w] == np.inf:
                        self.dist_to_furthest[w] = current_dist + 1
                        queue.append(w)

        replacement_value = max_finite_dist + 1.0

        for u in dfa.states:
            if self.dist_to_furthest[u] == np.inf:
                self.dist_to_furthest[u] = replacement_value

        self.potential_fn = lambda q: self.dist_to_furthest[q]

reset

reset(*, seed: int | None = None, options: Dict[str, Any] | None = None)

Reset the environment and initialize shaping state.

Parameters:

Name Type Description Default
seed int | None

Random seed forwarded to the underlying environment.

None
options Dict[str, Any] | None

Reset options forwarded to the underlying environment.

None

Returns:

Type Description

A tuple (obs, info) from the underlying environment.

Notes

This wrapper reads info["automaton_state"] to initialize the previous potential _last_potential. If the key is missing, it assumes DFA state 0.

Source code in masa/common/wrappers.py
def reset(self, *, seed: int | None = None, options: Dict[str, Any] | None = None):
    """
    Reset the environment and initialize shaping state.

    Args:
        seed: Random seed forwarded to the underlying environment.
        options: Reset options forwarded to the underlying environment.

    Returns:
        A tuple ``(obs, info)`` from the underlying environment.

    Notes:
        This wrapper reads ``info["automaton_state"]`` to initialize the
        previous potential :attr:`_last_potential`. If the key is missing,
        it assumes DFA state ``0``.
    """
    obs, info = self.env.reset(seed=seed, options=options)
    automaton_state = info.get("automaton_state", 0)
    self._last_potential = self.potential_fn(automaton_state)
    return obs, info

step

step(action: Any)

Step the environment and apply potential-based shaping to the step cost.

Parameters:

Name Type Description Default
action Any

Action forwarded to the underlying environment.

required

Returns:

Type Description

A 5-tuple (observation, reward, terminated, truncated, info).

Side effects

Updates info["constraint"]["step"]["cost"] in-place with the shaped cost and updates _last_potential.

Notes

If the underlying info does not contain constraint metrics, this method assumes an unshaped step cost of 0.0 and will still attempt to write back into info["constraint"]["step"].

Source code in masa/common/wrappers.py
def step(self, action: Any):
    """
    Step the environment and apply potential-based shaping to the step cost.

    Args:
        action: Action forwarded to the underlying environment.

    Returns:
        A 5-tuple ``(observation, reward, terminated, truncated, info)``.

    Side effects:
        Updates ``info["constraint"]["step"]["cost"]`` in-place with the shaped
        cost and updates :attr:`_last_potential`.

    Notes:
        If the underlying ``info`` does not contain constraint metrics,
        this method assumes an unshaped step cost of ``0.0`` and will
        still attempt to write back into ``info["constraint"]["step"]``.
    """
    observation, reward, terminated, truncated, info = self.env.step(action)
    cost = info["constraint"]["step"].get("cost", 0.0)
    automaton_state = info.get("automaton_state", 0)
    potential = self.potential_fn(automaton_state)
    info["constraint"]["step"]["cost"] = cost + self._gamma * potential - self._last_potential
    self._last_potential = potential
    return observation, reward, terminated, truncated, info

masa.common.wrappers.NormWrapper

NormWrapper(env: Env, norm_obs: bool = True, norm_rew: bool = True, training: bool = True, clip_obs: float = 10.0, clip_rew: float = 10.0, gamma: float = 0.99, eps: float = 1e-08)

Bases: ConstraintPersistentWrapper

Normalize observations and/or rewards for a single (non-vectorized) environment.

This wrapper maintains running mean/variance estimates and applies:

  • Observation normalization (elementwise): \((x - \mu) / \sqrt{\sigma^2 + \varepsilon}\)
  • Reward normalization using a running variance estimate over discounted returns.

This wrapper is intended for non-vectorized environments. For vectorized environments, use VecNormWrapper.

Parameters:

Name Type Description Default
env Env

Base (non-vectorized) environment.

required
norm_obs bool

Whether to normalize observations.

True
norm_rew bool

Whether to normalize rewards.

True
training bool

If True, update running statistics; otherwise, statistics are frozen.

True
clip_obs float

Clip normalized observations to [-clip_obs, clip_obs].

10.0
clip_rew float

Clip normalized rewards to [-clip_rew, clip_rew].

10.0
gamma float

Discount factor for the running return used in reward normalization.

0.99
eps float

Small constant \(\varepsilon\) for numerical stability.

1e-08

Attributes:

Name Type Description
norm_obs

See Args.

norm_rew

See Args.

training

See Args.

clip_obs

See Args.

clip_rew

See Args.

gamma

See Args.

eps

See Args.

obs_rms

masa.common.running_mean_std.RunningMeanStd for observations.

rew_rms

masa.common.running_mean_std.RunningMeanStd for returns.

returns

Discounted return accumulator used for reward normalization.

Source code in masa/common/wrappers.py
def __init__(
    self, 
    env: gym.Env, 
    norm_obs: bool = True,
    norm_rew: bool = True,
    training: bool = True,
    clip_obs: float = 10.0,
    clip_rew: float = 10.0,
    gamma: float = 0.99,
    eps: float = 1e-8
):
    assert not isinstance(
        env, VecEnvWrapperBase
    ), "NormWrapper does not expect a vectorized environment (DummyVecWrapper / VecWrapper). Please use VecNormWrapper instead"

    assert norm_obs and isinstance(
        env.observation_space, spaces.Box
    ), "NormWrapper only supports Box observation spaces when norm_obs=True."

    super().__init__(env)

    self.norm_obs = norm_obs
    self.norm_rew = norm_rew
    self.training = training
    self.clip_obs = clip_obs
    self.clip_rew = clip_rew
    self.gamma = gamma
    self.eps = eps

    self.obs_rms = RunningMeanStd(shape=self.observation_space.shape)
    self.rew_rms = RunningMeanStd(shape=())

    self.returns = np.zeros(1, dtype=np.float32)

norm_obs instance-attribute

norm_obs = norm_obs

norm_rew instance-attribute

norm_rew = norm_rew

training instance-attribute

training = training

clip_obs instance-attribute

clip_obs = clip_obs

clip_rew instance-attribute

clip_rew = clip_rew

gamma instance-attribute

gamma = gamma

eps instance-attribute

eps = eps

obs_rms instance-attribute

obs_rms = RunningMeanStd(shape=self.observation_space.shape)

rew_rms instance-attribute

rew_rms = RunningMeanStd(shape=())

returns instance-attribute

returns = np.zeros(1, dtype=np.float32)

_normalize_obs

_normalize_obs(obs: ndarray) -> np.ndarray

Normalize (and clip) a single observation.

Parameters:

Name Type Description Default
obs ndarray

Raw observation.

required

Returns:

Type Description
ndarray

Normalized observation.

Source code in masa/common/wrappers.py
def _normalize_obs(self, obs: np.ndarray) -> np.ndarray:
    """
    Normalize (and clip) a single observation.

    Args:
        obs: Raw observation.

    Returns:
        Normalized observation.
    """
    return np.clip(
        (obs - self.obs_rms.mean) / np.sqrt(self.obs_rms.var + self.eps),
        -self.clip_obs,
        self.clip_obs
    )

_normalize_rew

_normalize_rew(rew: float) -> float

Normalize (and clip) a single reward.

Parameters:

Name Type Description Default
rew float

Raw reward.

required

Returns:

Type Description
float

Normalized reward.

Source code in masa/common/wrappers.py
def _normalize_rew(self, rew: float) -> float:
    """
    Normalize (and clip) a single reward.

    Args:
        rew: Raw reward.

    Returns:
        Normalized reward.
    """
    return np.clip(
        rew / np.sqrt(self.rew_rms.var + self.eps),
        -self.clip_rew,
        self.clip_rew,
    )

reset

reset(*, seed: int | None = None, options: Dict[str, Any] | None = None)

Reset the environment and (optionally) update normalization statistics.

Parameters:

Name Type Description Default
seed int | None

Random seed forwarded to the underlying environment.

None
options Dict[str, Any] | None

Reset options forwarded to the underlying environment.

None

Returns:

Type Description

A tuple (obs, info) where obs may be normalized.

Source code in masa/common/wrappers.py
def reset(self, *, seed: int | None = None, options: Dict[str, Any] | None = None):
    """
    Reset the environment and (optionally) update normalization statistics.

    Args:
        seed: Random seed forwarded to the underlying environment.
        options: Reset options forwarded to the underlying environment.

    Returns:
        A tuple ``(obs, info)`` where ``obs`` may be normalized.
    """
    obs, info = self.env.reset(seed=seed, options=options)

    if self.norm_obs and self.training:
        self.obs_rms.update(obs)

    if self.norm_rew and self.training:
        self.returns[:] = 0.0

    if self.norm_obs:
        obs = self._normalize_obs(obs)

    return obs, info

step

step(action)

Step the environment and apply observation/reward normalization.

Parameters:

Name Type Description Default
action

Action forwarded to the underlying environment.

required

Returns:

Type Description

A 5-tuple (obs, rew, terminated, truncated, info) where obs and/or

rew may be normalized.

Source code in masa/common/wrappers.py
def step(self, action):
    """
    Step the environment and apply observation/reward normalization.

    Args:
        action: Action forwarded to the underlying environment.

    Returns:
        A 5-tuple ``(obs, rew, terminated, truncated, info)`` where ``obs`` and/or
        ``rew`` may be normalized.
    """
    obs, rew, term, trunc, info = self.env.step(action)

    if self.norm_obs and self.training:
        self.obs_rms.update(obs)

    if self.norm_rew:
        self.returns = self.returns * self.gamma + rew
        if self.training:
            self.rew_rms.update(self.returns)

        rew = self._normalize_rew(rew)

    if self.norm_obs:
        obs = self._normalize_obs(obs)

    return obs, rew, term, trunc, info

masa.common.wrappers.OneHotObsWrapper

OneHotObsWrapper(env: Env)

Bases: ConstraintPersistentObsWrapper

One-hot encode gymnasium.spaces.Discrete observations.

Supported input observation spaces:

  • gymnasium.spaces.Discrete: returns a 1D one-hot vector of length n.
  • gymnasium.spaces.Dict: one-hot encodes any Discrete subspaces and passes through non-Discrete subspaces.
  • Otherwise: passes observations through unchanged.

The wrapper updates gymnasium.Env.observation_space accordingly.

Parameters:

Name Type Description Default
env Env

Base environment to wrap.

required

Attributes:

Name Type Description
_orig_obs_space

The original observation space of the wrapped env.

_mode

One of {"discrete", "dict", "pass"} describing the encoding mode.

Source code in masa/common/wrappers.py
def __init__(self, env: gym.Env):
    super().__init__(env)

    self._orig_obs_space = self.env.observation_space

    if isinstance(self._orig_obs_space, spaces.Discrete):
        self._mode = "discrete"
        n = self._orig_obs_space.n
        self.observation_space = spaces.Box(
            low=0.0,
            high=1.0,
            shape=(n,),
            dtype=np.float32,
        )


    elif isinstance(self._orig_obs_space, spaces.Dict):
        self._mode = "dict"

        new_spaces: Dict[str, spaces.Space] = {}
        for key, subspace in self._orig_obs_space.spaces.items():
            if isinstance(subspace, spaces.Discrete):
                n = subspace.n
                new_spaces[key] = spaces.Box(
                    low=0.0,
                    high=1.0,
                    shape=(n,),
                    dtype=np.float32,
                )
            else:
                # Preserve non-Discrete subspace as-is
                new_spaces[key] = subspace

        self.observation_space = spaces.Dict(new_spaces)

    else:
        self._mode = "pass"
        self.observation_space = self._orig_obs_space

_orig_obs_space instance-attribute

_orig_obs_space = self.env.observation_space

_mode instance-attribute

_mode = 'discrete'

observation_space instance-attribute

observation_space = spaces.Box(low=0.0, high=1.0, shape=(n,), dtype=np.float32)

_one_hot_scalar staticmethod

_one_hot_scalar(idx: int, n: int) -> np.ndarray

One-hot encode an integer index.

Parameters:

Name Type Description Default
idx int

Index in {0, 1, ..., n-1}.

required
n int

Vector length.

required

Returns:

Type Description
ndarray

A float32 vector v with v[idx] = 1 and zeros elsewhere.

Source code in masa/common/wrappers.py
@staticmethod
def _one_hot_scalar(idx: int, n: int) -> np.ndarray:
    """
    One-hot encode an integer index.

    Args:
        idx: Index in ``{0, 1, ..., n-1}``.
        n: Vector length.

    Returns:
        A float32 vector ``v`` with ``v[idx] = 1`` and zeros elsewhere.
    """
    one_hot = np.zeros(n, dtype=np.float32)
    one_hot[idx] = 1.0
    return one_hot

_get_obs

_get_obs(obs: Union[int, Dict[str, Any], ndarray]) -> np.ndarray

Transform an observation according to the wrapper's configured mode.

Parameters:

Name Type Description Default
obs Union[int, Dict[str, Any], ndarray]

Raw observation.

required

Returns:

Type Description
ndarray

One-hot encoded observation (or dict containing one-hot fields) when applicable,

ndarray

otherwise the original observation.

Source code in masa/common/wrappers.py
def _get_obs(self, obs: Union[int, Dict[str, Any], np.ndarray]) -> np.ndarray:
    """
    Transform an observation according to the wrapper's configured mode.

    Args:
        obs: Raw observation.

    Returns:
        One-hot encoded observation (or dict containing one-hot fields) when applicable,
        otherwise the original observation.
    """
    if self._mode == "discrete":
        # Original obs_space is Discrete; obs is an int-like
        idx = int(obs)
        n = self._orig_obs_space.n
        return self._one_hot_scalar(idx, n)

    elif self._mode == "dict":
        assert isinstance(obs, dict), (
            f"Expected dict observation for Dict space, got {type(obs)}"
        )

        new_obs: Dict[str, Any] = {}
        for key, subspace in self._orig_obs_space.spaces.items():
            value = obs[key]

            if isinstance(subspace, spaces.Discrete):
                idx = int(value)
                new_obs[key] = self._one_hot_scalar(idx, subspace.n)
            else:
                # Leave non-Discrete parts unchanged
                new_obs[key] = value

        return new_obs
    else: 
        # pass
        return obs

masa.common.wrappers.FlattenDictObsWrapper

FlattenDictObsWrapper(env: Env)

Bases: ConstraintPersistentObsWrapper

Flatten a gymnasium.spaces.Dict observation into a 1D Box.

The wrapper creates a deterministic key ordering (alphabetical) and concatenates each sub-observation in that order.

Supported Dict subspaces:

  • gymnasium.spaces.Box: flattened via reshape(-1).
  • gymnasium.spaces.Discrete: represented as a length-n one-hot segment for the purposes of bounds (note: the current implementation of _get_obs expects Box values; see Notes).

Parameters:

Name Type Description Default
env Env

Base environment with Dict observation space.

required

Attributes:

Name Type Description
_orig_obs_space

Original Dict observation space.

_key_slices dict[str, slice]

Mapping from key to slice in the flattened vector.

Raises:

Type Description
TypeError

If the underlying observation space is not a Dict, or contains unsupported subspaces.

Source code in masa/common/wrappers.py
def __init__(self, env: gym.Env):
    super().__init__(env)

    self._orig_obs_space = self.env.observation_space

    if not isinstance(self._orig_obs_space, spaces.Dict):
        raise TypeError(
            f"FlattenDictObsWrapper requires Dict observation space, got {type(self._orig_obs_space)}"
        )

    # To be able to reconstruct if needed, keep slices for each key
    self._key_slices: dict[str, slice] = {}

    low_parts = []
    high_parts = []
    offset = 0

    # Sort keys alphabetically for deterministic ordering
    for key in sorted(self._orig_obs_space.spaces.keys()):
        subspace = self._orig_obs_space.spaces[key]

        if isinstance(subspace, spaces.Box):
            # Flatten Box
            low = np.asarray(subspace.low, dtype=np.float32).reshape(-1)
            high = np.asarray(subspace.high, dtype=np.float32).reshape(-1)
            length = low.shape[0]

            low_parts.append(low)
            high_parts.append(high)

        elif isinstance(subspace, spaces.Discrete):
            # One-hot will be in [0, 1]
            length = subspace.n
            low_parts.append(np.zeros(length, dtype=np.float32))
            high_parts.append(np.ones(length, dtype=np.float32))

        else:
            raise TypeError(
                f"Unsupported subspace type for key '{key}': {type(subspace)}"
            )

        self._key_slices[key] = slice(offset, offset + length)
        offset += length

    low = np.concatenate(low_parts).astype(np.float32)
    high = np.concatenate(high_parts).astype(np.float32)

    self.observation_space = spaces.Box(
        low=low,
        high=high,
        dtype=np.float32,
    )

_orig_obs_space instance-attribute

_orig_obs_space = self.env.observation_space

_key_slices instance-attribute

_key_slices: dict[str, slice] = {}

observation_space instance-attribute

observation_space = spaces.Box(low=low, high=high, dtype=np.float32)

_get_obs

_get_obs(obs: Dict[str, Any]) -> np.ndarray

Flatten a Dict observation into a 1D vector.

Parameters:

Name Type Description Default
obs Dict[str, Any]

Dict observation keyed the same way as the original Dict space.

required

Returns:

Type Description
ndarray

A 1D float32 array created by concatenating flattened sub-observations.

Raises:

Type Description
TypeError

If any subspace is not a Box.

Notes

Although the constructor supports Discrete subspaces when building bounds, this implementation currently enforces Box-only subspaces at runtime.

Source code in masa/common/wrappers.py
def _get_obs(self, obs: Dict[str, Any]) -> np.ndarray:
    """
    Flatten a Dict observation into a 1D vector.

    Args:
        obs: Dict observation keyed the same way as the original Dict space.

    Returns:
        A 1D float32 array created by concatenating flattened sub-observations.

    Raises:
        TypeError: If any subspace is not a Box.

    Notes:
        Although the constructor supports Discrete subspaces when building bounds,
        this implementation currently enforces Box-only subspaces at runtime.
    """
    assert isinstance(obs, dict), (
            f"Expected dict observation for Dict space, got {type(obs)}"
        )

    parts = []
    for key in sorted(self._orig_obs_space.spaces.keys()):
        subspace = self._orig_obs_space.spaces[key]
        value = obs[key]

        if not isinstance(subspace, spaces.Box):
            raise TypeError(
                f"FlattenDictObsWrapper only supports Box subspaces, "
                f"got {type(subspace)} for key '{key}'"
            )

        arr = np.asarray(value, dtype=np.float32).reshape(-1)
        parts.append(arr)

    return np.concatenate(parts).astype(np.float32)

masa.common.pettingzoo_record_video.RecordVideoParallel

RecordVideoParallel(env: ParallelEnv, video_folder: str, episode_trigger: Callable[[int], bool] | None = None, step_trigger: Callable[[int], bool] | None = None, video_length: int = 0, name_prefix: str = 'rl-video', fps: int | None = None, disable_logger: bool = True, gc_trigger: Callable[[int], bool] | None = lambda episode: True)

Bases: BaseParallelWrapper

Record videos from a PettingZoo parallel environment.

Source code in masa/common/pettingzoo_record_video.py
def __init__(
    self,
    env: ParallelEnv,
    video_folder: str,
    episode_trigger: Callable[[int], bool] | None = None,
    step_trigger: Callable[[int], bool] | None = None,
    video_length: int = 0,
    name_prefix: str = "rl-video",
    fps: int | None = None,
    disable_logger: bool = True,
    gc_trigger: Callable[[int], bool] | None = lambda episode: True,
):
    super().__init__(env)
    assert isinstance(env, ParallelEnv), "RecordVideoParallel is only compatible with ParallelEnv environments."

    if env.render_mode in {None, "human", "ansi"}:  # type: ignore[attr-defined]
        raise ValueError(
            f"Render mode is {env.render_mode}, which is incompatible with RecordVideoParallel. "  # type: ignore[attr-defined]
            "Initialize your environment with a render_mode that returns an image, such as rgb_array."
        )

    if episode_trigger is None and step_trigger is None:
        episode_trigger = (
            lambda episode_id: int(round(episode_id ** (1.0 / 3))) ** 3 == episode_id
            if episode_id < 1000
            else episode_id % 1000 == 0
        )

    self.episode_trigger = episode_trigger
    self.step_trigger = step_trigger
    self.disable_logger = disable_logger
    self.gc_trigger = gc_trigger

    self.video_folder = os.path.abspath(video_folder)
    if os.path.isdir(self.video_folder):
        gymnasium.logger.warn(
            f"Overwriting existing videos at {self.video_folder} folder "
            "(try specifying a different `video_folder` for the `RecordVideoParallel` wrapper if this is not desired)"
        )
    os.makedirs(self.video_folder, exist_ok=True)

    if fps is None:
        fps = int(getattr(env, "metadata", {}).get("render_fps", 30))
    self.frames_per_sec: int = fps
    self.name_prefix: str = name_prefix
    self._video_name: str | None = None
    self.video_length: int | float = video_length if video_length != 0 else float("inf")
    self.recording: bool = False
    self.recorded_frames: list[RenderFrame] = []
    self.render_history: list[RenderFrame] = []

    self.step_id: int = -1
    self.episode_id: int = -1

    try:
        import moviepy  # noqa: F401
    except ImportError as e:
        raise DependencyNotInstalled(
            'MoviePy is not installed, run `pip install "moviepy>=2.2.1,<3.0.0"`'
        ) from e

episode_trigger instance-attribute

episode_trigger = episode_trigger

step_trigger instance-attribute

step_trigger = step_trigger

disable_logger instance-attribute

disable_logger = disable_logger

gc_trigger instance-attribute

gc_trigger = gc_trigger

video_folder instance-attribute

video_folder = os.path.abspath(video_folder)

frames_per_sec instance-attribute

frames_per_sec: int = fps

name_prefix instance-attribute

name_prefix: str = name_prefix

_video_name instance-attribute

_video_name: str | None = None

video_length instance-attribute

video_length: int | float = video_length if video_length != 0 else float('inf')

recording instance-attribute

recording: bool = False

recorded_frames instance-attribute

recorded_frames: list[RenderFrame] = []

render_history instance-attribute

render_history: list[RenderFrame] = []

step_id instance-attribute

step_id: int = -1

episode_id instance-attribute

episode_id: int = -1

_capture_frame

_capture_frame()
Source code in masa/common/pettingzoo_record_video.py
def _capture_frame(self):
    assert self.recording, "Cannot capture a frame, recording wasn't started."

    frame = self.env.render()
    if isinstance(frame, list):
        if len(frame) == 0:
            return
        self.render_history += frame
        frame = frame[-1]

    if isinstance(frame, np.ndarray):
        self.recorded_frames.append(frame)
    else:
        self.stop_recording()
        gymnasium.logger.warn(
            f"Recording stopped: expected type of frame returned by render to be a numpy array, got {type(frame)}."
        )

reset

reset(seed: int | None = None, options: dict | None = None) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]
Source code in masa/common/pettingzoo_record_video.py
def reset(
    self, seed: int | None = None, options: dict | None = None
) -> tuple[dict[AgentID, ObsType], dict[AgentID, dict]]:
    obs, info = self.env.reset(seed=seed, options=options)
    self.episode_id += 1

    if self.recording and self.video_length == float("inf"):
        self.stop_recording()

    if self.episode_trigger and self.episode_trigger(self.episode_id):
        self.start_recording(f"{self.name_prefix}-episode-{self.episode_id}")
    if self.recording:
        self._capture_frame()
        if len(self.recorded_frames) > self.video_length:
            self.stop_recording()

    return obs, info

step

step(actions: dict[AgentID, ActionType]) -> tuple[dict[AgentID, ObsType], dict[AgentID, float], dict[AgentID, bool], dict[AgentID, bool], dict[AgentID, dict]]
Source code in masa/common/pettingzoo_record_video.py
def step(
    self, actions: dict[AgentID, ActionType]
) -> tuple[
    dict[AgentID, ObsType],
    dict[AgentID, float],
    dict[AgentID, bool],
    dict[AgentID, bool],
    dict[AgentID, dict],
]:
    obs, rew, terminated, truncated, info = self.env.step(actions)
    self.step_id += 1

    if self.step_trigger and self.step_trigger(self.step_id):
        self.start_recording(f"{self.name_prefix}-step-{self.step_id}")
    if self.recording:
        self._capture_frame()
        if len(self.recorded_frames) > self.video_length:
            self.stop_recording()

    return obs, rew, terminated, truncated, info

render

render()
Source code in masa/common/pettingzoo_record_video.py
def render(self):
    render_out = self.env.render()
    if self.recording and isinstance(render_out, list):
        self.recorded_frames += render_out

    if len(self.render_history) > 0:
        tmp_history = self.render_history
        self.render_history = []
        frames = render_out if isinstance(render_out, list) else [render_out]
        return tmp_history + frames
    return render_out

close

close()
Source code in masa/common/pettingzoo_record_video.py
def close(self):
    super().close()
    if self.recording:
        self.stop_recording()

start_recording

start_recording(video_name: str)
Source code in masa/common/pettingzoo_record_video.py
def start_recording(self, video_name: str):
    if self.recording:
        self.stop_recording()

    self.recording = True
    self._video_name = video_name

stop_recording

stop_recording()
Source code in masa/common/pettingzoo_record_video.py
def stop_recording(self):
    assert self.recording, "stop_recording was called, but no recording was started"

    if len(self.recorded_frames) == 0:
        gymnasium.logger.warn("Ignored saving a video as there were zero frames to save.")
    else:
        try:
            from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
        except ImportError as e:
            raise DependencyNotInstalled(
                'MoviePy is not installed, run `pip install "moviepy>=2.2.1,<3.0.0"`'
            ) from e

        clip = ImageSequenceClip(self.recorded_frames, fps=self.frames_per_sec)
        moviepy_logger = None if self.disable_logger else "bar"
        path = os.path.join(self.video_folder, f"{self._video_name}.mp4")
        clip.write_videofile(path, logger=moviepy_logger)

    self.recorded_frames = []
    self.recording = False
    self._video_name = None

    if self.gc_trigger and self.gc_trigger(self.episode_id):
        gc.collect()

__del__

__del__()
Source code in masa/common/pettingzoo_record_video.py
def __del__(self):
    if len(getattr(self, "recorded_frames", [])) > 0:
        gymnasium.logger.warn("Unable to save last video! Did you call close()?")

masa.common.pettingzoo_record_video.RecordVideoAEC

RecordVideoAEC(env: AECEnv, video_folder: str, episode_trigger: Callable[[int], bool] | None = None, step_trigger: Callable[[int], bool] | None = None, video_length: int = 0, name_prefix: str = 'rl-video', fps: int | None = None, disable_logger: bool = True, gc_trigger: Callable[[int], bool] | None = lambda episode: True)

Bases: BaseWrapper

Record videos from a PettingZoo AEC environment.

Source code in masa/common/pettingzoo_record_video.py
def __init__(
    self,
    env: AECEnv,
    video_folder: str,
    episode_trigger: Callable[[int], bool] | None = None,
    step_trigger: Callable[[int], bool] | None = None,
    video_length: int = 0,
    name_prefix: str = "rl-video",
    fps: int | None = None,
    disable_logger: bool = True,
    gc_trigger: Callable[[int], bool] | None = lambda episode: True,
):
    super().__init__(env)
    assert isinstance(env, AECEnv), "RecordVideoAEC is only compatible with AECEnv environments."

    if env.render_mode in {None, "human", "ansi"}:  # type: ignore[attr-defined]
        raise ValueError(
            f"Render mode is {env.render_mode}, which is incompatible with RecordVideoAEC. "  # type: ignore[attr-defined]
            "Initialize your environment with a render_mode that returns an image, such as rgb_array."
        )

    if episode_trigger is None and step_trigger is None:
        episode_trigger = (
            lambda episode_id: int(round(episode_id ** (1.0 / 3))) ** 3 == episode_id
            if episode_id < 1000
            else episode_id % 1000 == 0
        )

    self.episode_trigger = episode_trigger
    self.step_trigger = step_trigger
    self.disable_logger = disable_logger
    self.gc_trigger = gc_trigger

    self.video_folder = os.path.abspath(video_folder)
    if os.path.isdir(self.video_folder):
        gymnasium.logger.warn(
            f"Overwriting existing videos at {self.video_folder} folder "
            "(try specifying a different `video_folder` for the `RecordVideoAEC` wrapper if this is not desired)"
        )
    os.makedirs(self.video_folder, exist_ok=True)

    if fps is None:
        fps = int(getattr(env, "metadata", {}).get("render_fps", 30))
    self.frames_per_sec: int = fps
    self.name_prefix: str = name_prefix
    self._video_name: str | None = None
    self.video_length: int | float = video_length if video_length != 0 else float("inf")
    self.recording: bool = False
    self.recorded_frames: list[RenderFrame] = []
    self.render_history: list[RenderFrame] = []

    self.step_id: int = -1
    self.episode_id: int = -1

    try:
        import moviepy  # noqa: F401
    except ImportError as e:
        raise DependencyNotInstalled(
            'MoviePy is not installed, run `pip install "moviepy>=2.2.1,<3.0.0"`'
        ) from e

episode_trigger instance-attribute

episode_trigger = episode_trigger

step_trigger instance-attribute

step_trigger = step_trigger

disable_logger instance-attribute

disable_logger = disable_logger

gc_trigger instance-attribute

gc_trigger = gc_trigger

video_folder instance-attribute

video_folder = os.path.abspath(video_folder)

frames_per_sec instance-attribute

frames_per_sec: int = fps

name_prefix instance-attribute

name_prefix: str = name_prefix

_video_name instance-attribute

_video_name: str | None = None

video_length instance-attribute

video_length: int | float = video_length if video_length != 0 else float('inf')

recording instance-attribute

recording: bool = False

recorded_frames instance-attribute

recorded_frames: list[RenderFrame] = []

render_history instance-attribute

render_history: list[RenderFrame] = []

step_id instance-attribute

step_id: int = -1

episode_id instance-attribute

episode_id: int = -1

_capture_frame

_capture_frame()
Source code in masa/common/pettingzoo_record_video.py
def _capture_frame(self):
    assert self.recording, "Cannot capture a frame, recording wasn't started."

    frame = self.env.render()
    if isinstance(frame, list):
        if len(frame) == 0:
            return
        self.render_history += frame
        frame = frame[-1]

    if isinstance(frame, np.ndarray):
        self.recorded_frames.append(frame)
    else:
        self.stop_recording()
        gymnasium.logger.warn(
            f"Recording stopped: expected type of frame returned by render to be a numpy array, got {type(frame)}."
        )

reset

reset(seed: int | None = None, options: dict | None = None)
Source code in masa/common/pettingzoo_record_video.py
def reset(self, seed: int | None = None, options: dict | None = None):
    self.env.reset(seed=seed, options=options)
    self.episode_id += 1

    if self.recording and self.video_length == float("inf"):
        self.stop_recording()

    if self.episode_trigger and self.episode_trigger(self.episode_id):
        self.start_recording(f"{self.name_prefix}-episode-{self.episode_id}")
    if self.recording:
        self._capture_frame()
        if len(self.recorded_frames) > self.video_length:
            self.stop_recording()

step

step(action: ActionType)
Source code in masa/common/pettingzoo_record_video.py
def step(self, action: ActionType):
    self.env.step(action)
    self.step_id += 1

    if self.step_trigger and self.step_trigger(self.step_id):
        self.start_recording(f"{self.name_prefix}-step-{self.step_id}")
    if self.recording:
        self._capture_frame()
        if len(self.recorded_frames) > self.video_length:
            self.stop_recording()

render

render()
Source code in masa/common/pettingzoo_record_video.py
def render(self):
    render_out = self.env.render()
    if self.recording and isinstance(render_out, list):
        self.recorded_frames += render_out

    if len(self.render_history) > 0:
        tmp_history = self.render_history
        self.render_history = []
        frames = render_out if isinstance(render_out, list) else [render_out]
        return tmp_history + frames
    return render_out

close

close()
Source code in masa/common/pettingzoo_record_video.py
def close(self):
    super().close()
    if self.recording:
        self.stop_recording()

start_recording

start_recording(video_name: str)
Source code in masa/common/pettingzoo_record_video.py
def start_recording(self, video_name: str):
    if self.recording:
        self.stop_recording()

    self.recording = True
    self._video_name = video_name

stop_recording

stop_recording()
Source code in masa/common/pettingzoo_record_video.py
def stop_recording(self):
    assert self.recording, "stop_recording was called, but no recording was started"

    if len(self.recorded_frames) == 0:
        gymnasium.logger.warn("Ignored saving a video as there were zero frames to save.")
    else:
        try:
            from moviepy.video.io.ImageSequenceClip import ImageSequenceClip
        except ImportError as e:
            raise DependencyNotInstalled(
                'MoviePy is not installed, run `pip install "moviepy>=2.2.1,<3.0.0"`'
            ) from e

        clip = ImageSequenceClip(self.recorded_frames, fps=self.frames_per_sec)
        moviepy_logger = None if self.disable_logger else "bar"
        path = os.path.join(self.video_folder, f"{self._video_name}.mp4")
        clip.write_videofile(path, logger=moviepy_logger)

    self.recorded_frames = []
    self.recording = False
    self._video_name = None

    if self.gc_trigger and self.gc_trigger(self.episode_id):
        gc.collect()

__del__

__del__()
Source code in masa/common/pettingzoo_record_video.py
def __del__(self):
    if len(getattr(self, "recorded_frames", [])) > 0:
        gymnasium.logger.warn("Unable to save last video! Did you call close()?")