Bases: ParallelEnv
Parallel environment with an enumerable finite-state transition model.
Subclasses set _n_states and expose either:
_transition_matrix[next_state, state, joint_action_index]; or
_successor_states[state] together with
_transition_probs[state, joint_action_index].
Joint-action indices use the Cartesian-product order induced by
possible_agents and their zero-based discrete action spaces. For example,
two binary agents use (0, 0), (0, 1), (1, 0), (1, 1).
get_state_id() returns the current tabular state. By default this reads
self._state. observations_from_state() reconstructs the per-agent
observations used by LabelledParallelEnv
when synthesising an LTL product. The default implementation supports the
common case where every agent directly observes the same discrete state ID.
The class deliberately does not define rewards, labels, reset, or step. It
only standardises the finite game model needed by planning and shielding.
Source code in masa/envs/multiagent/tabular_env.py
| def __init__(self) -> None:
super().__init__()
self._n_states: int | None = None
self._transition_matrix: np.ndarray | None = None
self._successor_states: Mapping[int, Sequence[int]] | None = None
self._transition_probs: Mapping[tuple[int, int], Sequence[float]] | None = None
self._state: int | None = None
|
_n_states
instance-attribute
_n_states: int | None = None
_transition_matrix
instance-attribute
_transition_matrix: ndarray | None = None
_successor_states
instance-attribute
_successor_states: Mapping[int, Sequence[int]] | None = None
_transition_probs
instance-attribute
_transition_probs: Mapping[tuple[int, int], Sequence[float]] | None = None
_state
instance-attribute
_state: int | None = None
n_states
property
Number of finite model states.
action_sizes
property
action_sizes: dict[str, int]
Primitive action count for each agent in possible_agents order.
n_joint_actions
property
Size of the full Cartesian joint-action space.
has_transition_matrix
property
has_transition_matrix: bool
has_successor_states_dict
property
has_successor_states_dict: bool
get_transition_matrix
get_transition_matrix() -> np.ndarray | None
Return P[next_state, state, joint_action_index], when available.
Source code in masa/envs/multiagent/tabular_env.py
| def get_transition_matrix(self) -> np.ndarray | None:
"""Return ``P[next_state, state, joint_action_index]``, when available."""
return self._transition_matrix
|
get_successor_states_dict
get_successor_states_dict() -> tuple[Mapping[int, Sequence[int]], Mapping[tuple[int, int], Sequence[float]]] | None
Return the sparse state-successor/probability representation.
Probability vectors are aligned with successor_states[state] and
keyed by (state, joint_action_index).
Source code in masa/envs/multiagent/tabular_env.py
| def get_successor_states_dict(
self,
) -> tuple[
Mapping[int, Sequence[int]],
Mapping[tuple[int, int], Sequence[float]],
] | None:
"""Return the sparse state-successor/probability representation.
Probability vectors are aligned with ``successor_states[state]`` and
keyed by ``(state, joint_action_index)``.
"""
if not self.has_successor_states_dict:
return None
assert self._successor_states is not None
assert self._transition_probs is not None
return self._successor_states, self._transition_probs
|
_check_state
_check_state(state: int) -> int
Source code in masa/envs/multiagent/tabular_env.py
| def _check_state(self, state: int) -> int:
if not isinstance(state, Integral) or isinstance(state, bool):
raise TypeError(f"State ID must be an integer, got {state!r}.")
state = int(state)
if not 0 <= state < self.n_states:
raise ValueError(
f"State ID {state} is outside [0, {self.n_states})."
)
return state
|
_joint_action_tuple
_joint_action_tuple(actions: Mapping[str, int] | Sequence[int]) -> JointAction
Source code in masa/envs/multiagent/tabular_env.py
| def _joint_action_tuple(
self, actions: Mapping[str, int] | Sequence[int]
) -> JointAction:
agents = tuple(self.possible_agents)
if isinstance(actions, Mapping):
supplied = set(actions)
expected = set(agents)
if supplied != expected:
raise ValueError(
"Joint action must contain exactly possible_agents; "
f"missing={sorted(expected - supplied)}, "
f"extra={sorted(supplied - expected)}."
)
raw = tuple(actions[agent] for agent in agents)
else:
if isinstance(actions, (str, bytes)):
raise TypeError("Joint action must be a mapping or action sequence.")
raw = tuple(actions)
if len(raw) != len(agents):
raise ValueError(
f"Expected {len(agents)} primitive actions, got {len(raw)}."
)
sizes = self.action_sizes
result: list[int] = []
for agent, primitive in zip(agents, raw):
if not isinstance(primitive, Integral) or isinstance(primitive, bool):
raise TypeError(
f"Action for {agent!r} must be an integer, got {primitive!r}."
)
primitive = int(primitive)
if not 0 <= primitive < sizes[agent]:
raise ValueError(
f"Action {primitive} is outside the action space of {agent!r}."
)
result.append(primitive)
return tuple(result)
|
encode_joint_action
encode_joint_action(actions: Mapping[str, int] | Sequence[int]) -> int
Encode a full joint action in canonical Cartesian-product order.
Source code in masa/envs/multiagent/tabular_env.py
| def encode_joint_action(
self, actions: Mapping[str, int] | Sequence[int]
) -> int:
"""Encode a full joint action in canonical Cartesian-product order."""
action = self._joint_action_tuple(actions)
index = 0
for primitive, size in zip(action, self.action_sizes.values()):
index = index * size + primitive
return int(index)
|
decode_joint_action
decode_joint_action(index: int) -> dict[str, int]
Decode a canonical joint-action index to an agent-action mapping.
Source code in masa/envs/multiagent/tabular_env.py
| def decode_joint_action(self, index: int) -> dict[str, int]:
"""Decode a canonical joint-action index to an agent-action mapping."""
if not isinstance(index, Integral) or isinstance(index, bool):
raise TypeError("Joint-action index must be an integer.")
index = int(index)
if not 0 <= index < self.n_joint_actions:
raise ValueError(
f"Joint-action index {index} is outside [0, {self.n_joint_actions})."
)
sizes = tuple(self.action_sizes.values())
primitives = [0] * len(sizes)
remainder = index
for position in range(len(sizes) - 1, -1, -1):
remainder, primitives[position] = divmod(remainder, sizes[position])
return dict(zip(self.possible_agents, primitives))
|
legal_actions
legal_actions(state: int, agent: str) -> tuple[int, ...]
Legal primitive actions for an agent in a model state.
Override for state-dependent action availability. The default permits the
entire action space.
Source code in masa/envs/multiagent/tabular_env.py
| def legal_actions(self, state: int, agent: str) -> tuple[int, ...]:
"""Legal primitive actions for an agent in a model state.
Override for state-dependent action availability. The default permits the
entire action space.
"""
self._check_state(state)
try:
size = self.action_sizes[agent]
except KeyError as exc:
raise ValueError(f"Unknown agent: {agent!r}.") from exc
return tuple(range(size))
|
get_legal_actions
get_legal_actions(state: int, agent: str) -> tuple[int, ...]
Return a validated, sorted copy of legal_actions.
Source code in masa/envs/multiagent/tabular_env.py
| def get_legal_actions(self, state: int, agent: str) -> tuple[int, ...]:
"""Return a validated, sorted copy of :meth:`legal_actions`."""
state = self._check_state(state)
try:
size = self.action_sizes[agent]
except KeyError as exc:
raise ValueError(f"Unknown agent: {agent!r}.") from exc
raw = tuple(self.legal_actions(state, agent))
if not raw:
raise ValueError(
f"Agent {agent!r} has no legal action in model state {state}."
)
if any(
not isinstance(action, Integral)
or isinstance(action, bool)
or not 0 <= int(action) < size
for action in raw
):
raise ValueError(
f"Invalid legal actions for {agent!r} in state {state}: {raw!r}."
)
return tuple(sorted(set(map(int, raw))))
|
successors
successors(state: int, actions: Mapping[str, int] | Sequence[int]) -> tuple[int, ...]
Return every non-zero-probability successor of a legal joint action.
Source code in masa/envs/multiagent/tabular_env.py
| def successors(
self,
state: int,
actions: Mapping[str, int] | Sequence[int],
) -> tuple[int, ...]:
"""Return every non-zero-probability successor of a legal joint action."""
state = self._check_state(state)
action = self._joint_action_tuple(actions)
for agent, primitive in zip(self.possible_agents, action):
if primitive not in self.get_legal_actions(state, agent):
raise ValueError(
f"Action {primitive} is not legal for {agent!r} in state {state}."
)
action_index = self.encode_joint_action(action)
if self.has_successor_states_dict:
sparse = self.get_successor_states_dict()
assert sparse is not None
successor_states, transition_probs = sparse
ids = np.asarray(successor_states.get(state, ()))
probs = np.asarray(
transition_probs.get((state, action_index), ()),
dtype=np.float64,
)
elif self.has_transition_matrix:
matrix = np.asarray(self.get_transition_matrix())
expected = (self.n_states, self.n_states, self.n_joint_actions)
if matrix.shape != expected:
raise ValueError(
"Expected transition shape "
f"(next_state, state, joint_action)={expected}, got {matrix.shape}."
)
ids = np.arange(self.n_states, dtype=np.intp)
probs = np.asarray(matrix[:, state, action_index], dtype=np.float64)
else:
raise ValueError(
"TabularParallelEnv must expose a transition matrix or sparse "
"successor-state dictionaries."
)
if ids.ndim != 1 or (ids.size and ids.dtype.kind not in "iu"):
raise ValueError(f"Successors of state {state} must be integer IDs.")
ids = ids.astype(np.intp, copy=False)
if np.any((ids < 0) | (ids >= self.n_states)):
raise ValueError(f"Successor of state {state} is out of range.")
if probs.shape != (ids.size,):
raise ValueError(
f"Probability vector for state {state}, joint action {action} "
"has the wrong shape."
)
if not np.all(np.isfinite(probs)) or np.any(probs < 0):
raise ValueError(
f"Invalid probabilities at state {state}, joint action {action}."
)
if not np.isclose(probs.sum(), 1.0, rtol=1e-6, atol=1e-8):
raise ValueError(
f"Transition probabilities for state {state}, joint action "
f"{action} must sum to 1."
)
# No probability threshold: every positive-probability outcome matters.
result = tuple(sorted(set(map(int, ids[probs > 0]))))
if not result:
raise ValueError(
f"Missing transition support for state {state}, joint action {action}."
)
return result
|
get_state_id
Return the current finite model-state ID.
Subclasses with a non-integer runtime representation may override this,
but should still return the exact state indexing the transition model.
Source code in masa/envs/multiagent/tabular_env.py
| def get_state_id(self) -> int:
"""Return the current finite model-state ID.
Subclasses with a non-integer runtime representation may override this,
but should still return the exact state indexing the transition model.
"""
if self._state is None:
raise RuntimeError("The environment has no active tabular state.")
return self._check_state(self._state)
|
observations_from_state
observations_from_state(state: int) -> dict[str, Any]
Return each possible agent's observation for a finite model state.
The default supports fully observed environments where every agent's
observation is the same zero-based Discrete(n_states) state ID.
Environments with structured or local observations should override it.
Source code in masa/envs/multiagent/tabular_env.py
| def observations_from_state(self, state: int) -> dict[str, Any]:
"""Return each possible agent's observation for a finite model state.
The default supports fully observed environments where every agent's
observation is the same zero-based ``Discrete(n_states)`` state ID.
Environments with structured or local observations should override it.
"""
state = self._check_state(state)
observations: dict[str, Any] = {}
for agent in self.possible_agents:
space = self.observation_space(agent)
if (
not isinstance(space, spaces.Discrete)
or int(space.start) != 0
or int(space.n) != self.n_states
):
raise NotImplementedError(
f"{type(self).__name__}.observations_from_state() must be "
"implemented for structured or non-state observations."
)
observations[agent] = state
return observations
|